Passed
Branch development (e0e718)
by Nils
04:45
created

Auth_Yubico::verify()   F

Complexity

Conditions 43
Paths > 20000

Size

Total Lines 207
Code Lines 120

Duplication

Lines 21
Ratio 10.14 %

Importance

Changes 0
Metric Value
cc 43
eloc 120
nc 561409
nop 5
dl 21
loc 207
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
    /**
3
     * Class for verifying Yubico One-Time-Passcodes
4
     *
5
     * @category    Auth
6
     * @package     Auth_Yubico
7
     * @author      Simon Josefsson <[email protected]>, Olov Danielson <[email protected]>
8
     * @copyright   2007-2015 Yubico AB
9
     * @license     http://opensource.org/licenses/bsd-license.php New BSD License
10
     * @version     2.0
11
     * @link        http://www.yubico.com/
12
     */
13
14
require_once 'PEAR.php';
15
16
/**
17
 * Class for verifying Yubico One-Time-Passcodes
18
 *
19
 * Simple example:
20
 * <code>
21
 * require_once 'Auth/Yubico.php';
22
 * $otp = "ccbbddeertkrctjkkcglfndnlihhnvekchkcctif";
23
 *
24
 * # Generate a new id+key from https://api.yubico.com/get-api-key/
25
 * $yubi = new Auth_Yubico('42', 'FOOBAR=');
26
 * $auth = $yubi->verify($otp);
27
 * if (PEAR::isError($auth)) {
28
 *    print "<p>Authentication failed: " . $auth->getMessage();
29
 *    print "<p>Debug output from server: " . $yubi->getLastResponse();
30
 * } else {
31
 *    print "<p>You are authenticated!";
32
 * }
33
 * </code>
34
 */
35
class Auth_Yubico
36
{
37
    /**#@+
38
	 * @access private
39
	 */
40
41
    /**
42
     * Yubico client ID
43
     * @var string
44
     */
45
    var $_id;
46
47
    /**
48
     * Yubico client key
49
     * @var string
50
     */
51
    var $_key;
52
53
    /**
54
     * URL part of validation server
55
     * @var string
56
     */
57
    var $_url;
58
59
    /**
60
     * List with URL part of validation servers
61
     * @var array
62
     */
63
    var $_url_list;
64
65
    /**
66
     * index to _url_list
67
     * @var int
68
     */
69
    var $_url_index;
70
71
    /**
72
     * Last query to server
73
     * @var string
74
     */
75
    var $_lastquery;
76
77
    /**
78
     * Response from server
79
     * @var string
80
     */
81
    var $_response;
82
83
    /**
84
     * Flag whether to use https or not.
85
     * @var boolean
86
     */
87
    var $_https;
88
89
    /**
90
     * Flag whether to verify HTTPS server certificates or not.
91
     * @var boolean
92
     */
93
    var $_httpsverify;
94
95
    /**
96
     * Constructor
97
     *
98
     * Sets up the object
99
     * @param    string  $id     The client identity
100
     * @param    string  $key    The client MAC key (optional)
101
     * @param    integer $https  Flag whether to use https (optional)
102
     * @param    integer $httpsverify  Flag whether to use verify HTTPS
103
     *                                 server certificates (optional,
104
     *                                 default true)
105
     * @access public
106
     */
107
    public function __construct($id, $key = '', $https = 0, $httpsverify = 1)
108
    {
109
        $this->_id = $id;
110
        $this->_key = base64_decode($key);
111
        $this->_https = $https;
112
        $this->_httpsverify = $httpsverify;
113
    }
114
115
    /**
116
     * Specify to use a different URL part for verification.
117
     * The default is "api.yubico.com/wsapi/verify".
118
     *
119
     * @param  string $url  New server URL part to use
120
     * @access public
121
     */
122
    function setURLpart($url)
123
    {
124
        $this->_url = $url;
125
    }
126
127
    /**
128
     * Get URL part to use for validation.
129
     *
130
     * @return string  Server URL part
131
     * @access public
132
     */
133
    function getURLpart()
134
    {
135
        if ($this->_url) {
136
            return $this->_url;
137
        } else {
138
            return "api.yubico.com/wsapi/verify";
139
        }
140
    }
141
142
143
    /**
144
     * Get next URL part from list to use for validation.
145
     *
146
     * @return mixed string with URL part of false if no more URLs in list
147
     * @access public
148
     */
149
    function getNextURLpart()
150
    {
151
        if ($this->_url_list) {
152
            $url_list = $this->_url_list;
153
        } else {
154
            $url_list = array('api.yubico.com/wsapi/2.0/verify',
155
                    'api2.yubico.com/wsapi/2.0/verify', 
156
                    'api3.yubico.com/wsapi/2.0/verify', 
157
                    'api4.yubico.com/wsapi/2.0/verify',
158
                    'api5.yubico.com/wsapi/2.0/verify');
159
        }
160
	  
161
        if ($this->_url_index >= count($url_list)) {
162
            return false;
163
        } else {
164
            return $url_list[$this->_url_index++];
165
        }
166
    }
167
168
    /**
169
     * Resets index to URL list
170
     *
171
     * @access public
172
     */
173
    function URLreset()
174
    {
175
        $this->_url_index = 0;
176
    }
177
178
    /**
179
     * Add another URLpart.
180
     *
181
     * @access public
182
     */
183
    function addURLpart($URLpart) 
184
    {
185
        $this->_url_list[] = $URLpart;
186
    }
187
	
188
    /**
189
     * Return the last query sent to the server, if any.
190
     *
191
     * @return string  Request to server
192
     * @access public
193
     */
194
    function getLastQuery()
195
    {
196
        return $this->_lastquery;
197
    }
198
199
    /**
200
     * Return the last data received from the server, if any.
201
     *
202
     * @return string  Output from server
203
     * @access public
204
     */
205
    function getLastResponse()
206
    {
207
        return $this->_response;
208
    }
209
210
    /**
211
     * Parse input string into password, yubikey prefix,
212
     * ciphertext, and OTP.
213
     *
214
     * @param  string    Input string to parse
215
     * @param  string    Optional delimiter re-class, default is '[:]'
216
     * @param string $str
217
     * @return array     Keyed array with fields
218
     * @access public
219
     */
220
    function parsePasswordOTP($str, $delim = '[:]')
221
    {
222
        if (!preg_match("/^((.*)".$delim.")?".
223
                "(([cbdefghijklnrtuv]{0,16})".
224
                "([cbdefghijklnrtuv]{32}))$/i",
225
                $str, $matches)) {
226
        /* Dvorak? */
227
        if (!preg_match("/^((.*)".$delim.")?".
228
                "(([jxe\.uidchtnbpygk]{0,16})".
229
                "([jxe\.uidchtnbpygk]{32}))$/i",
230
                $str, $matches)) {
231
            return false;
232
        } else {
233
            $ret['otp'] = strtr($matches[3], "jxe.uidchtnbpygk", "cbdefghijklnrtuv");
234
        }
235
        } else {
236
        $ret['otp'] = $matches[3];
237
        }
238
        $ret['password'] = $matches[2];
239
        $ret['prefix'] = $matches[4];
240
        $ret['ciphertext'] = $matches[5];
241
        return $ret;
242
    }
243
244
    /* TODO? Add functions to get parsed parts of server response? */
245
246
    /**
247
     * Parse parameters from last response
248
     *
249
     * example: getParameters("timestamp", "sessioncounter", "sessionuse");
250
     *
251
     * @param  array @parameters  Array with strings representing
252
     *                            parameters to parse
253
     * @return array  parameter array from last response
254
     * @access public
255
     */
256
    function getParameters($parameters)
257
    {
258
        if ($parameters == null) {
259
        $parameters = array('timestamp', 'sessioncounter', 'sessionuse');
260
        }
261
        $param_array = array();
262
        foreach ($parameters as $param) {
263
        if (!preg_match("/".$param."=([0-9]+)/", $this->_response, $out)) {
264
            return PEAR::raiseError('Could not parse parameter '.$param.' from response');
265
        }
266
        $param_array[$param] = $out[1];
267
        }
268
        return $param_array;
269
    }
270
271
    /**
272
     * Verify Yubico OTP against multiple URLs
273
     * Protocol specification 2.0 is used to construct validation requests
274
     *
275
     * @param string $token        Yubico OTP
276
     * @param int $use_timestamp   1=>send request with &timestamp=1 to
277
     *                             get timestamp and session information
278
     *                             in the response
279
     * @param boolean $wait_for_all  If true, wait until all
280
     *                               servers responds (for debugging)
281
     * @param string $sl           Sync level in percentage between 0
282
     *                             and 100 or "fast" or "secure".
283
     * @param int $timeout         Max number of seconds to wait
284
     *                             for responses
285
     * @return mixed               PEAR error on error, true otherwise
286
     * @access public
287
     */
288
    function verify($token, $use_timestamp = null, $wait_for_all = False,
289
            $sl = null, $timeout = null)
290
    {
291
        /* Construct parameters string */
292
        $ret = $this->parsePasswordOTP($token);
293
        if (!$ret) {
294
        return PEAR::raiseError('Could not parse Yubikey OTP');
295
        }
296
        $params = array('id'=>$this->_id, 
297
                'otp'=>$ret['otp'],
298
                'nonce'=>md5(uniqid(rand())));
299
        /* Take care of protocol version 2 parameters */
300
        if ($use_timestamp) {
301
            $params['timestamp'] = 1;
302
        }
303
        if ($sl) {
304
            $params['sl'] = $sl;
305
        }
306
        if ($timeout) {
307
            $params['timeout'] = $timeout;
308
        }
309
        ksort($params);
310
        $parameters = '';
311
        foreach ($params as $p=>$v) {
312
            $parameters .= "&".$p."=".$v;
313
        }
314
        $parameters = ltrim($parameters, "&");
315
	  
316
        /* Generate signature. */
317
        if ($this->_key <> "") {
318
        $signature = base64_encode(hash_hmac('sha1', $parameters,
319
                            $this->_key, true));
320
        $signature = preg_replace('/\+/', '%2B', $signature);
321
        $parameters .= '&h='.$signature;
322
        }
323
324
        /* Generate and prepare request. */
325
        $this->_lastquery = null;
326
        $this->URLreset();
327
        $mh = curl_multi_init();
328
        $ch = array();
329
        while ($URLpart = $this->getNextURLpart()) 
330
        {
331
            /* Support https. */
332
            if ($this->_https) {
333
        $query = "https://";
334
            } else {
335
        $query = "http://";
336
            }
337
            $query .= $URLpart."?".$parameters;
338
339
            if ($this->_lastquery) { $this->_lastquery .= " "; }
340
            $this->_lastquery .= $query;
341
	      
342
            $handle = curl_init($query);
343
            curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");
344
            curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
345
            if (!$this->_httpsverify) {
346
        curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
347
        curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0);
348
            }
349
            curl_setopt($handle, CURLOPT_FAILONERROR, true);
350
            /* If timeout is set, we better apply it here as well
351
	         in case the validation server fails to follow it. 
352
	      */ 
353
            if ($timeout) {
354
                curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);
355
            }
356
            curl_multi_add_handle($mh, $handle);
357
	      
358
            $ch[(int) $handle] = $handle;
359
        }
360
361
        /* Execute and read request. */
362
        $this->_response = null;
363
        $replay = False;
364
        $valid = False;
365
        do {
366
        /* Let curl do its work. */
367
        while (($mrc = curl_multi_exec($mh, $active))
368
            == CURLM_CALL_MULTI_PERFORM)
369
            ;
370
371
        while ($info = curl_multi_info_read($mh)) {
372
            if ($info['result'] == CURLE_OK) {
373
374
        /* We have a complete response from one server. */
375
376
        $str = curl_multi_getcontent($info['handle']);
377
        $cinfo = curl_getinfo($info['handle']);
378
		
379
        if ($wait_for_all) { # Better debug info
380
            $this->_response .= 'URL='.$cinfo['url']."\n"
381
            . $str."\n";
382
        }
383
384
        if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) {
385
            $status = $out[1];
386
387
            /* 
388
		   * There are 3 cases.
389
		   *
390
		   * 1. OTP or Nonce values doesn't match - ignore
391
		   * response.
392
		   *
393
		   * 2. We have a HMAC key.  If signature is invalid -
394
		   * ignore response.  Return if status=OK or
395
		   * status=REPLAYED_OTP.
396
		   *
397
		   * 3. Return if status=OK or status=REPLAYED_OTP.
398
		   */
399
            if (!preg_match("/otp=".$params['otp']."/", $str) ||
400
              !preg_match("/nonce=".$params['nonce']."/", $str)) {
401
            /* Case 1. Ignore response. */
402
            } elseif ($this->_key <> "") {
403
            /* Case 2. Verify signature first */
404
            $rows = explode("\r\n", trim($str));
405
            $response = array();
406
            while (list($key, $val) = each($rows)) {
407
                /* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
408
                $val = preg_replace('/=/', '#', $val, 1);
409
                $row = explode("#", $val);
410
                $response[$row[0]] = $row[1];
411
            }
412
		    
413
            $parameters = array('nonce', 'otp', 'sessioncounter', 'sessionuse', 'sl', 'status', 't', 'timeout', 'timestamp');
414
            sort($parameters);
415
            $check = Null;
416
            foreach ($parameters as $param) {
417
                if (array_key_exists($param, $response)) {
418
            if ($check) {
419
                $check = $check.'&';
420
            }
421
            $check = $check.$param.'='.$response[$param];
422
                }
423
            }
424
425
            $checksignature =
426
                base64_encode(hash_hmac('sha1', utf8_encode($check),
427
                            $this->_key, true));
428
429
            if ($response['h'] == $checksignature) {
430
                if ($status == 'REPLAYED_OTP') {
431
            if (!$wait_for_all) { $this->_response = $str; }
432
            $replay = True;
433
                } 
434
                if ($status == 'OK') {
435
            if (!$wait_for_all) { $this->_response = $str; }
436
            $valid = True;
437
                }
438
            }
439
            } else {
440
            /* Case 3. We check the status directly */
441
            if ($status == 'REPLAYED_OTP') {
442
                if (!$wait_for_all) { $this->_response = $str; }
443
                $replay = True;
444
            } 
445
            if ($status == 'OK') {
446
                if (!$wait_for_all) { $this->_response = $str; }
447
                $valid = True;
448
            }
449
            }
450
        }
451
        if (!$wait_for_all && ($valid || $replay)) 
452
            {
453
            /* We have status=OK or status=REPLAYED_OTP, return. */
454
            foreach ($ch as $h) {
455
                curl_multi_remove_handle($mh, $h);
456
                curl_close($h);
457
            }
458
            curl_multi_close($mh);
459
            if ($replay) {
460
                return PEAR::raiseError('REPLAYED_OTP');
461
            }
462
            if ($valid) {
463
                return true;
464
            }
465
            return PEAR::raiseError($status);
466
            }
467
		
468
        curl_multi_remove_handle($mh, $info['handle']);
469
        curl_close($info['handle']);
470
        unset ($ch[(int) $info['handle']]);
471
            }
472
            curl_multi_select($mh);
473
        }
474
        } while ($active);
475
476
        /* Typically this is only reached for wait_for_all=true or
477
	   * when the timeout is reached and there is no
478
	   * OK/REPLAYED_REQUEST answer (think firewall).
479
	   */
480
481
        foreach ($ch as $h) {
482
        curl_multi_remove_handle($mh, $h);
483
        curl_close($h);
484
        }
485
        curl_multi_close($mh);
486
	  
487
        if ($replay) {
488
            return PEAR::raiseError('REPLAYED_OTP');
489
        }
490
        if ($valid) {
491
            return true;
492
        }
493
        return PEAR::raiseError('NO_VALID_ANSWER');
494
    }
495
}
496
?>
497