Test Failed
Push — master ( b5ef5e...9e488e )
by Michiel
17:47 queued 02:26
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
require_once ( __DIR__ . "/../Tiqr/Random.php");
4
5
class OATH_OCRAParser {
6
7
	private $key = NULL;
0 ignored issues
show
introduced by
The private property $key is not used, and could be removed.
Loading history...
8
9
	private $OCRASuite = NULL;
10
11
	private $OCRAVersion = NULL;
12
13
	private $CryptoFunctionType = NULL;
14
	private $CryptoFunctionHash = NULL;
15
	private $CryptoFunctionHashLength = NULL;
16
	private $CryptoFunctionTruncation = NULL;
17
18
	private $C = FALSE;
19
	private $Q = FALSE;
20
	private $QType = 'N';
21
	private $QLength = 8;
22
23
	private $P = FALSE;
24
	private $PType = 'SHA1';
25
	private $PLength = 20;
26
27
	private $S = FALSE;
28
	private $SLength = 64;
29
30
	private $T = FALSE;
31
	private $TLength = 60; // 1M
32
	private $TPeriods = array('H' => 3600, 'M' => 60, 'S' => 1);
33
34
	private $supportedHashFunctions = array('SHA1' => 20, 'SHA256' => 32, 'SHA512' => 64);
35
36
37
	public function __construct($ocraSuite) {
38
		$this->parseOCRASuite($ocraSuite);
39
	}
40
41
	/**
42
	 * Inspired by https://github.com/bdauvergne/python-oath
43
	 */
44
	private function parseOCRASuite($ocraSuite) {
45
		if (!is_string($ocraSuite)) {
46
			throw new Exception('OCRASuite not in string format: ' . var_export($ocraSuite, TRUE));
47
		}
48
49
		$ocraSuite = strtoupper($ocraSuite);
50
		$this->OCRASuite = $ocraSuite;
51
52
		$s = explode(':', $ocraSuite);
53
		if (count($s) != 3) {
54
			throw new Exception('Invalid OCRASuite format: ' . var_export($ocraSuite, TRUE));
55
		}
56
57
		$algo = explode('-', $s[0]);
58
		if (count($algo) != 2) {
59
			throw new Exception('Invalid OCRA version: ' . var_export($s[0], TRUE));
60
		}
61
62
		if ($algo[0] !== 'OCRA') {
63
			throw new Exception('Unsupported OCRA algorithm: ' . var_export($algo[0], TRUE));
64
		}
65
66
		if ($algo[1] !== '1') {
67
			throw new Exception('Unsupported OCRA version: ' . var_export($algo[1], TRUE));
68
		}
69
		$this->OCRAVersion = $algo[1];
70
71
		$cf = explode('-', $s[1]);
72
		if (count($cf) != 3) {
73
			throw new Exception('Invalid OCRA suite crypto function: ' . var_export($s[1], TRUE));
74
		}
75
76
		if ($cf[0] !== 'HOTP') {
77
			throw new Exception('Unsupported OCRA suite crypto function: ' . var_export($cf[0], TRUE));
78
		}
79
		$this->CryptoFunctionType = $cf[0];
80
81
		if (!array_key_exists($cf[1], $this->supportedHashFunctions)) {
82
			throw new Exception('Unsupported hash function in OCRA suite crypto function: ' . var_export($cf[1], TRUE));
83
		}
84
		$this->CryptoFunctionHash = $cf[1];
85
		$this->CryptoFunctionHashLength = $this->supportedHashFunctions[$cf[1]];
86
87
		if (!preg_match('/^\d+$/', $cf[2]) || (($cf[2] < 4 || $cf[2] > 10) && $cf[2] != 0)) {
88
			throw new Exception('Invalid OCRA suite crypto function truncation length: ' . var_export($cf[2], TRUE));
89
		}
90
		$this->CryptoFunctionTruncation = intval($cf[2]);
91
92
		$di = explode('-', $s[2]);
93
		if (count($cf) == 0) {
94
			throw new Exception('Invalid OCRA suite data input: ' . var_export($s[2], TRUE));
95
		}
96
97
		$data_input = array();
98
		foreach($di as $elem) {
99
			$letter = $elem[0];
100
			if (array_key_exists($letter, $data_input)) {
101
				throw new Exception('Duplicate field in OCRA suite data input: ' . var_export($elem, TRUE));
102
			}
103
			$data_input[$letter] = 1;
104
105
			if ($letter === 'C' && strlen($elem) == 1) {
106
				$this->C = TRUE;
107
			} elseif ($letter === 'Q') {
108
				if (strlen($elem) == 1) {
109
					$this->Q = TRUE;
110
				} elseif (preg_match('/^Q([AHN])(\d+)$/', $elem, $match)) {
111
					$q_len = intval($match[2]);
112
					if ($q_len < 4 || $q_len > 64) {
113
						throw new Exception('Invalid OCRA suite data input question length: ' . var_export($q_len, TRUE));
114
					}
115
					$this->Q = TRUE;
116
					$this->QType = $match[1];
117
					$this->QLength = $q_len;
118
				} else {
119
					throw new Exception('Invalid OCRA suite data input question: ' . var_export($elem, TRUE));
120
				}
121
			} elseif ($letter === 'P') {
122
				if (strlen($elem) == 1) {
123
					$this->P = TRUE;
124
				} else {
125
					$p_algo = substr($elem, 1);
126
					if (!array_key_exists($p_algo, $this->supportedHashFunctions)) {
127
						throw new Exception('Unsupported OCRA suite PIN hash function: ' . var_export($elem, TRUE));
128
					}
129
					$this->P = TRUE;
130
					$this->PType = $p_algo;
131
					$this->PLength = $this->supportedHashFunctions[$p_algo];
132
				}
133
			} elseif ($letter === 'S') {
134
				if (strlen($elem) == 1) {
135
					$this->S = TRUE;
136
				} elseif (preg_match('/^S(\d+)$/', $elem, $match)) {
137
					$s_len = intval($match[1]);
138
					if ($s_len <= 0 || $s_len > 512) {
139
						throw new Exception('Invalid OCRA suite data input session information length: ' . var_export($s_len, TRUE));
140
					}
141
142
					$this->S = TRUE;
143
					$this->SLength = $s_len;
144
				} else {
145
					throw new Exception('Invalid OCRA suite data input session information length: ' . var_export($elem, TRUE));
146
				}
147
			} elseif ($letter === 'T') {
148
				if (strlen($elem) == 1) {
149
					$this->T = TRUE;
150
				} elseif (preg_match('/^T(\d+[HMS])+$/', $elem)) {
151
					preg_match_all('/(\d+)([HMS])/', $elem, $match);
152
153
					if (count($match[1]) !== count(array_unique($match[2]))) {
154
						throw new Exception('Duplicate definitions in OCRA suite data input timestamp: ' . var_export($elem, TRUE));
155
					}
156
157
					$length = 0;
158
					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...
159
						$length += intval($match[1][$i]) * $this->TPeriods[$match[2][$i]];
160
					}
161
					if ($length <= 0) {
162
						throw new Exception('Invalid OCRA suite data input timestamp: ' . var_export($elem, TRUE));
163
					}
164
165
					$this->T = TRUE;
166
					$this->TLength = $length;
167
				} else {
168
					throw new Exception('Invalid OCRA suite data input timestamp: ' . var_export($elem, TRUE));
169
				}
170
			} else {
171
				throw new Exception('Unsupported OCRA suite data input field: ' . var_export($elem, TRUE));
172
			}
173
		}
174
175
		if (!$this->Q) {
176
			throw new Exception('OCRA suite data input question not defined: ' . var_export($s[2], TRUE));
177
		}
178
	}
179
180
	public function generateChallenge() {
181
		$q_length = $this->QLength;
182
		$q_type = $this->QType;
183
184
        $bytes = Tiqr_Random::randomBytes($q_length);
185
186
		switch($q_type) {
187
			case 'A':
188
				$challenge = base64_encode($bytes);
189
				$tr = implode("", unpack('H*', $bytes));
190
				$challenge = rtrim(strtr($challenge, '+/', $tr), '=');
191
				break;
192
			case 'H':
193
				$challenge = implode("", unpack('H*', $bytes));
194
				break;
195
			case 'N':
196
				$challenge = implode("", unpack('N*', $bytes));
197
				break;
198
			default:
199
				throw new Exception('Unsupported OCRASuite challenge type: ' . var_export($q_type, TRUE));
200
				break;
201
		}
202
203
		$challenge = substr($challenge, 0, $q_length);
204
205
		return $challenge;
206
	}
207
208
209
	public function generateSessionInformation() {
210
		if (!$this->S) {
211
			throw new Exception('Session information not defined in OCRASuite: ' . var_export($this->OCRASuite, TRUE));
212
		}
213
214
		$s_length = $this->SLength;
215
        $bytes = Tiqr_Random::randomBytes($s_length);
216
217
		// The OCRA spec doesn't specify that the session data should be hexadecimal.
218
		// However the reference implementation in the RFC does treat it as hex.
219
		$session = bin2hex($bytes);
220
		
221
		$session = substr($session, 0, $s_length);
222
		
223
		return $session;
224
	}
225
226
227
	/**
228
	 * Constant time string comparison, see http://codahale.com/a-lesson-in-timing-attacks/
229
	 */
230
	public static function constEqual($s1, $s2) {
231
		if (strlen($s1) != strlen($s2)) {
232
			return FALSE;
233
		}
234
235
		$result = TRUE;
236
		$length = strlen($s1);
237
		for ($i = 0; $i < $length; $i++) {
238
			$result &= ($s1[$i] == $s2[$i]);
239
		}
240
241
		return (boolean)$result;
242
	}
243
244
}
245