1<?php 2// Load the Twofactor_Auth_Module Class 3require_once(dirname(__FILE__).'/../twofactor/authmod.php'); 4// Load the PHP_YubiAuthenticator Class 5require_once(dirname(__FILE__).'/YubiAuthenticator.php'); 6 7/** 8 * If we turn this into a helper class, it can have its own language and settings files. 9 * Until then, we can only use per-user settings. 10 */ 11class helper_plugin_twofactoryubiauth extends Twofactor_Auth_Module { 12 /** 13 * The user must have verified their GA is configured correctly first. 14 */ 15 public function canUse($user = null){ 16 return ($this->_settingExists("verified", $user) && $this->getConf('enable') === 1); 17 } 18 19 /** 20 * This module does provide authentication functionality at the main login screen. 21 */ 22 public function canAuthLogin() { 23 return true; 24 } 25 26 /** 27 * This user will need to insert an OTP in order to configure YA. 28 */ 29 public function renderProfileForm(){ 30 global $conf,$USERINFO; 31 $elements = array(); 32 33 if ($this->_settingExists("publicID")) { // The user has a revokable YA Public ID. 34 35 // Show saved Public ID 36 $elements[] = form_makeTextField('yubiauth_showPublicID', $this->_settingGet("publicID"), $this->getLang('currentPublicID') , '', 'block', array('size'=>'12', 'readonly'=>'true')); 37 // User can remove Yubi Key here 38 $elements[] = form_makeCheckboxField('yubiauth_disable', '1', $this->getLang('killmodule'), '', 'block'); 39 } 40 else { // The user may opt in using YA. 41 // Provide a checkbox to create a personal secret. 42 $elements[] = form_makeCheckboxField('yubiauth_enable', '1', $this->getLang('enablemodule'), '', 'block'); 43 // Provide text field for setup OTP 44 $elements[] = form_makeTextField('yubiauth_setup', '', $this->getLang('needsetup'), '', 'block', array('size'=>'44')); 45 } 46 return $elements; 47 } 48 49 /** 50 * Process any user configuration. 51 */ 52 public function processProfileForm(){ 53 global $INPUT; 54 55 if ($this->_settingExists("publicID")) { 56 if ($INPUT->bool('yubiauth_disable', false)) { 57 $this->_settingDelete("publicID"); 58 $this->_settingDelete("verified"); 59 return true; 60 } 61 } else { 62 if ($INPUT->bool('yubiauth_enable', false)) { 63 $otp = $INPUT->str('yubiauth_setup', ''); 64 $checkResult = $this->processLogin($otp); 65 if ($checkResult) { 66 $this->_settingSet("publicID", substr($otp, 0, 12)); 67 $this->_settingSet("verified", true); 68 return 'verified'; 69 } else { 70 return 'failed'; 71 } 72 } 73 } 74 return null; 75 } 76 77 /** 78 * This module cannot send messages. 79 */ 80 public function canTransmitMessage() { return false; } 81 82 /** 83 * Transmit the message via email to the address on file. 84 * As a special case, configure the mail settings to send only via text. 85 */ 86 //public function transmitMessage($subject, $message); 87 88 /** 89 * This module authenticates against the configured API Server. 90 */ 91 public function processLogin($code, $user = null){ 92 if (strlen($code) < 44) { 93 # wrong length (12 Chars static + 32 Chars dynamic) 94 return false; 95 } 96 97 if ($this->_settingExists("publicID", $user)) { 98 if (!$this->validateUser($code, $user)) { 99 return false; 100 } 101 } 102 $yubiAuthenticator = new PHP_YubiAuthenticator(); 103 $response = array(); 104 $auth = $yubiAuthenticator->verifyCode($this->generateYubiURL($code),$response); 105 if($auth) { 106 if($code != $response["otp"]) 107 // otp is not the one we used 108 return false; 109 110 if(empty($this->getConf("clientSecret"))) { 111 return true; 112 } else { 113 return $this->checkSignature($response); 114 } 115 } else { 116 return false; 117 } 118 } 119 120 /** 121 * Check if provided $code uses the users Public ID 122 * @param $code OTP generated by Yubi Key 123 * @param $user current user 124 * @return boolean 125 */ 126 public function validateUser($code, $user) { 127 $publicID = $this->_settingGet("publicID",'', $user); 128 $static = substr($code,0,12); 129 return $publicID==$static; 130 } 131 132 /** 133 * Generate URL for API Request 134 * @param $code OTP generated by Yubi Key 135 * @return string url 136 */ 137 public function generateYubiURL($code) { 138 $validationServer = $this->getConf("validationServer"); 139 $url = $validationServer; 140 141 $data = array(); 142 $data["id"] = $this->getConf("clientID"); 143 if(empty($data["id"])) $data["id"]=87; 144 $data["otp"] = $code; 145 $data["nonce"] = $this->generateNonce(); 146 //https://api.yubico.com/wsapi/2.0/verify?otp=vvvvvvcucrlcietctckflvnncdgckubflugerlnr&id=87&timeout=8&sl=50&nonce=askjdnkajsndjkasndkjsnad 147 return sprintf("%s?%s", $url, http_build_query($data)); 148 } 149 150 /** 151 * Generate random nonce 152 * @param int $size length of desired nonce, must be between 16 and 40, otherwise goes for 32 153 * @return string 154 */ 155 private function generateNonce($size = 32) { 156 $chars = array("a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","0","1","2","3","4","5","6","7","8","9"); 157 $nonce = ""; 158 if($size >= 16 && $size <= 40) { 159 } else {$size = 32;} 160 for($i=0; $i < $size; $i++) { 161 $rand = rand(0,count($chars)-1); 162 $nonce .= $chars[$rand]; 163 } 164 return $nonce; 165 } 166 167 /** 168 * Generate signature over response values and compare to the servers signature 169 * @param $response array with key=>value pairs responded by the API server 170 * @return boolean 171 */ 172 private function checkSignature($response) { 173 $secret = base64_decode($this->getConf("clientSecret")); 174 $their_hash = $response["h"]; 175 unset($response["h"]); 176 ksort($response); 177 $sorted = ""; 178 foreach ($response as $k => $v) { 179 $sorted .= '&' . $k . '=' . $v; 180 } 181 $sorted = substr($sorted,1); 182 $hash = base64_encode(hash_hmac('sha1',$sorted,$secret, true)); 183 return ($hash == $their_hash); 184 } 185 186} 187