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