Passed
Push — develop ( 1e050e...a909ab )
by Pieter van der
01:06 queued 12s
created

OATH_OCRAParser   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 234
Duplicated Lines 0 %

Test Coverage

Coverage 56.29%

Importance

Changes 0
Metric Value
wmc 46
eloc 140
c 0
b 0
f 0
dl 0
loc 234
ccs 67
cts 119
cp 0.5629
rs 8.72

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A generateChallenge() 0 26 4
D parseOCRASuite() 0 133 38
A constEqual() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like OATH_OCRAParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OATH_OCRAParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
require_once(__DIR__ . "/../Random.php");
4
5
class OATH_OCRAParser {
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
    /**
36
     * @param String $ocraSuite The RFC 6287 OCRA suite to parse. E.g. "OCRA-1:HOTP-SHA1-6:QH10-S"
37
     * @throws Exception
38
     */
39 8
    public function __construct(String $ocraSuite) {
40 8
		$this->parseOCRASuite($ocraSuite);
41 8
	}
42
43
	/**
44
	 * Inspired by https://github.com/bdauvergne/python-oath
45
     *
46
     * @throws Exception
47
	 */
48 8
	private function parseOCRASuite($ocraSuite) {
49 8
		if (!is_string($ocraSuite)) {
50
			throw new Exception('OCRASuite not in string format: ' . var_export($ocraSuite, TRUE));
51
		}
52
53 8
		$ocraSuite = strtoupper($ocraSuite);
54 8
		$this->OCRASuite = $ocraSuite;
55
56 8
		$s = explode(':', $ocraSuite);
57 8
		if (count($s) != 3) {
58
			throw new Exception('Invalid OCRASuite format: ' . var_export($ocraSuite, TRUE));
59
		}
60
61 8
		$algo = explode('-', $s[0]);
62 8
		if (count($algo) != 2) {
63
			throw new Exception('Invalid OCRA version: ' . var_export($s[0], TRUE));
64
		}
65
66 8
		if ($algo[0] !== 'OCRA') {
67
			throw new Exception('Unsupported OCRA algorithm: ' . var_export($algo[0], TRUE));
68
		}
69
70 8
		if ($algo[1] !== '1') {
71
			throw new Exception('Unsupported OCRA version: ' . var_export($algo[1], TRUE));
72
		}
73 8
		$this->OCRAVersion = $algo[1];
74
75 8
		$cf = explode('-', $s[1]);
76 8
		if (count($cf) != 3) {
77
			throw new Exception('Invalid OCRA suite crypto function: ' . var_export($s[1], TRUE));
78
		}
79
80 8
		if ($cf[0] !== 'HOTP') {
81
			throw new Exception('Unsupported OCRA suite crypto function: ' . var_export($cf[0], TRUE));
82
		}
83 8
		$this->CryptoFunctionType = $cf[0];
84
85 8
		if (!array_key_exists($cf[1], $this->supportedHashFunctions)) {
86
			throw new Exception('Unsupported hash function in OCRA suite crypto function: ' . var_export($cf[1], TRUE));
87
		}
88 8
		$this->CryptoFunctionHash = $cf[1];
89 8
		$this->CryptoFunctionHashLength = $this->supportedHashFunctions[$cf[1]];
90
91 8
		if (!preg_match('/^\d+$/', $cf[2]) || (($cf[2] < 4 || $cf[2] > 10) && $cf[2] != 0)) {
92
			throw new Exception('Invalid OCRA suite crypto function truncation length: ' . var_export($cf[2], TRUE));
93
		}
94 8
		$this->CryptoFunctionTruncation = intval($cf[2]);
95
96 8
		$di = explode('-', $s[2]);
97 8
		if (count($cf) == 0) {
98
			throw new Exception('Invalid OCRA suite data input: ' . var_export($s[2], TRUE));
99
		}
100
101 8
		$data_input = array();
102 8
		foreach($di as $elem) {
103 8
			$letter = $elem[0];
104 8
			if (array_key_exists($letter, $data_input)) {
105
				throw new Exception('Duplicate field in OCRA suite data input: ' . var_export($elem, TRUE));
106
			}
107 8
			$data_input[$letter] = 1;
108
109 8
			if ($letter === 'C' && strlen($elem) == 1) {
110
				$this->C = TRUE;
111 8
			} elseif ($letter === 'Q') {
112 8
				if (strlen($elem) == 1) {
113
					$this->Q = TRUE;
114 8
				} elseif (preg_match('/^Q([AHN])(\d+)$/', $elem, $match)) {
115 8
					$q_len = intval($match[2]);
116 8
					if ($q_len < 4 || $q_len > 64) {
117
						throw new Exception('Invalid OCRA suite data input question length: ' . var_export($q_len, TRUE));
118
					}
119 8
					$this->Q = TRUE;
120 8
					$this->QType = $match[1];
121 8
					$this->QLength = $q_len;
122
				} else {
123 8
					throw new Exception('Invalid OCRA suite data input question: ' . var_export($elem, TRUE));
124
				}
125 8
			} elseif ($letter === 'P') {
126
				if (strlen($elem) == 1) {
127
					$this->P = TRUE;
128
				} else {
129
					$p_algo = substr($elem, 1);
130
					if (!array_key_exists($p_algo, $this->supportedHashFunctions)) {
131
						throw new Exception('Unsupported OCRA suite PIN hash function: ' . var_export($elem, TRUE));
132
					}
133
					$this->P = TRUE;
134
					$this->PType = $p_algo;
135
					$this->PLength = $this->supportedHashFunctions[$p_algo];
136
				}
137 8
			} elseif ($letter === 'S') {
138 8
				if (strlen($elem) == 1) {
139 8
					$this->S = TRUE;
140
				} elseif (preg_match('/^S(\d+)$/', $elem, $match)) {
141
					$s_len = intval($match[1]);
142
					if ($s_len <= 0 || $s_len > 512) {
143
						throw new Exception('Invalid OCRA suite data input session information length: ' . var_export($s_len, TRUE));
144
					}
145
146
					$this->S = TRUE;
147
					$this->SLength = $s_len;
148
				} else {
149 8
					throw new Exception('Invalid OCRA suite data input session information length: ' . var_export($elem, TRUE));
150
				}
151
			} elseif ($letter === 'T') {
152
				if (strlen($elem) == 1) {
153
					$this->T = TRUE;
154
				} elseif (preg_match('/^T(\d+[HMS])+$/', $elem)) {
155
					preg_match_all('/(\d+)([HMS])/', $elem, $match);
156
157
					if (count($match[1]) !== count(array_unique($match[2]))) {
158
						throw new Exception('Duplicate definitions in OCRA suite data input timestamp: ' . var_export($elem, TRUE));
159
					}
160
161
					$length = 0;
162
					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...
163
						$length += intval($match[1][$i]) * $this->TPeriods[$match[2][$i]];
164
					}
165
					if ($length <= 0) {
166
						throw new Exception('Invalid OCRA suite data input timestamp: ' . var_export($elem, TRUE));
167
					}
168
169
					$this->T = TRUE;
170
					$this->TLength = $length;
171
				} else {
172
					throw new Exception('Invalid OCRA suite data input timestamp: ' . var_export($elem, TRUE));
173
				}
174
			} else {
175 8
				throw new Exception('Unsupported OCRA suite data input field: ' . var_export($elem, TRUE));
176
			}
177
		}
178
179 8
		if (!$this->Q) {
180
			throw new Exception('OCRA suite data input question not defined: ' . var_export($s[2], TRUE));
181
		}
182 8
	}
183
184
    /** Generate an OCRA challenge question according to the ocra suite specified in the constructor
185
     * @return The randomly generated OCRA question
186
     * @throws Exception
187
     *
188
     * The required format and length of the challenge question are specified in the "Q" part OCRA suite:
189
     * OCRA-1:...:QH10-... means 10 hex digits
190
     * OCRA-1:...:QN08-... means 8 decimal digits
191
     * OCRA-1:...:QA06-... means 6 ASCII digits
192
     * Note that the question string is the exact question string a specified in the OCRA strandard (RFC 6287)
193
     * The challenge is not yet hex encoded as expected by OCRA::generateOCRA()
194
     */
195 4
	public function generateChallenge() : String {
196 4
		$q_length = $this->QLength;
197 4
		$q_type = $this->QType;
198
199 4
        $bytes = Tiqr_Random::randomBytes($q_length);
200
201 4
		switch($q_type) {
202 4
			case 'A':
203
				$challenge = base64_encode($bytes);
204
				$tr = implode("", unpack('H*', $bytes));
205
				$challenge = rtrim(strtr($challenge, '+/', $tr), '=');
206
				break;
207 4
			case 'H':
208 4
				$challenge = implode("", unpack('H*', $bytes));
209 4
				break;
210
			case 'N':
211
				$challenge = implode("", unpack('N*', $bytes));
212
				break;
213
			default:
214
				throw new Exception('Unsupported OCRASuite challenge type: ' . var_export($q_type, TRUE));
215
				break;
216
		}
217
218 4
		$challenge = substr($challenge, 0, $q_length);
219
220 4
		return $challenge;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $challenge returns the type string which is incompatible with the documented return type The.
Loading history...
221
	}
222
223
224
	/**
225
	 * Constant time string comparison, see http://codahale.com/a-lesson-in-timing-attacks/
226
	 */
227 3
	public static function constEqual(string $s1, string $s2): bool {
228 3
		if (strlen($s1) != strlen($s2)) {
229 1
			return FALSE;
230
		}
231
232 3
		$result = TRUE;
233 3
		$length = strlen($s1);
234 3
		for ($i = 0; $i < $length; $i++) {
235 3
			$result &= ($s1[$i] == $s2[$i]);
236
		}
237
238 3
		return (boolean)$result;
239
	}
240
241
}
242