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