Skip to content
This repository has been archived by the owner on May 5, 2023. It is now read-only.

Commit

Permalink
Argon2 the second part: implement decryption of v2 keys
Browse files Browse the repository at this point in the history
1. Note: I have rebased this on top of 78f0414 and fixed the code to work with the new Passphrase.argon2 interface
2. Refactor: s/enc_key/encrypted_key/ - I intend to use the name enc_key for something else
3. Dispatch on algorithm instead of version borgbackup#747 (comment)

New: rebased on 28731c5 (current master)
  • Loading branch information
hexagonrecursion committed Mar 27, 2022
1 parent 28731c5 commit 46243e1
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 7 deletions.
43 changes: 37 additions & 6 deletions src/borg/crypto/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ class RepoKeyNotFoundError(Error):
"""No key entry found in the config of repository {}."""


class UnsupportedKeyFormatError(Error):
"""Your borg key is stored in an unsupported format. Try using a newer version of borg."""


class TAMRequiredError(IntegrityError):
__doc__ = textwrap.dedent("""
Manifest is unauthenticated, but it is required for this repository.
Expand Down Expand Up @@ -430,13 +434,40 @@ def decrypt_key_file(self, data, passphrase):
unpacker = get_limited_unpacker('key')
unpacker.feed(data)
data = unpacker.unpack()
enc_key = EncryptedKey(internal_dict=data)
assert enc_key.version == 1
assert enc_key.algorithm == 'sha256'
key = passphrase.kdf(enc_key.salt, enc_key.iterations, 32)
data = AES(key, b'\0'*16).decrypt(enc_key.data)
if hmac.compare_digest(hmac_sha256(key, data), enc_key.hash):
encrypted_key = EncryptedKey(internal_dict=data)
if encrypted_key.version != 1:
raise UnsupportedKeyFormatError()
else:
if encrypted_key.algorithm == 'sha256':
return self.decrypt_key_file_pbkdf2(encrypted_key, passphrase)
elif encrypted_key.algorithm == 'argon2 aes256-ctr hmac-sha256':
return self.decrypt_key_file_argon2(encrypted_key, passphrase)
else:
raise UnsupportedKeyFormatError()

def decrypt_key_file_pbkdf2(self, encrypted_key, passphrase):
key = passphrase.kdf(encrypted_key.salt, encrypted_key.iterations, 32)
data = AES(key, b'\0'*16).decrypt(encrypted_key.data)
if hmac.compare_digest(hmac_sha256(key, data), encrypted_key.hash):
return data
return None

def decrypt_key_file_argon2(self, encrypted_key, passphrase):
key = passphrase.argon2(
output_len_in_bytes=64,
salt=encrypted_key.salt,
time_cost=encrypted_key.argon2_time_cost,
memory_cost=encrypted_key.argon2_memory_cost,
parallelism=encrypted_key.argon2_parallelism,
type=encrypted_key.argon2_type,
)
enc_key, mac_key = key[:32], key[32:]
ae_cipher = AES256_CTR_HMAC_SHA256(
iv=0, header_len=0, aad_offset=0,
enc_key=enc_key,
mac_key=mac_key,
)
return ae_cipher.decrypt(encrypted_key.data)

def encrypt_key_file(self, data, passphrase):
salt = os.urandom(32)
Expand Down
8 changes: 7 additions & 1 deletion src/borg/item.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ from .helpers import bigint_to_int, int_to_bigint
from .helpers import StableDict
from .helpers import format_file_size


cdef extern from "_item.c":
object _object_to_optr(object obj)
object _optr_to_object(object bytes)
Expand Down Expand Up @@ -294,7 +295,8 @@ class EncryptedKey(PropDict):
If a EncryptedKey shall be serialized, give as_dict() method output to msgpack packer.
"""

VALID_KEYS = {'version', 'algorithm', 'iterations', 'salt', 'hash', 'data'} # str-typed keys
VALID_KEYS = { 'version', 'algorithm', 'iterations', 'salt', 'hash', 'data',
'argon2_time_cost', 'argon2_memory_cost', 'argon2_parallelism', 'argon2_type' }

__slots__ = ("_dict", ) # avoid setting attributes not supported by properties

Expand All @@ -304,6 +306,10 @@ class EncryptedKey(PropDict):
salt = PropDict._make_property('salt', bytes)
hash = PropDict._make_property('hash', bytes)
data = PropDict._make_property('data', bytes)
argon2_time_cost = PropDict._make_property('argon2_time_cost', int)
argon2_memory_cost = PropDict._make_property('argon2_memory_cost', int)
argon2_parallelism = PropDict._make_property('argon2_parallelism', int)
argon2_type = PropDict._make_property('argon2_type', str, encode=str.encode, decode=bytes.decode)


class Key(PropDict):
Expand Down
89 changes: 89 additions & 0 deletions src/borg/testsuite/crypto.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from binascii import hexlify

import pytest

from ..crypto.low_level import AES256_CTR_HMAC_SHA256, AES256_OCB, CHACHA20_POLY1305, UNENCRYPTED, \
IntegrityError, is_libressl
from ..crypto.low_level import bytes_to_long, bytes_to_int, long_to_bytes
from ..crypto.low_level import hkdf_hmac_sha512
from ..crypto.low_level import AES, hmac_sha256
from ..crypto.key import KeyfileKey, UnsupportedKeyFormatError
from ..helpers.passphrase import Passphrase
from ..helpers import msgpack

from . import BaseTestCase

Expand Down Expand Up @@ -248,3 +254,86 @@ def test_hkdf_hmac_sha512_5(self):

okm = hkdf_hmac_sha512(ikm, salt, info, l)
assert okm == bytes.fromhex('1407d46013d98bc6decefcfee55f0f90b0c7f63d68eb1a80eaf07e953cfc0a3a5240a155d6e4daa965bb')


def test_decrypt_key_file_argon2_aes256_ctr_hmac_sha256(monkeypatch):
plain = b'hello'
# echo -n "hello, pass phrase" | argon2 saltsaltsaltsalt -id -t 3 -m 16 -p 4 -l 64 -r
enc_key = bytes.fromhex('3dd855b778ba292eda7bf708a9ea111ee99c5c45d2e9a2773d126de46d344410')
mac_key = bytes.fromhex('0b0b65fdccaea7cf5b9a6214cd867983e2326abeccedf1dceb1feee0ae74075b')
ae_cipher = AES256_CTR_HMAC_SHA256(
iv=0, header_len=0, aad_offset=0,
enc_key=enc_key,
mac_key=mac_key,
)

envelope = ae_cipher.encrypt(plain)

encrypted = msgpack.packb({
'version': 1,
'salt': b'salt'*4,
'argon2_time_cost': 3,
'argon2_memory_cost': 2**16,
'argon2_parallelism': 4,
'argon2_type': b'id',
'algorithm': 'argon2 aes256-ctr hmac-sha256',
'data': envelope,
})
monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
passphrase = Passphrase.new()
key = KeyfileKey(None)

decrypted = key.decrypt_key_file(encrypted, passphrase)

assert decrypted == plain


def test_decrypt_key_file_pbkdf2_sha256_aes256_ctr_hmac_sha256(monkeypatch):
plain = b'hello'
salt = b'salt'*4
iterations = 100000
monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
passphrase = Passphrase.new()
key = passphrase.kdf(salt, iterations, 32)
hash = hmac_sha256(key, plain)
data = AES(key, b'\0'*16).encrypt(plain)
encrypted = msgpack.packb({
'version': 1,
'algorithm': 'sha256',
'iterations': iterations,
'salt': salt,
'data': data,
'hash': hash,
})
key = KeyfileKey(None)

decrypted = key.decrypt_key_file(encrypted, passphrase)

assert decrypted == plain


def test_decrypt_key_file_unsupported_algorithm(monkeypatch):
'''We will add more algorithms in the future. We should raise a helpful error.'''
monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
passphrase = Passphrase.new()
key = KeyfileKey(None)
encrypted = msgpack.packb({
'algorithm': 'THIS ALGORITHM IS NOT SUPPORTED',
'version': 1,
})

with pytest.raises(UnsupportedKeyFormatError):
key.decrypt_key_file(encrypted, passphrase)


def test_decrypt_key_file_v2_is_unsupported(monkeypatch):
'''There may eventually be a version 2 of the format. For now we should raise a helpful error.'''
monkeypatch.setenv('BORG_PASSPHRASE', "hello, pass phrase")
passphrase = Passphrase.new()
key = KeyfileKey(None)
encrypted = msgpack.packb({
'version': 2,
})

with pytest.raises(UnsupportedKeyFormatError):
key.decrypt_key_file(encrypted, passphrase)

0 comments on commit 46243e1

Please sign in to comment.