Test Failed
Pull Request — develop (#53)
by Pieter van der
13:18
created

OCRA   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Test Coverage

Coverage 84.8%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 55
eloc 131
c 7
b 0
f 0
dl 0
loc 311
ccs 106
cts 125
cp 0.848
rs 6

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 1 1
A _hexStr2Bytes() 0 19 6
A _hmac() 0 7 2
A _oath_truncate() 0 23 2
F generateOCRA() 0 197 44

How to fix   Complexity   

Complex Class

Complex classes like OCRA 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 OCRA, and based on these observations, apply Extract Interface, too.

1
<?php 
2
/**
3
 * This file is part of the ocra-implementations package.
4
 *
5
 * More information: https://github.com/SURFnet/ocra-implementations/
6
 *
7
 * @author Ivo Jansch <[email protected]>
8
 * 
9
 * @license See the LICENSE file in the source distribution
10
 */
11
12
/**
13
 * This a PHP port of the example implementation of the 
14
 * OATH OCRA algorithm.
15
 * Visit www.openauthentication.org for more information.
16
 *
17
 * @author Johan Rydell, PortWise (original Java)
18
 * @author Ivo Jansch, Egeniq (PHP port)
19
 */
20
class OCRA {
21
22
    private function __construct() {
23
        
24
    }
25
26
    /**
27
     * This method uses the hmac_hash function to provide the crypto
28
     * algorithm.
29
     * HMAC computes a Hashed Message Authentication Code with the
30
     * crypto hash algorithm as a parameter.
31
     *
32
     * @param String crypto     the crypto algorithm (sha1, sha256 or sha512)
33
     * @param String keyBytes   the bytes to use for the HMAC key
34
     * @param String text       the message or text to be authenticated.
0 ignored issues
show
Bug introduced by
The type text was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
35
     * @throws Exception
36
     */
37 108
    private static function _hmac(string $crypto, string $keyBytes, string $text) : string
38
    {
39 108
         $hash = hash_hmac($crypto, $text, $keyBytes);
40 108
         if (false === $hash) {
41
             throw new Exception("calculating hash_hmac failed");
42
         }
43 108
         return $hash;
44
    }
45
46
    /**
47
     * This method converts HEX string to Byte[]
48
     *
49
     * @param string $hex The hex string to decode
50
     * @param int $maxBytes The maximum length of the resulting decoded string
51
     * @param string $parameterName A descriptive name for the $hex parameter, used in exception error message
52
     * @return String a string with the decoded raw bytes of $hex
53
     * @throws InvalidArgumentException
54
     *
55
     * The length of the returned string will always be length($hex)/2 bytes
56
     * Note that $maxBytes is the max length of the returned string, not the max number of hex digits in $hex
57
     */
58 128
    private static function _hexStr2Bytes(string $hex, int $maxBytes, string $parameterName) : string
59
    {
60 128
        $len = strlen($hex);
61 128
        if ( ($len !== 0) && (! ctype_xdigit($hex)) ) {
62 7
            throw new InvalidArgumentException("Parameter '$parameterName' contains non hex digits");
63
        }
64 125
        if ( $len % 2 !== 0 ) {
65 1
            throw new InvalidArgumentException("Parameter '$parameterName' contains odd number of hex digits");
66
        }
67 124
        if ( $len > $maxBytes * 2) {
68 6
            throw new InvalidArgumentException("Parameter '$parameterName' too long");
69
        }
70
        // hex2bin logs PHP warnings when $hex contains invalid characters or has uneven length. Because we
71
        // check for these conditions above hex2bin() should always be silent
72 121
        $res=hex2bin($hex);
73 121
        if (false === $res) {
74
            throw new InvalidArgumentException("Parameter '$parameterName' could not be decoded");
75
        }
76 121
        return $res;
77
    }
78
79
80
    /**
81
     * Calculate the OCRA (RFC 6287) response for the given set of parameters
82
     * This implementation uses the same interface as the Java reference implementation from the RFC
83
     *
84
     * @param string $ocraSuite the OCRASuite
85
     * @param string $key the shared OCRA secret, HEX encoded
86
     * @param string $counter the counter (C), HEX encoded
87
     * @param string $question the challenge question (Q), HEX encoded
88
     * @param string $password the Hashed version of PIN/password (P), HEX encoded
89
     * @param string $sessionInformation the session information (S), HEX encoded
90
     * @param string $timeStamp the timestamp (T), HEX encoded
91
     *
92
     * @return string The Response, A numeric String in base 10 that includes number of digits specified in the OCRASuite
93
     * @throws InvalidArgumentException|Exception InvalidArgumentException is thrown when a HEX encoded parameter does not contain hex or when it exceeds its maximum length
94
     *
95
     * Note: The OCRA secret and the parameters C, Q, P, S and T must be provided as HEX encoded strings.
96
     *       How each parameter must be encoded is specified in the RFC
97
     *
98
     * In addition to the RFC reference implementation this implementation supports using "-S" in OCRASuite as an
99
     * alternative to "-S064"
100
     */
101 119
    static function generateOCRA(string $ocraSuite,
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
102
                                 string $key,
103
                                 string $counter,
104
                                 string $question,
105
                                 string $password,
106
                                 string $sessionInformation,
107
                                 string $timeStamp) : string
108
    {
109 119
        $codeDigits = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $codeDigits is dead and can be removed.
Loading history...
110 119
        $crypto = "";
111 119
        $result = null;
112 119
        $ocraSuiteLength = strlen($ocraSuite);
113 119
        $counterLength = 0;
114 119
        $questionLength = 0;
115 119
        $passwordLength = 0;
116
117 119
        $sessionInformationLength = 0;
118 119
        $timeStampLength = 0;
119
120
        // Parse the cryptofunction
121 119
        // The cryptofucntion is defined as HOTP-<hash function>-<t>, where
122 119
        // <hash function> is one of sha1, sha256 or sha512
123 119
        // <t> is the truncation length: 0 (no truncation), 4-10
124
        $components = explode(":", $ocraSuite);
125 119
        $cryptoFunction = $components[1];
126 83
        $dataInput = strtolower($components[2]); // lower here so we can do case insensitive comparisons
127 119
128 17
        if(stripos($cryptoFunction, "hotp-sha1")!==false)
129 119
            $crypto = "sha1";
130 19
        elseif(stripos($cryptoFunction, "hotp-sha256")!==false)
131
            $crypto = "sha256";
132 119
        elseif(stripos($cryptoFunction, "hotp-sha512")!==false)
133
            $crypto = "sha512";
134
        else {
135
            throw new InvalidArgumentException('Unsupported OCRA CryptoFunction');
136 119
        }
137
138 24
        // The Cryptofucntion must ha a truncation of 0, 4-10
139 23
        $codeDigits_str = substr($cryptoFunction, strrpos($cryptoFunction, "-")+1);
140 24
        if (! ctype_digit($codeDigits_str)) {
141
            throw new InvalidArgumentException('Unsupported OCRA CryptoFunction');
142
        }
143 119
        $codeDigits = (integer)$codeDigits_str;
144 119
        if (($codeDigits != 0) && (($codeDigits < 4) || ($codeDigits > 10))) {
145 119
            throw new InvalidArgumentException('Unsupported OCRA CryptoFunction');
146 118
        }
147 119
                
148
        // The size of the byte array message to be encrypted
149
        // Counter
150
        if($dataInput[0] == "c" ) {
151 119
            // Fix the length of the HEX string
152 17
            while(strlen($counter) < 16)
153 1
                $counter = "0" . $counter;
154 17
            $counterLength=8;
155
        }
156
        // Question
157 119
        if($dataInput[0] == "q" ||
158
                stripos($dataInput, "-q")!==false) {
159
            while(strlen($question) < 256)
160
                $question = $question . "0";
161
            $questionLength=128;
162
        }
163 119
164
        // Password
165
        if(stripos($dataInput, "psha1")!==false) {
166
            while(strlen($password) < 40)
167
                $password = "0" . $password;
168
            $passwordLength=20;
169
        }
170 119
    
171 32
        if(stripos($dataInput, "psha256")!==false) {
172 32
            while(strlen($password) < 64)
173
                $password = "0" . $password;
174 32
            $passwordLength=32;
175 87
        }
176
        
177
        if(stripos($dataInput, "psha512")!==false) {
178
            while(strlen($password) < 128)
179
                $password = "0" . $password;
180 87
            $passwordLength=64;
181
        }
182
        
183
        // sessionInformation
184
        if(stripos($dataInput, "s064") !==false) {
185 87
            while(strlen($sessionInformation) < 128)
186
                $sessionInformation = "0" . $sessionInformation;
187
188
            $sessionInformationLength=64;
189
        } else if(stripos($dataInput, "s128") !==false) {
190 87
            while(strlen($sessionInformation) < 256)
191
                $sessionInformation = "0" . $sessionInformation;
192
        
193
            $sessionInformationLength=128;
194
        } else if(stripos($dataInput, "s256") !==false) {
195
            while(strlen($sessionInformation) < 512)
196
                $sessionInformation = "0" . $sessionInformation;
197
        
198
            $sessionInformationLength=256;
199 41
        } else if(stripos($dataInput, "s512") !==false) {
200 40
            while(strlen($sessionInformation) < 128)
201
                $sessionInformation = "0" . $sessionInformation;
202 41
        
203
            $sessionInformationLength=64;
204
        } else if (stripos($dataInput, "-s") !== false ) {
205
            // deviation from spec. Officially 's' without a length indicator is not in the reference implementation.
206
            // RFC is ambigious. However we have supported this in Tiqr since day 1, so we continue to support it.
207
208 119
            // See the format of the datainput below. "[]" denotes optional.
209 119
            // Because Q is mandatory, s will always be preceded by the separator "-". Matching "-s" is required
210 7
            // to prevent matching the "s" in the password input e.g. "psha1".
211 6
            // [C] | QFxx | [PH | Snnn | TG] : Challenge-Response computation
212 7
            // [C] | QFxx | [PH | TG] : Plain Signature computation
213
            while(strlen($sessionInformation) < 128)
214
                $sessionInformation = "0" . $sessionInformation;
215
            
216
            $sessionInformationLength=64;
217 119
        }
218
        
219 119
        
220 119
             
221
        // TimeStamp
222
        if($dataInput[0] == "t" ||
223
                stripos($dataInput, "-t") !== false) {
224 119
            while(strlen($timeStamp) < 16)
225
                $timeStamp = "0" . $timeStamp;
226
            $timeStampLength=8;
227
        }
228 119
229 24
        // Put the bytes of "ocraSuite" parameters into the message
230 22
        
231 22
        $msg = array_fill(0,$ocraSuiteLength+$counterLength+$questionLength+$passwordLength+$sessionInformationLength+$timeStampLength+1, 0);
232
                
233
        for($i=0;$i<strlen($ocraSuite);$i++) {
234
            $msg[$i] = $ocraSuite[$i];
235
        }
236
        
237
        // Delimiter
238 117
        $msg[strlen($ocraSuite)] = "\0";
239 117
240 115
        // Put the bytes of "Counter" to the message
241 115
        // Input is HEX encoded
242
        if($counterLength > 0 ) {
243
            $bArray = self::_hexStr2Bytes($counter, $counterLength, 'counter');
244
            for ($i=0;$i<strlen($bArray);$i++) {
245
                $msg [$i + $ocraSuiteLength + 1] = $bArray[$i];
246
            }
247 115
        }
248 17
249 15
250 15
        // Put the bytes of "question" to the message
251
        // Input is text encoded
252
        if($questionLength > 0 ) {
253
            $bArray = self::_hexStr2Bytes($question, $questionLength, 'question');
254
            for ($i=0;$i<strlen($bArray);$i++) {
255
                $msg [$i + $ocraSuiteLength + 1 + $counterLength] = $bArray[$i];
256 113
            }
257 71
        }
258 69
259 69
        // Put the bytes of "password" to the message
260
        // Input is HEX encoded
261
        if($passwordLength > 0){
262
            $bArray = self::_hexStr2Bytes($password, $passwordLength, 'password');
263
            for ($i=0;$i<strlen($bArray);$i++) {
264
                $msg [$i + $ocraSuiteLength + 1 + $counterLength + $questionLength] = $bArray[$i];
265 111
            }
266 7
        }
267 5
268 5
        // Put the bytes of "sessionInformation" to the message
269
        // Input is HEX encoded
270
        if($sessionInformationLength > 0 ){
271
            $bArray = self::_hexStr2Bytes($sessionInformation, $sessionInformationLength, 'sessionInformation');
272 109
            for ($i=0;$i<strlen($bArray);$i++) {
273
                $msg [$i + $ocraSuiteLength + 1 + $counterLength + $questionLength + $passwordLength] = $bArray[$i];
274 108
            }
275
        }
276 108
277
        // Put the bytes of "time" to the message
278 108
        // Input is HEX encoded value of minutes
279
        if($timeStampLength > 0){
280 108
            $bArray = self::_hexStr2Bytes($timeStamp, $timeStampLength, 'timeStamp');
281
            for ($i=0;$i<strlen($bArray);$i++) {
282
                $msg [$i + $ocraSuiteLength + 1 + $counterLength + $questionLength + $passwordLength + $sessionInformationLength] = $bArray[$i];
283
            }
284
        }
285
        
286
        $byteKey = self::_hexStr2Bytes($key, strlen($key)/2, 'key');
287
              
288
        $msg = implode("", $msg);
289
290
        $hash = self::_hmac($crypto, $byteKey, $msg);
291 109
292
        if ($codeDigits == 0)
293
            $result = $hash;
294 109
        else
295
            $result = self::_oath_truncate($hash, $codeDigits);
296 109
             
297
        return $result;
298
    }
299
300 109
    /**
301
     * Implementation of the Truncate function from RFC4226
302
     * Truncate a hex encoded (hash) result to a string of digital digits of $length
303 109
     *
304 109
     * @param string $hash hex encoded (hash) value to truncate. Minimum length is 20 bytes (i.e. a string of 40 hex digits)
305 109
     * @param int $length number of decimal digits to truncate to
306 109
     * @return string of $length digits
307
     */    
308
    static function _oath_truncate(string $hash, int $length = 6) : string
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
309
    {
310 109
        // Convert to dec
311
        foreach(str_split($hash,2) as $hex)
312 109
        {
313 109
            $hmac_result[]=hexdec($hex);
314
        }
315
    
316
        // Find offset
317
        $offset = $hmac_result[count($hmac_result) - 1] & 0xf;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $hmac_result seems to be defined by a foreach iteration on line 311. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
318
    
319
        $v = strval(
320
            (($hmac_result[$offset+0] & 0x7f) << 24 ) |
321
            (($hmac_result[$offset+1] & 0xff) << 16 ) |
322
            (($hmac_result[$offset+2] & 0xff) << 8 ) |
323
            ($hmac_result[$offset+3] & 0xff)
324
        );
325
326
        // Prefix truncated string with 0's to ensure it always has the required length
327
        $v=str_pad($v, $length, "0", STR_PAD_LEFT);
328
329
        $v = substr($v, strlen($v) - $length);
330
        return $v;
331
    }
332
    
333
}
334