OATH_OCRAParser   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 234
Duplicated Lines 0 %

Test Coverage

Coverage 54.31%

Importance

Changes 0
Metric Value
wmc 46
eloc 140
c 0
b 0
f 0
dl 0
loc 234
ccs 63
cts 116
cp 0.5431
rs 8.72

4 Methods

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

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