55use DateTimeInterface ;
66use Exception ;
77use Infocyph \OTP \Exceptions \OCRAException ;
8+ use Infocyph \OTP \Traits \Common ;
89
910final class OCRA
1011{
11- private string $ ocraRegex = ' /^OCRA-1:HOTP-SHA(1|256|512)-(0|[4-9]|10):(C-)?Q([ANH])(0[4-9]|[1-5]\d|6[0-4])(-(P(SHA1|SHA256|SHA512)|S\d{3}|(T((\d|[1-3]\d|4[0-8])H|(([1-9]|[1-5]\d)([SM]))))))?/ ' ;
12-
12+ use Common ;
13+ private const OCRA_REGEX = ' /^OCRA-1:HOTP-SHA(1|256|512)-(0|[4-9]|10):(C-)?Q([ANH])(0[4-9]|[1-5]\d|6[0-4])(-(P(SHA1|SHA256|SHA512)|S\d{3}|(T((\d|[1-3]\d|4[0-8])H|(([1-9]|[1-5]\d)([SM]))))))*$/ ' ;
1314 private array $ ocraSuite ;
14- private string $ pin ;
15- private string $ session ;
16- private string $ time ;
15+ private ? string $ pin = null ;
16+ private ? string $ session = null ;
17+ private ? string $ time = null ;
1718
1819 /**
1920 * Constructor for the class.
@@ -30,13 +31,17 @@ public function __construct(string $ocraSuite, private readonly string $sharedKe
3031 /**
3132 * Sets the pin for the OCRA instance.
3233 *
33- * Required if the suite supports session .
34+ * Required if the suite supports PIN .
3435 *
3536 * @param string $pin The pin to set.
3637 * @return OCRA
38+ * @throws OCRAException
3739 */
3840 public function setPin (string $ pin ): OCRA
3941 {
42+ if (empty ($ pin )) {
43+ throw new OCRAException ('PIN cannot be empty. ' );
44+ }
4045 $ this ->pin = $ pin ;
4146 return $ this ;
4247 }
@@ -48,9 +53,13 @@ public function setPin(string $pin): OCRA
4853 *
4954 * @param string $session The session to set.
5055 * @return OCRA
56+ * @throws OCRAException
5157 */
5258 public function setSession (string $ session ): OCRA
5359 {
60+ if (empty ($ session )) {
61+ throw new OCRAException ('Session cannot be empty. ' );
62+ }
5463 $ this ->session = $ session ;
5564 return $ this ;
5665 }
@@ -87,11 +96,11 @@ public function generate(string $challenge, int $counter = 0): string
8796
8897 $ msg .= $ this ->calculateQ ($ challenge );
8998
90- if ($ this ->ocraSuite ['optional ' ] ) {
99+ if (! empty ( $ this ->ocraSuite ['optionals ' ]) ) {
91100 $ msg .= $ this ->calculateOptionals ();
92101 }
93102
94- $ hash = hash_hmac ((string ) $ this ->ocraSuite ['algo ' ], $ msg , $ this ->sharedKey , true );
103+ $ hash = hash_hmac ((string )$ this ->ocraSuite ['algo ' ], $ msg , $ this ->sharedKey , true );
95104
96105 if (!$ this ->ocraSuite ['length ' ]) {
97106 return $ hash ;
@@ -123,30 +132,35 @@ private function calculateQ(string $input): string
123132 }
124133
125134 /**
126- * Calculates the optional value based on the format specified in the OCRA suite.
135+ * Calculates the optional values based on the formats specified in the OCRA suite.
127136 *
128- * @return string The calculated optional value .
137+ * @return string The concatenated calculated optional values .
129138 * @throws OCRAException
130139 */
131140 private function calculateOptionals (): string
132141 {
133- return match ($ this ->ocraSuite ['optional ' ]['format ' ]) {
134- 'p ' => hash (
135- (string ) $ this ->ocraSuite ['optional ' ]['value ' ],
136- $ this ->pin ?? throw new OCRAException ('Missing PIN ' ),
137- true
138- ),
139- 's ' => str_pad (
140- pack ('H* ' , $ this ->session ?? throw new OCRAException ('Missing Session ' )),
141- $ this ->ocraSuite ['optional ' ]['value ' ],
142- "\0" ,
143- STR_PAD_LEFT
144- ),
145- 't ' => [
146- $ time = (int )floor (($ this ->time ?? time ()) / $ this ->ocraSuite ['optional ' ]['value ' ]),
147- pack ('NN ' , ($ time >> 32 ) & 0xffffffff , $ time & 0xffffffff )
148- ][1 ]
149- };
142+ $ optionals = '' ;
143+ foreach ($ this ->ocraSuite ['optionals ' ] as $ optional ) {
144+ $ optionals .= match ($ optional ['format ' ]) {
145+ 'p ' => hash (
146+ (string )$ optional ['value ' ],
147+ $ this ->pin ?? throw new OCRAException ('Missing PIN ' ),
148+ true
149+ ),
150+ 's ' => str_pad (
151+ pack ('H* ' , $ this ->session ?? throw new OCRAException ('Missing Session ' )),
152+ $ optional ['value ' ],
153+ "\0" ,
154+ STR_PAD_LEFT
155+ ),
156+ 't ' => [
157+ $ time = (int )floor (($ this ->time ?? time ()) / $ optional ['value ' ]),
158+ pack ('NN ' , ($ time >> 32 ) & 0xffffffff , $ time & 0xffffffff )
159+ ][1 ],
160+ default => throw new OCRAException ('Invalid optional part format ' )
161+ };
162+ }
163+ return $ optionals ;
150164 }
151165
152166 /**
@@ -157,7 +171,7 @@ private function calculateOptionals(): string
157171 */
158172 private function validateAndParse (string $ ocraSuite ): void
159173 {
160- if (!preg_match ($ this -> ocraRegex , $ ocraSuite , $ matches )) {
174+ if (!preg_match (self :: OCRA_REGEX , $ ocraSuite , $ matches )) {
161175 throw new OCRAException ('Invalid OCRA Suite! ' );
162176 }
163177
@@ -178,37 +192,42 @@ private function validateAndParse(string $ocraSuite): void
178192 *
179193 * @param array $parts The array of parts to prepare the conditional parts from.
180194 * @return array The prepared conditional parts.
195+ * @throws OCRAException
181196 */
182197 private function prepareConditionalParts (array $ parts ): array
183198 {
184199 $ conditionalParts = (
185200 $ parts [5 ] === 'c '
186- ? ['c ' => true , 'q ' => substr ((string ) $ parts [6 ], 1 ), 'optional ' => $ parts[ 7 ] ?? null ]
187- : ['c ' => false , 'q ' => substr ((string ) $ parts [5 ], 1 ), 'optional ' => $ parts[ 6 ] ?? null ]
201+ ? ['c ' => true , 'q ' => substr ((string )$ parts [6 ], 1 ), 'optionals ' => array_slice ( $ parts, 7 ) ]
202+ : ['c ' => false , 'q ' => substr ((string )$ parts [5 ], 1 ), 'optionals ' => array_slice ( $ parts, 6 ) ]
188203 );
189204
190205 $ conditionalParts ['q ' ] = [
191206 'format ' => $ conditionalParts ['q ' ][0 ],
192207 'value ' => (int )($ conditionalParts ['q ' ][1 ] . $ conditionalParts ['q ' ][2 ]),
193208 ];
194209
195- if (! $ conditionalParts ['optional ' ] ) {
210+ if (empty ( $ conditionalParts ['optionals ' ]) ) {
196211 return $ conditionalParts ;
197212 }
198213
199- $ conditionalParts ['optional ' ] = [
200- 'format ' => $ conditionalParts ['optional ' ][0 ],
201- 'value ' => substr ((string ) $ conditionalParts ['optional ' ], 1 )
202- ];
203- $ conditionalParts ['optional ' ]['value ' ] = match ($ conditionalParts ['optional ' ]['format ' ]) {
204- 's ' => (int )$ conditionalParts ['optional ' ]['value ' ],
205- 'p ' => $ conditionalParts ['optional ' ]['value ' ],
206- 't ' => match (substr ($ conditionalParts ['optional ' ]['value ' ], -1 )) {
207- 's ' => (int )rtrim ($ conditionalParts ['optional ' ]['value ' ], 's ' ),
208- 'm ' => (int )rtrim ($ conditionalParts ['optional ' ]['value ' ], 'm ' ) * 60 ,
209- 'h ' => (int )rtrim ($ conditionalParts ['optional ' ]['value ' ], 'h ' ) * 3600
210- }
211- };
214+ $ conditionalParts ['optionals ' ] = array_map (fn ($ optional ) => [
215+ 'format ' => $ optional [0 ],
216+ 'value ' => substr ((string )$ optional , 1 )
217+ ], $ conditionalParts ['optionals ' ]);
218+
219+ foreach ($ conditionalParts ['optionals ' ] as &$ optional ) {
220+ $ optional ['value ' ] = match ($ optional ['format ' ]) {
221+ 's ' => (int )$ optional ['value ' ],
222+ 'p ' => $ optional ['value ' ],
223+ 't ' => match (substr ($ optional ['value ' ], -1 )) {
224+ 's ' => (int )rtrim ($ optional ['value ' ], 's ' ),
225+ 'm ' => (int )rtrim ($ optional ['value ' ], 'm ' ) * 60 ,
226+ 'h ' => (int )rtrim ($ optional ['value ' ], 'h ' ) * 3600 ,
227+ default => throw new OCRAException ('Invalid time format ' )
228+ }
229+ };
230+ }
212231
213232 return $ conditionalParts ;
214233 }
0 commit comments