3232from .low_level import AES256_CTR_HMAC_SHA256 , AES256_CTR_BLAKE2b , AES256_OCB , CHACHA20_POLY1305
3333from . import low_level
3434
35+ from ..fido2 import has_fido2 , Fido2Operations
36+
3537# workaround for lost passphrase or key in "authenticated" or "authenticated-blake2" mode
3638AUTHENTICATED_NO_KEY = "authenticated_no_key" in workarounds
3739
@@ -388,6 +390,7 @@ class FlexiKey:
388390 def detect (cls , repository , manifest_data , * , other = False ):
389391 key = cls (repository )
390392 target = key .find_key ()
393+ # TODO: ask for "PIN" when applicable
391394 prompt = "Enter passphrase for key %s: " % target
392395 passphrase = Passphrase .env_passphrase (other = other )
393396 if passphrase is None :
@@ -442,6 +445,8 @@ def decrypt_key_file(self, data, passphrase):
442445 return self .decrypt_key_file_pbkdf2 (encrypted_key , passphrase )
443446 elif encrypted_key .algorithm == "argon2 chacha20-poly1305" :
444447 return self .decrypt_key_file_argon2 (encrypted_key , passphrase )
448+ elif encrypted_key .algorithm == "fido2 hmac-secret chacha20-poly1305" :
449+ return self .decrypt_key_file_fido2 (encrypted_key , passphrase )
445450 else :
446451 raise UnsupportedKeyFormatError ()
447452
@@ -501,11 +506,26 @@ def decrypt_key_file_argon2(self, encrypted_key, passphrase):
501506 except low_level .IntegrityError :
502507 return None
503508
504- def encrypt_key_file (self , data , passphrase , algorithm ):
509+ def decrypt_key_file_fido2 (self , encrypted_key , pin ):
510+ device = Fido2Operations .find_device (encrypted_key .fido2_credential_id )
511+ operations = Fido2Operations (device , pin )
512+ secret = operations .use_hmac_hash (
513+ encrypted_key .salt ,
514+ encrypted_key .fido2_credential_id ,
515+ )
516+ ae_cipher = CHACHA20_POLY1305 (key = secret , iv = 0 , header_len = 0 , aad_offset = 0 )
517+ try :
518+ return ae_cipher .decrypt (encrypted_key .data )
519+ except low_level .IntegrityError :
520+ return None
521+
522+ def encrypt_key_file (self , data , passphrase , algorithm , args ):
505523 if algorithm == "sha256" :
506524 return self .encrypt_key_file_pbkdf2 (data , passphrase )
507525 elif algorithm == "argon2 chacha20-poly1305" :
508526 return self .encrypt_key_file_argon2 (data , passphrase )
527+ elif algorithm == "fido2 hmac-secret chacha20-poly1305" :
528+ return self .encrypt_key_file_fido2 (data , passphrase , args )
509529 else :
510530 raise ValueError (f"Unexpected algorithm: { algorithm } " )
511531
@@ -531,22 +551,48 @@ def encrypt_key_file_argon2(self, data, passphrase):
531551 )
532552 return msgpack .packb (encrypted_key .as_dict ())
533553
534- def _save (self , passphrase , algorithm ):
554+ def encrypt_key_file_fido2 (self , data , pin , args ):
555+ operations = Fido2Operations (args .fido2_device , pin )
556+ credential_id , salt , secret = operations .generate_hmac_hash (user = self .repository_id )
557+ ae_cipher = CHACHA20_POLY1305 (key = secret , iv = 0 , header_len = 0 , aad_offset = 0 )
558+ encrypted_key = EncryptedKey (
559+ version = 1 ,
560+ algorithm = "fido2 hmac-secret chacha20-poly1305" ,
561+ salt = salt ,
562+ data = ae_cipher .encrypt (data ),
563+ fido2_credential_id = credential_id ,
564+ )
565+ return msgpack .packb (encrypted_key .as_dict ())
566+
567+ def _save (self , passphrase , algorithm , args ):
535568 key = Key (
536569 version = 2 ,
537570 repository_id = self .repository_id ,
538571 crypt_key = self .crypt_key ,
539572 id_key = self .id_key ,
540573 chunk_seed = self .chunk_seed ,
541574 )
542- data = self .encrypt_key_file (msgpack .packb (key .as_dict ()), passphrase , algorithm )
575+ data = self .encrypt_key_file (msgpack .packb (key .as_dict ()), passphrase , algorithm , args )
543576 key_data = "\n " .join (textwrap .wrap (binascii .b2a_base64 (data ).decode ("ascii" )))
544577 return key_data
545578
546- def change_passphrase (self , passphrase = None ):
547- if passphrase is None :
548- passphrase = Passphrase .new (allow_empty = True )
549- self .save (self .target , passphrase , algorithm = self ._encrypted_key_algorithm )
579+ def change_passphrase (self , args , passphrase = None ):
580+ if args .fido2_device :
581+ operations = Fido2Operations (args .fido2_device )
582+ if operations .has_client_pin :
583+ # TODO: try to be more descriptive about the device
584+ passphrase = Passphrase .new (pin_prompt = f"Enter PIN for { args .fido2_device } : " , only_new = True )
585+ else :
586+ passphrase = Passphrase ('' )
587+ key_algorithm = KEY_ALGORITHMS ["fido2" ]
588+ else :
589+ if passphrase is None :
590+ passphrase = Passphrase .new (allow_empty = True , only_new = True )
591+ key_algorithm = self ._encrypted_key_algorithm
592+ # If fido2 was used before change it to argon2
593+ if key_algorithm == KEY_ALGORITHMS ["fido2" ]:
594+ key_algorithm = KEY_ALGORITHMS ["argon2" ]
595+ self .save (self .target , passphrase , algorithm = key_algorithm , args = args )
550596
551597 @classmethod
552598 def create (cls , repository , args , * , other_key = None ):
@@ -569,10 +615,15 @@ def create(cls, repository, args, *, other_key=None):
569615 key .init_from_given_data (crypt_key = crypt_key , id_key = other_key .id_key , chunk_seed = other_key .chunk_seed )
570616 else :
571617 key .init_from_random_data ()
572- passphrase = Passphrase .new (allow_empty = True )
618+ if args .fido2_device :
619+ key_algorithm = KEY_ALGORITHMS ["fido2" ]
620+ passphrase = Passphrase .new (pin_prompt = "Enter PIN for {args.fido2_device}: " )
621+ else :
622+ key_algorithm = KEY_ALGORITHMS ["argon2" ]
623+ passphrase = Passphrase .new (allow_empty = True )
573624 key .init_ciphers ()
574625 target = key .get_new_target (args )
575- key .save (target , passphrase , create = True , algorithm = KEY_ALGORITHMS [ "argon2" ] )
626+ key .save (target , passphrase , key_algorithm , args , create = True )
576627 logger .info ('Key in "%s" created.' % target )
577628 logger .info ("Keep this key safe. Your data will be inaccessible without it." )
578629 return key
@@ -700,8 +751,8 @@ def load(self, target, passphrase):
700751 self .target = target
701752 return success
702753
703- def save (self , target , passphrase , algorithm , create = False ):
704- key_data = self ._save (passphrase , algorithm )
754+ def save (self , target , passphrase , algorithm , args , create = False ):
755+ key_data = self ._save (passphrase , algorithm , args )
705756 if self .STORAGE == KeyBlobStorage .KEYFILE :
706757 if create and os .path .isfile (target ):
707758 # if a new keyfile key repository is created, ensure that an existing keyfile of another
@@ -788,8 +839,8 @@ def load(self, target, passphrase):
788839 self .logically_encrypted = False
789840 return success
790841
791- def save (self , target , passphrase , algorithm , create = False ):
792- super ().save (target , passphrase , algorithm , create = create )
842+ def save (self , target , passphrase , algorithm , args , create = False ):
843+ super ().save (target , passphrase , algorithm , args , create = create )
793844 self .logically_encrypted = False
794845
795846 def init_ciphers (self , manifest_data = None ):
0 commit comments