|
1 <?php |
|
2 |
|
3 function yms_add_yubikey($key, $otp, $client_id = false, $enabled = true, $any_client = false, $notes = false) |
|
4 { |
|
5 global $db, $session, $paths, $template, $plugins; // Common objects |
|
6 |
|
7 if ( $client_id === false ) |
|
8 $client_id = $session->user_id; |
|
9 |
|
10 $key = yms_tobinary($key); |
|
11 $otp = yms_tobinary($otp); |
|
12 |
|
13 if ( strlen($key) != 16 ) |
|
14 { |
|
15 return 'yms_err_addkey_invalid_key'; |
|
16 } |
|
17 |
|
18 if ( strlen($otp) != 22 ) |
|
19 { |
|
20 return 'yms_err_addkey_invalid_otp'; |
|
21 } |
|
22 |
|
23 $otpdata = yms_decode_otp($otp, $key); |
|
24 if ( $otpdata === false ) |
|
25 { |
|
26 return 'yms_err_addkey_invalid_otp'; |
|
27 } |
|
28 if ( !$otpdata['crc_good'] ) |
|
29 { |
|
30 return 'yms_err_addkey_crc_failed'; |
|
31 } |
|
32 |
|
33 // make sure it's not already in there |
|
34 $q = $db->sql_query('SELECT 1 FROM ' . table_prefix . "yms_yubikeys WHERE public_id = '{$otpdata['publicid']}';"); |
|
35 if ( !$q ) |
|
36 $db->_die(); |
|
37 |
|
38 if ( $db->numrows() > 0 ) |
|
39 { |
|
40 $db->free_result(); |
|
41 return 'yms_err_addkey_key_exists'; |
|
42 } |
|
43 $db->free_result(); |
|
44 |
|
45 $now = time(); |
|
46 $key = yms_hex_encode($key); |
|
47 |
|
48 $flags = 0; |
|
49 if ( $enabled ) |
|
50 $flags |= YMS_ENABLED; |
|
51 if ( $any_client ) |
|
52 $flags |= YMS_ANY_CLIENT; |
|
53 |
|
54 $notes = $notes ? $db->escape(strval($notes)) : ''; |
|
55 |
|
56 $q = $db->sql_query("INSERT INTO " . table_prefix . "yms_yubikeys(client_id, public_id, private_id, session_count, token_count, create_time, access_time, token_time, aes_secret, flags, notes) VALUES\n" |
|
57 . " ($client_id, '{$otpdata['publicid']}', '{$otpdata['privateid']}', {$otpdata['session']}, {$otpdata['count']}, $now, $now, {$otpdata['timestamp']}, '$key', $flags, '$notes');"); |
|
58 if ( !$q ) |
|
59 $db->_die(); |
|
60 |
|
61 return true; |
|
62 } |
|
63 |
|
64 function yms_chown_yubikey($otp, $client_id = false, $enabled = true, $any_client = false, $notes = false) |
|
65 { |
|
66 global $db, $session, $paths, $template, $plugins; // Common objects |
|
67 |
|
68 if ( $client_id === false ) |
|
69 $client_id = $session->user_id; |
|
70 |
|
71 $otp = yms_tobinary($otp); |
|
72 |
|
73 if ( strlen($otp) != 22 ) |
|
74 { |
|
75 return 'yms_err_addkey_invalid_otp'; |
|
76 } |
|
77 |
|
78 $public_id = yms_hex_encode(substr($otp, 0, 6)); |
|
79 |
|
80 // make sure it's already in there |
|
81 $q = $db->sql_query('SELECT id FROM ' . table_prefix . "yms_yubikeys WHERE public_id = '{$public_id}' AND client_id = 0;"); |
|
82 if ( !$q ) |
|
83 $db->_die(); |
|
84 |
|
85 if ( $db->numrows() < 1 ) |
|
86 { |
|
87 // this should never happen, as the OTP is put through validation before this function is called |
|
88 $db->free_result(); |
|
89 return 'yms_err_claimkey_owner_invalid'; |
|
90 } |
|
91 |
|
92 list($key_id) = $db->fetchrow_num(); |
|
93 $db->free_result(); |
|
94 |
|
95 $now = time(); |
|
96 |
|
97 $flags = 0; |
|
98 if ( $enabled ) |
|
99 $flags |= YMS_ENABLED; |
|
100 if ( $any_client ) |
|
101 $flags |= YMS_ANY_CLIENT; |
|
102 |
|
103 $notes = $notes ? $db->escape(strval($notes)) : ''; |
|
104 |
|
105 $q = $db->sql_query("UPDATE " . table_prefix . "yms_yubikeys SET flags = $flags, notes = '$notes', client_id = $client_id WHERE id = $key_id;"); |
|
106 if ( !$q ) |
|
107 $db->_die(); |
|
108 |
|
109 return true; |
|
110 } |
|
111 |
|
112 function yms_validate_custom_field($value, $otp, $url) |
|
113 { |
|
114 require_once(ENANO_ROOT . '/includes/http.php'); |
|
115 $url = strtr($url, array( |
|
116 '%c' => rawurlencode($value), |
|
117 '%o' => rawurlencode($otp) |
|
118 )); |
|
119 // do we need to sign this? |
|
120 if ( strstr($url, '%h') && ($key = getConfig('yms_claim_auth_key', false)) ) |
|
121 { |
|
122 list(, $signpart) = explode('?', $url); |
|
123 $signpart = preg_replace('/(&h=%h|^h=%h&)/', '', $signpart); |
|
124 $signpart = yms_ksort_url($signpart); |
|
125 |
|
126 $key = yms_tobinary($key); |
|
127 $key = yms_hex_encode($key); |
|
128 $hash = hmac_sha1($signpart, $key); |
|
129 $hash = yms_hex_decode($hash); |
|
130 $hash = base64_encode($hash); |
|
131 |
|
132 $url = str_replace('%h', rawurlencode($hash), $url); |
|
133 } |
|
134 |
|
135 // run authentication |
|
136 $result = yms_get_url($url); |
|
137 $result = yms_parse_auth_result($result, $key); |
|
138 |
|
139 if ( !$result['sig_valid'] ) |
|
140 return 'yubiauth_err_response_bad_signature'; |
|
141 |
|
142 if ( $result['status'] !== 'OK' ) |
|
143 { |
|
144 if ( preg_match('/^[A-Z_]+$/', $result['status']) ) |
|
145 return 'yubiauth_err_response_' . strtolower($result['status']); |
|
146 else |
|
147 return $result['status']; |
|
148 } |
|
149 |
|
150 // authentication is ok |
|
151 return true; |
|
152 } |
|
153 |
|
154 function yms_get_url($url) |
|
155 { |
|
156 require_once(ENANO_ROOT . '/includes/http.php'); |
|
157 |
|
158 $url = preg_replace('#^https?://#i', '', $url); |
|
159 if ( !preg_match('#^(\[?[a-z0-9-:]+(?:\.[a-z0-9-:]+\]?)*)(?::([0-9]+))?(/.*)$#U', $url, $match) ) |
|
160 { |
|
161 return 'invalid_auth_url'; |
|
162 } |
|
163 $server =& $match[1]; |
|
164 $port = ( !empty($match[2]) ) ? intval($match[2]) : 80; |
|
165 $uri =& $match[3]; |
|
166 try |
|
167 { |
|
168 $req = new Request_HTTP($server, $uri, 'GET', $port); |
|
169 $response = $req->get_response_body(); |
|
170 } |
|
171 catch ( Exception $e ) |
|
172 { |
|
173 return 'http_failed:' . $e->getMessage(); |
|
174 } |
|
175 |
|
176 if ( $req->response_code !== HTTP_OK ) |
|
177 return 'http_failed_status:' . $req->response_code; |
|
178 |
|
179 return $response; |
|
180 } |
|
181 |
|
182 function yms_parse_auth_result($result, $api_key = false) |
|
183 { |
|
184 $result = explode("\n", trim($result)); |
|
185 $arr = array(); |
|
186 foreach ( $result as $line ) |
|
187 { |
|
188 list($name) = explode('=', $line); |
|
189 $value = substr($line, strlen($name) + 1); |
|
190 $arr[$name] = $value; |
|
191 } |
|
192 // signature check |
|
193 if ( $api_key ) |
|
194 { |
|
195 $signarr = $arr; |
|
196 ksort($signarr); |
|
197 unset($signarr['h']); |
|
198 $signpart = array(); |
|
199 foreach ( $signarr as $name => $value ) |
|
200 $signpart[] = "{$name}={$value}"; |
|
201 |
|
202 $signpart = implode('&', $signpart); |
|
203 $api_key = yms_hex_encode(yms_tobinary($api_key)); |
|
204 $right_sig = base64_encode(yms_hex_decode( |
|
205 hmac_sha1($signpart, $api_key) |
|
206 )); |
|
207 $arr['sig_valid'] = ( $arr['h'] === $right_sig ); |
|
208 } |
|
209 else |
|
210 { |
|
211 $arr['sig_valid'] = true; |
|
212 } |
|
213 return $arr; |
|
214 } |
|
215 |
|
216 function yms_ksort_url($signpart) |
|
217 { |
|
218 $arr = array(); |
|
219 $values = explode('&', $signpart); |
|
220 foreach ( $values as $var ) |
|
221 { |
|
222 list($name) = explode('=', $var); |
|
223 $value = substr($var, strlen($name) + 1); |
|
224 $arr[$name] = $value; |
|
225 } |
|
226 ksort($arr); |
|
227 $result = array(); |
|
228 foreach ( $arr as $name => $value ) |
|
229 { |
|
230 $result[] = "{$name}={$value}"; |
|
231 } |
|
232 return implode('&', $result); |
|
233 } |
|
234 |
|
235 function yms_validate_otp($otp, $id) |
|
236 { |
|
237 global $db, $session, $paths, $template, $plugins; // Common objects |
|
238 |
|
239 $public_id = yms_modhex_decode(substr($otp, 0, 12)); |
|
240 if ( !$public_id ) |
|
241 { |
|
242 return 'BAD_OTP'; |
|
243 } |
|
244 // Just in case |
|
245 $public_id = $db->escape($public_id); |
|
246 |
|
247 $q = $db->sql_query("SELECT id, private_id, session_count, token_count, access_time, token_time, aes_secret, flags, client_id FROM " . table_prefix . "yms_yubikeys WHERE ( client_id = 0 or client_id = $id OR flags & " . YMS_ANY_CLIENT . " ) AND public_id = '$public_id';"); |
|
248 if ( !$q ) |
|
249 $db->_die(); |
|
250 |
|
251 if ( $db->numrows($q) < 1 ) |
|
252 { |
|
253 return 'NO_SUCH_KEY'; |
|
254 } |
|
255 |
|
256 list($yubikey_id, $private_id, $session_count, $token_count, $access_time, $token_time, $aes_secret, $flags, $client_id) = $db->fetchrow_num($q); |
|
257 $session_count = intval($session_count); |
|
258 $token_count = intval($token_count); |
|
259 $access_time = intval($access_time); |
|
260 $token_time = intval($token_time); |
|
261 |
|
262 // check flags |
|
263 if ( $client_id > 0 ) |
|
264 { |
|
265 if ( !($flags & YMS_ANY_CLIENT) ) |
|
266 { |
|
267 return 'NO_SUCH_KEY'; |
|
268 } |
|
269 if ( !($flags & YMS_ENABLED) ) |
|
270 { |
|
271 return 'NO_SUCH_KEY'; |
|
272 } |
|
273 } |
|
274 |
|
275 // decode the OTP |
|
276 $otp = yms_decode_otp($otp, $aes_secret); |
|
277 |
|
278 // check CRC |
|
279 if ( !$otp['crc_good'] ) |
|
280 { |
|
281 return 'BAD_OTP'; |
|
282 } |
|
283 |
|
284 // check private UID (avoids combining a whitelisted known public UID with the increment part of a malicious token) |
|
285 if ( $private_id !== $otp['privateid'] ) |
|
286 { |
|
287 return 'BAD_OTP'; |
|
288 } |
|
289 |
|
290 // check counters |
|
291 if ( $otp['session'] < $session_count ) |
|
292 { |
|
293 return 'REPLAYED_OTP'; |
|
294 } |
|
295 if ( $otp['session'] == $session_count && $otp['count'] <= $token_count ) |
|
296 { |
|
297 return 'REPLAYED_OTP'; |
|
298 } |
|
299 |
|
300 // update DB |
|
301 $q = $db->sql_query("UPDATE " . table_prefix . "yms_yubikeys SET session_count = {$otp['session']}, token_count = {$otp['count']}, access_time = " . time() . ", token_time = {$otp['timestamp']} WHERE id = $yubikey_id;"); |
|
302 if ( !$q ) |
|
303 $db->_die(); |
|
304 |
|
305 // check timestamp |
|
306 if ( $otp['session'] == $session_count ) |
|
307 { |
|
308 $expect_delta = time() - $access_time; |
|
309 // 8Hz Yubikey internal clock |
|
310 $actual_delta = intval(( $otp['timestamp'] - $token_time ) / 8); |
|
311 $fuzz = 150; |
|
312 if ( !yms_within($expect_delta, $actual_delta, $fuzz) ) |
|
313 { |
|
314 // if we have a likely wraparound, just pass it |
|
315 if ( !($token_time > 0xe80000 && $otp['timestamp'] < 0x800000) ) |
|
316 { |
|
317 return 'BAD_OTP'; |
|
318 } |
|
319 } |
|
320 // $debug_array = array('ts_debug_delta_expected' => $expect_delta, 'ts_debug_delta_received' => $actual_delta); |
|
321 } |
|
322 |
|
323 // looks like we're good |
|
324 return 'OK'; |
|
325 } |