Passed
Push — master ( 1c7137...b5ef5e )
by Pieter van der
03:26 queued 14s
created

OATH_OCRAParser::generateRandomBytes()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
c 0
b 0
f 0
dl 0
loc 34
rs 8.4444
cc 8
nc 16
nop 2
1
<?php
2
3
class OATH_OCRAParser {
4
5
	private $key = NULL;
0 ignored issues
show
introduced by
The private property $key is not used, and could be removed.
Loading history...
6
7
	private $OCRASuite = NULL;
8
9
	private $OCRAVersion = NULL;
10
11
	private $CryptoFunctionType = NULL;
12
	private $CryptoFunctionHash = NULL;
13
	private $CryptoFunctionHashLength = NULL;
14
	private $CryptoFunctionTruncation = NULL;
15
16
	private $C = FALSE;
17
	private $Q = FALSE;
18
	private $QType = 'N';
19
	private $QLength = 8;
20
21
	private $P = FALSE;
22
	private $PType = 'SHA1';
23
	private $PLength = 20;
24
25
	private $S = FALSE;
26
	private $SLength = 64;
27
28
	private $T = FALSE;
29
	private $TLength = 60; // 1M
30
	private $TPeriods = array('H' => 3600, 'M' => 60, 'S' => 1);
31
32
	private $supportedHashFunctions = array('SHA1' => 20, 'SHA256' => 32, 'SHA512' => 64);
33
34
35
	public function __construct($ocraSuite) {
36
		$this->parseOCRASuite($ocraSuite);
37
	}
38
39
	/**
40
	 * Inspired by https://github.com/bdauvergne/python-oath
41
	 */
42
	private function parseOCRASuite($ocraSuite) {
43
		if (!is_string($ocraSuite)) {
44
			throw new Exception('OCRASuite not in string format: ' . var_export($ocraSuite, TRUE));
45
		}
46
47
		$ocraSuite = strtoupper($ocraSuite);
48
		$this->OCRASuite = $ocraSuite;
49
50
		$s = explode(':', $ocraSuite);
51
		if (count($s) != 3) {
52
			throw new Exception('Invalid OCRASuite format: ' . var_export($ocraSuite, TRUE));
53
		}
54
55
		$algo = explode('-', $s[0]);
56
		if (count($algo) != 2) {
57
			throw new Exception('Invalid OCRA version: ' . var_export($s[0], TRUE));
58
		}
59
60
		if ($algo[0] !== 'OCRA') {
61
			throw new Exception('Unsupported OCRA algorithm: ' . var_export($algo[0], TRUE));
62
		}
63
64
		if ($algo[1] !== '1') {
65
			throw new Exception('Unsupported OCRA version: ' . var_export($algo[1], TRUE));
66
		}
67
		$this->OCRAVersion = $algo[1];
68
69
		$cf = explode('-', $s[1]);
70
		if (count($cf) != 3) {
71
			throw new Exception('Invalid OCRA suite crypto function: ' . var_export($s[1], TRUE));
72
		}
73
74
		if ($cf[0] !== 'HOTP') {
75
			throw new Exception('Unsupported OCRA suite crypto function: ' . var_export($cf[0], TRUE));
76
		}
77
		$this->CryptoFunctionType = $cf[0];
78
79
		if (!array_key_exists($cf[1], $this->supportedHashFunctions)) {
80
			throw new Exception('Unsupported hash function in OCRA suite crypto function: ' . var_export($cf[1], TRUE));
81
		}
82
		$this->CryptoFunctionHash = $cf[1];
83
		$this->CryptoFunctionHashLength = $this->supportedHashFunctions[$cf[1]];
84
85
		if (!preg_match('/^\d+$/', $cf[2]) || (($cf[2] < 4 || $cf[2] > 10) && $cf[2] != 0)) {
86
			throw new Exception('Invalid OCRA suite crypto function truncation length: ' . var_export($cf[2], TRUE));
87
		}
88
		$this->CryptoFunctionTruncation = intval($cf[2]);
89
90
		$di = explode('-', $s[2]);
91
		if (count($cf) == 0) {
92
			throw new Exception('Invalid OCRA suite data input: ' . var_export($s[2], TRUE));
93
		}
94
95
		$data_input = array();
96
		foreach($di as $elem) {
97
			$letter = $elem[0];
98
			if (array_key_exists($letter, $data_input)) {
99
				throw new Exception('Duplicate field in OCRA suite data input: ' . var_export($elem, TRUE));
100
			}
101
			$data_input[$letter] = 1;
102
103
			if ($letter === 'C' && strlen($elem) == 1) {
104
				$this->C = TRUE;
105
			} elseif ($letter === 'Q') {
106
				if (strlen($elem) == 1) {
107
					$this->Q = TRUE;
108
				} elseif (preg_match('/^Q([AHN])(\d+)$/', $elem, $match)) {
109
					$q_len = intval($match[2]);
110
					if ($q_len < 4 || $q_len > 64) {
111
						throw new Exception('Invalid OCRA suite data input question length: ' . var_export($q_len, TRUE));
112
					}
113
					$this->Q = TRUE;
114
					$this->QType = $match[1];
115
					$this->QLength = $q_len;
116
				} else {
117
					throw new Exception('Invalid OCRA suite data input question: ' . var_export($elem, TRUE));
118
				}
119
			} elseif ($letter === 'P') {
120
				if (strlen($elem) == 1) {
121
					$this->P = TRUE;
122
				} else {
123
					$p_algo = substr($elem, 1);
124
					if (!array_key_exists($p_algo, $this->supportedHashFunctions)) {
125
						throw new Exception('Unsupported OCRA suite PIN hash function: ' . var_export($elem, TRUE));
126
					}
127
					$this->P = TRUE;
128
					$this->PType = $p_algo;
129
					$this->PLength = $this->supportedHashFunctions[$p_algo];
130
				}
131
			} elseif ($letter === 'S') {
132
				if (strlen($elem) == 1) {
133
					$this->S = TRUE;
134
				} elseif (preg_match('/^S(\d+)$/', $elem, $match)) {
135
					$s_len = intval($match[1]);
136
					if ($s_len <= 0 || $s_len > 512) {
137
						throw new Exception('Invalid OCRA suite data input session information length: ' . var_export($s_len, TRUE));
138
					}
139
140
					$this->S = TRUE;
141
					$this->SLength = $s_len;
142
				} else {
143
					throw new Exception('Invalid OCRA suite data input session information length: ' . var_export($elem, TRUE));
144
				}
145
			} elseif ($letter === 'T') {
146
				if (strlen($elem) == 1) {
147
					$this->T = TRUE;
148
				} elseif (preg_match('/^T(\d+[HMS])+$/', $elem)) {
149
					preg_match_all('/(\d+)([HMS])/', $elem, $match);
150
151
					if (count($match[1]) !== count(array_unique($match[2]))) {
152
						throw new Exception('Duplicate definitions in OCRA suite data input timestamp: ' . var_export($elem, TRUE));
153
					}
154
155
					$length = 0;
156
					for ($i = 0; $i < count($match[1]); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
157
						$length += intval($match[1][$i]) * $this->TPeriods[$match[2][$i]];
158
					}
159
					if ($length <= 0) {
160
						throw new Exception('Invalid OCRA suite data input timestamp: ' . var_export($elem, TRUE));
161
					}
162
163
					$this->T = TRUE;
164
					$this->TLength = $length;
165
				} else {
166
					throw new Exception('Invalid OCRA suite data input timestamp: ' . var_export($elem, TRUE));
167
				}
168
			} else {
169
				throw new Exception('Unsupported OCRA suite data input field: ' . var_export($elem, TRUE));
170
			}
171
		}
172
173
		if (!$this->Q) {
174
			throw new Exception('OCRA suite data input question not defined: ' . var_export($s[2], TRUE));
175
		}
176
	}
177
178
	public function generateChallenge() {
179
		$q_length = $this->QLength;
180
		$q_type = $this->QType;
181
182
		$bytes = self::generateRandomBytes($q_length);
183
184
		switch($q_type) {
185
			case 'A':
186
				$challenge = base64_encode($bytes);
187
				$tr = implode("", unpack('H*', $bytes));
188
				$challenge = rtrim(strtr($challenge, '+/', $tr), '=');
189
				break;
190
			case 'H':
191
				$challenge = implode("", unpack('H*', $bytes));
192
				break;
193
			case 'N':
194
				$challenge = implode("", unpack('N*', $bytes));
195
				break;
196
			default:
197
				throw new Exception('Unsupported OCRASuite challenge type: ' . var_export($q_type, TRUE));
198
				break;
199
		}
200
201
		$challenge = substr($challenge, 0, $q_length);
202
203
		return $challenge;
204
	}
205
206
207
	public function generateSessionInformation() {
208
		if (!$this->S) {
209
			throw new Exception('Session information not defined in OCRASuite: ' . var_export($this->OCRASuite, TRUE));
210
		}
211
212
		$s_length = $this->SLength;
213
		$bytes = self::generateRandomBytes($s_length);
214
215
		// The OCRA spec doesn't specify that the session data should be hexadecimal.
216
		// However the reference implementation in the RFC does treat it as hex.
217
		$session = bin2hex($bytes);
218
		
219
		$session = substr($session, 0, $s_length);
220
		
221
		return $session;
222
	}
223
224
	/**
225
	 * Borrowed from SimpleSAMLPHP http://simplesamlphp.org/
226
	 */
227
	public static function generateRandomBytesMTrand($length) {
228
229
		/* Use mt_rand to generate $length random bytes. */
230
		$data = '';
231
		for($i = 0; $i < $length; $i++) {
232
			$data .= chr(mt_rand(0, 255));
233
		}
234
235
		return $data;
236
	}
237
238
239
	/**
240
	 * Borrowed from SimpleSAMLPHP http://simplesamlphp.org/
241
	 */
242
	public static function generateRandomBytes($length, $fallback = TRUE) {
243
		static $fp = NULL;
244
245
		if (function_exists('openssl_random_pseudo_bytes')) {
246
			return openssl_random_pseudo_bytes($length);
247
		}
248
249
		if($fp === NULL) {
250
			if (@file_exists('/dev/urandom')) {
251
				$fp = @fopen('/dev/urandom', 'rb');
252
			} else {
253
				$fp = FALSE;
254
			}
255
		}
256
257
		if($fp !== FALSE) {
258
			/* Read random bytes from /dev/urandom. */
259
			$data = fread($fp, $length);
260
			if($data === FALSE) {
261
				throw new Exception('Error reading random data.');
262
			}
263
			if(strlen($data) != $length) {
264
				if ($fallback) {
265
					$data = self::generateRandomBytesMTrand($length);
266
				} else {
267
					throw new Exception('Did not get requested number of bytes from random source. Requested (' . $length . ') got (' . strlen($data) . ')');
268
				}
269
			}
270
		} else {
271
			/* Use mt_rand to generate $length random bytes. */
272
			$data = self::generateRandomBytesMTrand($length);
273
		}
274
275
		return $data;
276
	}
277
278
279
	/**
280
	 * Constant time string comparison, see http://codahale.com/a-lesson-in-timing-attacks/
281
	 */
282
	public static function constEqual($s1, $s2) {
283
		if (strlen($s1) != strlen($s2)) {
284
			return FALSE;
285
		}
286
287
		$result = TRUE;
288
		$length = strlen($s1);
289
		for ($i = 0; $i < $length; $i++) {
290
			$result &= ($s1[$i] == $s2[$i]);
291
		}
292
293
		return (boolean)$result;
294
	}
295
296
}
297