|
1 <?php |
|
2 |
|
3 define('YK_SEC_NORMAL_USERNAME', 1); |
|
4 define('YK_SEC_NORMAL_PASSWORD', 2); |
|
5 define('YK_SEC_ELEV_USERNAME', 4); |
|
6 define('YK_SEC_ELEV_PASSWORD', 8); |
|
7 define('YK_SEC_ALLOW_NO_OTP', 16); |
|
8 |
|
9 define('YK_DEFAULT_VERIFY_URL', 'http://api.yubico.com/wsapi/verify'); |
|
10 |
|
11 function generate_yubikey_field($name = 'yubikey_otp', $value = false) |
|
12 { |
|
13 global $lang; |
|
14 |
|
15 $fid = substr(sha1(microtime() . mt_rand()), 0, 12); |
|
16 $class = $value ? 'wasfull' : 'wasempty'; |
|
17 $html = '<input id="yubifield' . $fid . '" class="' . $class . '" type="hidden" name="' . $name . '" value="' . ( is_string($value) ? $value : '' ) . '" />'; |
|
18 $html .= '<noscript><input type="text" name="' . $name . '" class="yubikey_noscript" value="' . ( is_string($value) ? $value : '' ) . '" /> </noscript>'; |
|
19 if ( $value ) |
|
20 { |
|
21 $html .= '<span id="yubistat' . $fid . '" class="yubikey_status enrolled">' . $lang->get('yubiauth_ctl_status_enrolled') . '</span>'; |
|
22 $atext = $lang->get('yubiauth_ctl_btn_change_key'); |
|
23 $classadd = ' abutton_green'; |
|
24 } |
|
25 else |
|
26 { |
|
27 $html .= '<span id="yubistat' . $fid . '" class="yubikey_status empty">' . $lang->get('yubiauth_ctl_status_empty') . '</span>'; |
|
28 $atext = $lang->get('yubiauth_ctl_btn_enroll'); |
|
29 $classadd = ''; |
|
30 } |
|
31 |
|
32 $html .= ' <span class="yubikey_pubkey">'; |
|
33 if ( !empty($value) ) |
|
34 $html .= htmlspecialchars(substr($value, 0, 12)); |
|
35 $html .= '</span> '; |
|
36 |
|
37 $html .= ' <a class="abutton' . $classadd . ' yubikey_enroll" onclick="yk_mb_init(\'yubifield' . $fid . '\', \'yubistat' . $fid . '\'); return false;" href="#enroll">' . $atext . '</a>'; |
|
38 if ( $value ) |
|
39 { |
|
40 $html .= ' <a class="abutton abutton_red yubikey_enroll" onclick="yk_clear(\'yubifield' . $fid . '\', \'yubistat' . $fid . '\'); return false;" href="#enroll">' |
|
41 . $lang->get('yubiauth_ctl_btn_clear') . |
|
42 '</a>'; |
|
43 } |
|
44 |
|
45 return $html; |
|
46 } |
|
47 |
|
48 function yubikey_validate_otp($otp) |
|
49 { |
|
50 $api_key = getConfig('yubikey_api_key'); |
|
51 $api_id = getConfig('yubikey_api_key_id'); |
|
52 // Don't require an API key or user ID to be installed if we're using local YMS |
|
53 if ( !(getConfig('yubikey_use_local_yms', 0) && defined('YMS_INSTALLED')) && (!$api_key || !$api_id) ) |
|
54 { |
|
55 return array( |
|
56 'success' => false, |
|
57 'error' => 'missing_api_key' |
|
58 ); |
|
59 } |
|
60 if ( !preg_match('/^[cbdefghijklnrtuv]{44}$/', $otp) ) |
|
61 { |
|
62 return array( |
|
63 'success' => false, |
|
64 'error' => 'otp_invalid_chars' |
|
65 ); |
|
66 } |
|
67 // are we using local YMS? |
|
68 if ( getConfig('yubikey_use_local_yms', 0) && defined('YMS_INSTALLED') ) |
|
69 { |
|
70 $result = yms_validate_otp($otp, $api_id); |
|
71 if ( $result == 'OK' ) |
|
72 { |
|
73 return array( |
|
74 'success' => true |
|
75 ); |
|
76 } |
|
77 else |
|
78 { |
|
79 return array( |
|
80 'success' => false, |
|
81 'error' => strtolower("response_{$result}") |
|
82 ); |
|
83 } |
|
84 } |
|
85 // make HTTP request |
|
86 require_once( ENANO_ROOT . '/includes/http.php' ); |
|
87 $auth_url = getConfig('yubikey_auth_server', YK_DEFAULT_VERIFY_URL); |
|
88 $auth_url = preg_replace('#^https?://#i', '', $auth_url); |
|
89 if ( !preg_match('#^(\[?[a-z0-9-:]+(?:\.[a-z0-9-:]+\]?)*)(?::([0-9]+))?(/.*)$#U', $auth_url, $match) ) |
|
90 { |
|
91 return array( |
|
92 'success' => false, |
|
93 'error' => 'invalid_auth_url' |
|
94 ); |
|
95 } |
|
96 $auth_server =& $match[1]; |
|
97 $auth_port = ( !empty($match[2]) ) ? intval($match[2]) : 80; |
|
98 $auth_uri =& $match[3]; |
|
99 try |
|
100 { |
|
101 $req = new Request_HTTP($auth_server, $auth_uri, 'GET', $auth_port); |
|
102 $req->add_get('id', strval($api_id)); |
|
103 $req->add_get('otp', $otp); |
|
104 $req->add_get('h', yubikey_sign($req->parms_get)); |
|
105 |
|
106 $response = $req->get_response_body(); |
|
107 } |
|
108 catch ( Exception $e ) |
|
109 { |
|
110 return array( |
|
111 'success' => false, |
|
112 'error' => 'http_failed', |
|
113 'http_error' => $e->getMessage() |
|
114 ); |
|
115 } |
|
116 |
|
117 if ( $req->response_code != HTTP_OK ) |
|
118 { |
|
119 return array( |
|
120 'success' => false, |
|
121 'error' => 'http_response_error' |
|
122 ); |
|
123 } |
|
124 $response = trim($response); |
|
125 if ( !preg_match_all('/^([a-z0-9_]+)=(.*?)\r?$/m', $response, $matches) ) |
|
126 { |
|
127 return array( |
|
128 'success' => false, |
|
129 'error' => 'malformed_response' |
|
130 ); |
|
131 } |
|
132 $response = array(); |
|
133 foreach ( $matches[0] as $i => $_ ) |
|
134 { |
|
135 $response[$matches[1][$i]] = $matches[2][$i]; |
|
136 } |
|
137 // make sure we have a status |
|
138 if ( !isset($response['status']) ) |
|
139 { |
|
140 return array( |
|
141 'success' => false, |
|
142 'error' => 'response_missing_status' |
|
143 ); |
|
144 } |
|
145 // verify response signature |
|
146 // MISSING_PARAMETER is the ONLY situation under which an unsigned response is acceptable |
|
147 if ( $response['status'] !== 'MISSING_PARAMETER' ) |
|
148 { |
|
149 if ( !isset($response['h']) ) |
|
150 { |
|
151 return array( |
|
152 'success' => false, |
|
153 'error' => 'response_missing_sig' |
|
154 ); |
|
155 } |
|
156 if ( yubikey_sign($response) !== $response['h'] ) |
|
157 { |
|
158 return array( |
|
159 'success' => false, |
|
160 'error' => 'response_invalid_sig' |
|
161 ); |
|
162 } |
|
163 } |
|
164 if ( $response['status'] === 'OK' ) |
|
165 { |
|
166 if ( yubikey_verify_timestamp($response['t']) ) |
|
167 { |
|
168 return array( |
|
169 'success' => true |
|
170 ); |
|
171 } |
|
172 else |
|
173 { |
|
174 return array( |
|
175 'success' => false, |
|
176 'error' => 'timestamp_check_failed' |
|
177 ); |
|
178 } |
|
179 } |
|
180 else |
|
181 { |
|
182 return array( |
|
183 'success' => false, |
|
184 'error' => strtolower("response_{$response['status']}") |
|
185 ); |
|
186 } |
|
187 } |
|
188 |
|
189 function yubikey_sign($arr, $use_api_key = false) |
|
190 { |
|
191 static $api_key = false; |
|
192 |
|
193 ksort($arr); |
|
194 |
|
195 if ( !$use_api_key ) |
|
196 { |
|
197 if ( !$api_key ) |
|
198 { |
|
199 $api_key = getConfig('yubikey_api_key'); |
|
200 $api_key = hexencode(base64_decode($api_key), '', ''); |
|
201 } |
|
202 $use_api_key = $api_key; |
|
203 } |
|
204 /* |
|
205 else |
|
206 { |
|
207 $use_api_key = hexencode(base64_decode($use_api_key), '', ''); |
|
208 } |
|
209 */ |
|
210 |
|
211 foreach ( array('h', 'title', 'auth', 'do') as $key ) |
|
212 { |
|
213 if ( isset($arr[$key]) ) |
|
214 unset($arr[$key]); |
|
215 } |
|
216 |
|
217 $req = array(); |
|
218 foreach ( $arr as $key => $val ) |
|
219 { |
|
220 $req[] = "$key=$val"; |
|
221 } |
|
222 $req = implode('&', $req); |
|
223 |
|
224 $sig = hmac_sha1($req, $use_api_key); |
|
225 $sig = hexdecode($sig); |
|
226 $sig = base64_encode($sig); |
|
227 |
|
228 return $sig; |
|
229 } |
|
230 |
|
231 /** |
|
232 * Validate the timestamp returned in a Yubico API response. Borrowed from Drupal and backported for friendliness with earlier versions of PHP. |
|
233 * @param string Yubico timestamp |
|
234 * @return bool True if valid, false otherwise |
|
235 */ |
|
236 |
|
237 function yubikey_verify_timestamp($timestamp) |
|
238 { |
|
239 $tolerance = intval(getConfig('yubikey_api_ts_tolerance', 150)); |
|
240 |
|
241 $now = time(); |
|
242 $timestamp_seconds = yk_strtotime($timestamp); |
|
243 |
|
244 if ( !$timestamp || !$now || !$timestamp_seconds ) |
|
245 { |
|
246 return false; |
|
247 } |
|
248 |
|
249 if ( ( $timestamp_seconds + $tolerance ) > $now && ( $timestamp_seconds - $tolerance ) < $now ) |
|
250 { |
|
251 return true; |
|
252 } |
|
253 |
|
254 return false; |
|
255 } |
|
256 |
|
257 function yk_strtotime($timestamp) |
|
258 { |
|
259 if ( !preg_match('/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:Z[0-9]+)?$/', $timestamp, $match) ) |
|
260 return 0; |
|
261 |
|
262 $hour = intval($match[4]); |
|
263 $minute = intval($match[5]); |
|
264 $second = intval($match[6]); |
|
265 $month = intval($match[2]); |
|
266 $day = intval($match[3]); |
|
267 $year = intval($match[1]); |
|
268 |
|
269 return gmmktime($hour, $minute, $second, $month, $day, $year); |
|
270 } |
|
271 |
|
272 $plugins->attachHook('compile_template', 'yubikey_attach_headers($this);'); |
|
273 |
|
274 function yubikey_attach_headers(&$template) |
|
275 { |
|
276 global $db, $session, $paths, $template, $plugins; // Common objects |
|
277 |
|
278 if ( getConfig('yubikey_enable', '1') != '1' ) |
|
279 return true; |
|
280 |
|
281 $template->add_header('<script type="text/javascript" src="' . scriptPath . '/plugins/yubikey/yubikey.js"></script>'); |
|
282 $template->add_header('<link rel="stylesheet" type="text/css" href="' . scriptPath . '/plugins/yubikey/yubikey.css" />'); |
|
283 // config option for all users have yubikey |
|
284 $user_flags = 0; |
|
285 $yk_enabled = 0; |
|
286 if ( $session->user_logged_in ) |
|
287 { |
|
288 $q = $db->sql_query('SELECT COUNT(y.yubi_uid) > 0, u.user_yubikey_flags FROM ' . table_prefix . "yubikey AS y LEFT JOIN " . table_prefix . "users AS u ON ( u.user_id = y.user_id ) WHERE y.user_id = {$session->user_id} GROUP BY u.user_id, u.user_yubikey_flags;"); |
|
289 if ( !$q ) |
|
290 $db->_die(); |
|
291 |
|
292 list($yk_enabled, $user_flags) = $db->fetchrow_num(); |
|
293 $db->free_result(); |
|
294 } |
|
295 $yk_enabled = intval($yk_enabled); |
|
296 $user_flags = intval($user_flags); |
|
297 |
|
298 $template->add_header('<script type="text/javascript">var yk_reg_require_otp = ' . getConfig('yubikey_reg_require_otp', '0') . '; var yk_user_enabled = ' . $yk_enabled . '; var yk_user_flags = ' . $user_flags . ';</script>'); |
|
299 } |
|
300 |