1
- use color_eyre:: eyre:: ErrReport ;
1
+ use color_eyre:: eyre:: { eyre, ErrReport } ;
2
+ use derive_builder:: Builder ;
2
3
use std:: { fs:: File , io:: Write , vec} ;
3
4
4
5
use crate :: crypto:: cryptography:: { argon_derive_key, encrypt_string_with_key, gen_salt} ;
@@ -126,17 +127,30 @@ impl OTPDatabase {
126
127
}
127
128
}
128
129
129
- #[ derive( Serialize , Deserialize , Clone , PartialEq , Eq , Debug , Hash , Zeroize , ZeroizeOnDrop ) ]
130
+ #[ derive(
131
+ Serialize , Deserialize , Builder , Clone , PartialEq , Eq , Debug , Hash , Zeroize , ZeroizeOnDrop ,
132
+ ) ]
133
+ #[ builder(
134
+ setter( into) ,
135
+ build_fn( validate = "Self::validate" , error = "ErrReport" )
136
+ ) ]
130
137
pub struct OTPElement {
138
+ #[ builder( setter( custom) ) ]
131
139
pub secret : String ,
132
140
pub issuer : String ,
133
141
pub label : String ,
142
+ #[ builder( default = "6" ) ]
134
143
pub digits : u64 ,
135
144
#[ serde( rename = "type" ) ]
145
+ #[ builder( setter( custom) , default ) ]
136
146
pub type_ : OTPType ,
147
+ #[ builder( default ) ]
137
148
pub algorithm : OTPAlgorithm ,
149
+ #[ builder( default = "30" ) ]
138
150
pub period : u64 ,
151
+ #[ builder( setter( into) , default ) ]
139
152
pub counter : Option < u64 > ,
153
+ #[ builder( setter( into) , default ) ]
140
154
pub pin : Option < String > ,
141
155
}
142
156
@@ -212,11 +226,50 @@ impl OTPElement {
212
226
let s = ( value % exponential) . to_string ( ) ;
213
227
Ok ( "0" . repeat ( self . digits as usize - s. chars ( ) . count ( ) ) + s. as_str ( ) )
214
228
}
229
+ }
215
230
216
- pub fn valid_secret ( & self ) -> bool {
217
- match self . type_ {
218
- OTPType :: Motp => hex:: decode ( & self . secret ) . is_ok ( ) ,
219
- _ => BASE32_NOPAD . decode ( self . secret . as_bytes ( ) ) . is_ok ( ) ,
231
+ impl OTPElementBuilder {
232
+ /// Makes the secret insertion case insensitive
233
+ pub fn secret < VALUE : Into < String > > ( & mut self , value : VALUE ) -> & mut Self {
234
+ self . secret = Some ( value. into ( ) . to_uppercase ( ) ) ;
235
+ self
236
+ }
237
+
238
+ /// Makes the secret insertion case insensitive
239
+ pub fn type_ < VALUE : Into < OTPType > > ( & mut self , value : VALUE ) -> & mut Self {
240
+ let otp_type: OTPType = value. into ( ) ;
241
+
242
+ if otp_type == OTPType :: Motp {
243
+ // Motp codes must be lowercase
244
+ self . secret = self . secret . as_ref ( ) . map ( |s| s. to_lowercase ( ) )
245
+ } else {
246
+ // Base32 codes must be uppercase
247
+ self . secret = self . secret . as_ref ( ) . map ( |s| s. to_uppercase ( ) )
248
+ }
249
+
250
+ self . type_ = Some ( otp_type) ;
251
+ self
252
+ }
253
+
254
+ /// Check if the OTPElement is valid
255
+ fn validate ( & self ) -> Result < ( ) , ErrReport > {
256
+ if self . secret . is_none ( ) {
257
+ return Err ( eyre ! ( "Secret must be set" , ) ) ;
258
+ }
259
+
260
+ if self . secret . as_ref ( ) . unwrap ( ) . is_empty ( ) {
261
+ return Err ( eyre ! ( "Secret must not be empty" , ) ) ;
262
+ }
263
+
264
+ // Validate secret encoding
265
+ match self . type_ . unwrap_or_default ( ) {
266
+ OTPType :: Motp => hex:: decode ( self . secret . as_ref ( ) . unwrap ( ) )
267
+ . map ( |_| { } )
268
+ . map_err ( |e| eyre ! ( "Invalid hex secret: {e}" ) ) ,
269
+ _ => BASE32_NOPAD
270
+ . decode ( self . secret . as_ref ( ) . unwrap ( ) . as_bytes ( ) )
271
+ . map ( |_| { } )
272
+ . map_err ( |e| eyre ! ( "Invalid BASE32 secret: {e}" ) ) ,
220
273
}
221
274
}
222
275
}
@@ -230,11 +283,12 @@ fn get_label(issuer: &str, label: &str) -> String {
230
283
#[ cfg( test) ]
231
284
mod test {
232
285
use crate :: otp:: otp_element:: OTPAlgorithm :: Sha1 ;
233
- use crate :: otp:: otp_element:: OTPElement ;
234
286
use crate :: otp:: otp_element:: OTPType :: Totp ;
287
+ use crate :: otp:: otp_element:: { OTPElement , OTPElementBuilder } ;
235
288
236
289
use crate :: otp:: from_otp_uri:: FromOtpUri ;
237
290
use crate :: otp:: otp_error:: OtpError ;
291
+ use crate :: otp:: otp_type:: OTPType ;
238
292
239
293
#[ test]
240
294
fn test_serialization_otp_uri_full_element ( ) {
@@ -315,4 +369,58 @@ mod test {
315
369
// Assert
316
370
assert_eq ! ( Err ( OtpError :: InvalidDigits ) , result)
317
371
}
372
+
373
+ #[ test]
374
+ fn test_lowercase_secret ( ) {
375
+ // Arrange / Act
376
+ let result = OTPElementBuilder :: default ( )
377
+ . secret ( "aa" )
378
+ . label ( "label" )
379
+ . issuer ( "" )
380
+ . build ( ) ;
381
+
382
+ // Assert
383
+ assert_eq ! ( "AA" , result. unwrap( ) . secret) ;
384
+ }
385
+
386
+ #[ test]
387
+ fn test_invalid_secret_base32 ( ) {
388
+ let result = OTPElementBuilder :: default ( )
389
+ . secret ( "aaa" )
390
+ . label ( "label" )
391
+ . issuer ( "" )
392
+ . build ( ) ;
393
+
394
+ assert_eq ! (
395
+ "Invalid BASE32 secret: invalid length at 2" ,
396
+ result. unwrap_err( ) . to_string( )
397
+ )
398
+ }
399
+
400
+ #[ test]
401
+ fn valid_hex_secret ( ) {
402
+ let result = OTPElementBuilder :: default ( )
403
+ . secret ( "aAAf" )
404
+ . label ( "label" )
405
+ . issuer ( "" )
406
+ . type_ ( OTPType :: Motp )
407
+ . build ( ) ;
408
+
409
+ assert_eq ! ( "aaaf" , result. unwrap( ) . secret)
410
+ }
411
+
412
+ #[ test]
413
+ fn invalid_secret_hex ( ) {
414
+ let result = OTPElementBuilder :: default ( )
415
+ . secret ( "aaa" )
416
+ . label ( "label" )
417
+ . issuer ( "" )
418
+ . type_ ( OTPType :: Motp )
419
+ . build ( ) ;
420
+
421
+ assert_eq ! (
422
+ "Invalid hex secret: Odd number of digits" ,
423
+ result. unwrap_err( ) . to_string( )
424
+ )
425
+ }
318
426
}
0 commit comments