Skip to content

Commit 7a306c6

Browse files
committed
WIP: propagate args down to decrypt_key_file_fido2()
Tested with repo-info only. More code paths need to be covered. To be continued.
1 parent aed7e96 commit 7a306c6

4 files changed

Lines changed: 40 additions & 32 deletions

File tree

src/borg/archiver/_common.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def wrapper(self, args, **kwargs):
154154
from ..legacy.repoobj import RepoObj1
155155

156156
ro_cls = RepoObj1
157-
manifest_ = Manifest.load(repository, compatibility, other=False, ro_cls=ro_cls)
157+
manifest_ = Manifest.load(repository, compatibility, args, other=False, ro_cls=ro_cls)
158158
kwargs["manifest"] = manifest_
159159
if "compression" in args:
160160
manifest_.repo_objs.compressor = args.compression.compressor
@@ -562,6 +562,13 @@ def define_common_options(add_common_option):
562562
action=Highlander,
563563
help="repository to use",
564564
)
565+
add_common_option(
566+
"--fido2-device",
567+
metavar="DEVICE",
568+
dest="fido2_device",
569+
default=os.environ.get("BORG_FIDO2_DEVICE", "none"),
570+
help="select fido2 device to protect the repository key, use ``fido2-token -L`` to list available devices.",
571+
)
565572

566573

567574
def build_matcher(inclexcl_patterns, include_paths, pattern_roots=()):

src/borg/archiver/repo_create_cmd.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,3 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser)
261261
help="copy the crypt_key (used for authenticated encryption) from the key of the other repository "
262262
"(default: new random key).",
263263
)
264-
subparser.add_argument(
265-
"--fido2-device",
266-
metavar="DEVICE",
267-
dest="fido2_device",
268-
help="select fido2 device to protect the repository key, use ``fido2-token -L`` "
269-
"to list available devices.",
270-
)

src/borg/crypto/key.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import binascii
22
import hmac
33
import os
4+
import stat
45
import textwrap
56
from hashlib import sha256
67
from 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

src/borg/manifest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,13 +500,13 @@ def last_timestamp(self):
500500
return parse_timestamp(self.timestamp)
501501

502502
@classmethod
503-
def load(cls, repository, operations, key=None, *, other=False, ro_cls=RepoObj):
503+
def load(cls, repository, operations, args, key=None, *, other=False, ro_cls=RepoObj):
504504
from .item import ManifestItem
505505
from .crypto.key import key_factory
506506

507507
cdata = repository.get_manifest()
508508
if not key:
509-
key = key_factory(repository, cdata, other=other, ro_cls=ro_cls)
509+
key = key_factory(repository, cdata, args, other=other, ro_cls=ro_cls)
510510
manifest = cls(key, repository, ro_cls=ro_cls)
511511
_, data = manifest.repo_objs.parse(cls.MANIFEST_ID, cdata, ro_type=ROBJ_MANIFEST)
512512
manifest_dict = key.unpack_manifest(data)

0 commit comments

Comments
 (0)