11import binascii
22import hmac
33import os
4+ import stat
45import textwrap
56from hashlib import sha256
67from math import ceil
@@ -222,7 +223,7 @@ def identify_key(manifest_data):
222223 raise UnsupportedPayloadError (key_type )
223224
224225
225- def key_factory (repository , manifest_chunk , * , other = False , ro_cls = RepoObj ):
226+ def key_factory (repository , manifest_chunk , args , * , other = False , ro_cls = RepoObj ):
226227 manifest_data = ro_cls .extract_crypted_data (manifest_chunk )
227228 assert manifest_data , "manifest data must not be zero bytes long"
228229 key_cls = identify_key (manifest_data )
@@ -232,7 +233,7 @@ def key_factory(repository, manifest_chunk, *, other=False, ro_cls=RepoObj):
232233 # tagged envelope modes (see MACKeyBase). The legacy key classes only exist to read borg
233234 # 1.x repositories (ro_cls is RepoObj1 then), e.g. for "borg transfer --from-borg1".
234235 raise UnsupportedPayloadError (manifest_data [0 ])
235- key = key_cls .detect (repository , manifest_data , other = other )
236+ key = key_cls .detect (repository , manifest_data , args , other = other )
236237 key .stored_type = manifest_data [0 ]
237238 return key
238239
@@ -505,7 +506,7 @@ class FlexiKey:
505506 _loaded_label = None # label of the borg key we unlocked
506507
507508 @classmethod
508- def detect (cls , repository , manifest_data , * , other = False ):
509+ def detect (cls , repository , manifest_data , args , * , other = False ):
509510 key = cls (repository )
510511 target = key .find_key ()
511512 # TODO: ask for "PIN" when applicable
@@ -515,31 +516,31 @@ def detect(cls, repository, manifest_data, *, other=False):
515516 # passphrase against all of them.
516517 if passphrase is None :
517518 passphrase = Passphrase ()
518- if not key .load_any (passphrase ):
519+ if not key .load_any (passphrase , args ):
519520 for retry in range (0 , 3 ):
520521 passphrase = Passphrase .getpass (prompt )
521- if key .load_any (passphrase ):
522+ if key .load_any (passphrase , args ):
522523 break
523524 Passphrase .display_debug_info (passphrase )
524525 else :
525526 raise PasswordRetriesExceeded
526527 else :
527- if not key .load_any (passphrase ):
528+ if not key .load_any (passphrase , args ):
528529 Passphrase .display_debug_info (passphrase )
529530 raise PassphraseWrong
530531 key .init_ciphers (manifest_data )
531532 key ._passphrase = passphrase
532533 return key
533534
534- def _load (self , key_data , passphrase ):
535+ def _load (self , key_data , passphrase , args ):
535536 try :
536537 key = binascii .a2b_base64 (key_data )
537538 except (ValueError , binascii .Error ):
538539 raise KeyfileInvalidError (self .repository ._location .canonical_path (), "(repokey)" ) from None
539540 if len (key ) < 20 :
540541 # this is in no way a precise check, usually we have about 400b key data.
541542 raise KeyfileInvalidError (self .repository ._location .canonical_path (), "(repokey)" )
542- data = self .decrypt_key_file (key , passphrase )
543+ data = self .decrypt_key_file (key , passphrase , args )
543544 if data :
544545 data = msgpack .unpackb (data )
545546 key = Key (internal_dict = data )
@@ -552,7 +553,7 @@ def _load(self, key_data, passphrase):
552553 return True
553554 return False
554555
555- def decrypt_key_file (self , data , passphrase ):
556+ def decrypt_key_file (self , data , passphrase , args ):
556557 unpacker = get_limited_unpacker ("key" )
557558 unpacker .feed (data )
558559 data = unpacker .unpack ()
@@ -565,7 +566,7 @@ def decrypt_key_file(self, data, passphrase):
565566 if encrypted_key .algorithm == "argon2 chacha20-poly1305" :
566567 return self .decrypt_key_file_argon2 (encrypted_key , passphrase )
567568 elif encrypted_key .algorithm == "fido2 hmac-secret chacha20-poly1305" :
568- return self .decrypt_key_file_fido2 (encrypted_key , passphrase )
569+ return self .decrypt_key_file_fido2 (encrypted_key , passphrase , args )
569570 else :
570571 raise UnsupportedKeyFormatError ()
571572
@@ -604,8 +605,14 @@ def decrypt_key_file_argon2(self, encrypted_key, passphrase):
604605 except low_level .IntegrityError :
605606 return None
606607
607- def decrypt_key_file_fido2 (self , encrypted_key , pin ):
608- device = Fido2Operations .find_device (encrypted_key .fido2_credential_id )
608+ def decrypt_key_file_fido2 (self , encrypted_key , pin , args ):
609+ device = args .fido2_device
610+ if device == "auto" :
611+ device = Fido2Operations .find_device (encrypted_key .fido2_credential_id )
612+ if device == "none" or not (os .access (device , os .F_OK ) and stat .S_ISCHR (os .stat (device ).st_mode )):
613+ # The device may be invalid despite passing this check, but if we are here
614+ # it is definitely invalid.
615+ raise ValueError (f"Invalid or unspecified FIDO2 device: { device } " )
609616 operations = Fido2Operations (device , pin )
610617 secret = operations .use_hmac_hash (encrypted_key .salt , encrypted_key .fido2_credential_id )
611618 ae_cipher = CHACHA20_POLY1305 (key = secret , iv = 0 , header_len = 0 , aad_offset = 0 )
@@ -870,7 +877,7 @@ def _key_envelope(self, blob_text):
870877 unpacker .feed (raw )
871878 return EncryptedKey (internal_dict = unpacker .unpack ())
872879
873- def _try_key (self , key_id , blob_text , keyfile_path , passphrase ):
880+ def _try_key (self , key_id , blob_text , keyfile_path , passphrase , args ):
874881 # try to unlock a single borg key with the given passphrase; on success, remember it.
875882 if is_keyfile (blob_text ):
876883 # keyfile / modern repokey: data is wrapped in keyfile_format (BORG_KEY header).
@@ -882,7 +889,7 @@ def _try_key(self, key_id, blob_text, keyfile_path, passphrase):
882889 # borg 1.x repokey: stored as raw base64 without the BORG_KEY header.
883890 key_data = blob_text
884891 try :
885- loaded = self ._load (key_data , passphrase )
892+ loaded = self ._load (key_data , passphrase , args )
886893 except Exception as exc : # noqa: BLE001 - a corrupted borg key must not break unlocking via the others
887894 logger .debug ("Borg key %s could not be loaded (corrupted?), skipping it: %s" , key_id [:12 ], exc )
888895 return False
@@ -900,14 +907,14 @@ def _try_key(self, key_id, blob_text, keyfile_path, passphrase):
900907 return True
901908 return False
902909
903- def load_any (self , passphrase ):
910+ def load_any (self , passphrase , args ):
904911 """Try the passphrase against every borg key of this repository."""
905912 for key_id , blob_text , keyfile_path in self ._iter_keys ():
906- if self ._try_key (key_id , blob_text , keyfile_path , passphrase ):
913+ if self ._try_key (key_id , blob_text , keyfile_path , passphrase , args ):
907914 return True
908915 return False
909916
910- def load (self , target , passphrase ):
917+ def load (self , target , passphrase , args ):
911918 # load a specific borg key: for keyfiles, the explicit file given as target; for repokey,
912919 # any of the repository's borg keys (which are addressed by passphrase, not by target).
913920 if self .storage == KeyBlobStorage .KEYFILE :
@@ -916,9 +923,9 @@ def load(self, target, passphrase):
916923 blob = fd .read ()
917924 except OSError :
918925 return False
919- return self ._try_key (sha256 (blob ).hexdigest (), blob .decode ("utf-8" ), str (target ), passphrase )
926+ return self ._try_key (sha256 (blob ).hexdigest (), blob .decode ("utf-8" ), str (target ), passphrase , args )
920927 else :
921- return self .load_any (passphrase )
928+ return self .load_any (passphrase , args )
922929
923930 def save (self , target , passphrase , algorithm , args , create = False , label = None , replace = True ):
924931 # replace=True replaces the previously-loaded borg key (change-passphrase semantics);
@@ -1228,6 +1235,7 @@ class AuthenticatedKeyBase(MACKeyBase, FlexiKey):
12281235 # It's only authenticated, not encrypted.
12291236 logically_encrypted = False
12301237
1238+ < << << << HEAD
12311239 # every read is authenticated by the envelope tag (which covers the chunk id via the AAD),
12321240 # independently of assert_id() - the same reasoning as for the AEAD keys, see
12331241 # AEADKeyBase.assert_id about what verifying the chunk id adds on top of that.
@@ -1247,7 +1255,7 @@ def tag_key(self):
12471255 self ._tag_key = self .derive_key (salt = b"" , domain = self .MAC_KEY_DOMAIN , size = 32 )
12481256 return self ._tag_key
12491257
1250- def _load (self , key_data , passphrase ):
1258+ def _load (self , key_data , passphrase , args ):
12511259 if AUTHENTICATED_NO_KEY :
12521260 # fake _load if we have no key or passphrase. The key material is all-zero and thus
12531261 # worthless, but these modes do not encrypt, so reading still works - decrypt() skips
@@ -1257,9 +1265,9 @@ def _load(self, key_data, passphrase):
12571265 self .id_key = bytes (32 )
12581266 self .chunk_seed = 0
12591267 return True
1260- return super ()._load (key_data , passphrase )
1268+ return super ()._load (key_data , passphrase , args )
12611269
1262- def load (self , target , passphrase ):
1270+ def load (self , target , passphrase , args ):
12631271 success = super ().load (target , passphrase )
12641272 self .logically_encrypted = False
12651273 return success
0 commit comments