mirror of https://github.com/borgbackup/borg.git
279 lines
13 KiB
Python
279 lines
13 KiB
Python
# Note: these tests are part of the self test, do not use or import pytest functionality here.
|
|
# See borg.selftest for details. If you add/remove test methods, update SELFTEST_COUNT
|
|
|
|
from unittest.mock import MagicMock
|
|
import unittest
|
|
|
|
from ..crypto.low_level import AES256_CTR_HMAC_SHA256, AES256_OCB, CHACHA20_POLY1305, UNENCRYPTED, IntegrityError
|
|
from ..crypto.low_level import bytes_to_long, bytes_to_int, long_to_bytes
|
|
from ..crypto.low_level import AES, hmac_sha256
|
|
from ..crypto.key import CHPOKeyfileKey, AESOCBRepoKey, FlexiKey
|
|
from ..helpers import msgpack, bin_to_hex
|
|
|
|
from . import BaseTestCase
|
|
|
|
|
|
class CryptoTestCase(BaseTestCase):
|
|
def test_bytes_to_int(self):
|
|
self.assert_equal(bytes_to_int(b"\0\0\0\1"), 1)
|
|
|
|
def test_bytes_to_long(self):
|
|
self.assert_equal(bytes_to_long(b"\0\0\0\0\0\0\0\1"), 1)
|
|
self.assert_equal(long_to_bytes(1), b"\0\0\0\0\0\0\0\1")
|
|
|
|
def test_UNENCRYPTED(self):
|
|
iv = b"" # any IV is ok, it just must be set and not None
|
|
data = b"data"
|
|
header = b"header"
|
|
cs = UNENCRYPTED(None, None, iv, header_len=6)
|
|
envelope = cs.encrypt(data, header=header)
|
|
self.assert_equal(envelope, header + data)
|
|
got_data = cs.decrypt(envelope)
|
|
self.assert_equal(got_data, data)
|
|
|
|
def test_AES256_CTR_HMAC_SHA256(self):
|
|
# this tests the layout as in borg < 1.2 (1 type byte, no aad)
|
|
mac_key = b"Y" * 32
|
|
enc_key = b"X" * 32
|
|
iv = 0
|
|
data = b"foo" * 10
|
|
header = b"\x42"
|
|
# encrypt-then-mac
|
|
cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, iv, header_len=1, aad_offset=1)
|
|
hdr_mac_iv_cdata = cs.encrypt(data, header=header)
|
|
hdr = hdr_mac_iv_cdata[0:1]
|
|
mac = hdr_mac_iv_cdata[1:33]
|
|
iv = hdr_mac_iv_cdata[33:41]
|
|
cdata = hdr_mac_iv_cdata[41:]
|
|
self.assert_equal(bin_to_hex(hdr), "42")
|
|
self.assert_equal(bin_to_hex(mac), "af90b488b0cc4a8f768fe2d6814fa65aec66b148135e54f7d4d29a27f22f57a8")
|
|
self.assert_equal(bin_to_hex(iv), "0000000000000000")
|
|
self.assert_equal(bin_to_hex(cdata), "c6efb702de12498f34a2c2bbc8149e759996d08bf6dc5c610aefc0c3a466")
|
|
self.assert_equal(cs.next_iv(), 2)
|
|
# auth-then-decrypt
|
|
cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, header_len=len(header), aad_offset=1)
|
|
pdata = cs.decrypt(hdr_mac_iv_cdata)
|
|
self.assert_equal(data, pdata)
|
|
self.assert_equal(cs.next_iv(), 2)
|
|
# auth-failure due to corruption (corrupted data)
|
|
cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, header_len=len(header), aad_offset=1)
|
|
hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:41] + b"\0" + hdr_mac_iv_cdata[42:]
|
|
self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted))
|
|
|
|
def test_AES256_CTR_HMAC_SHA256_aad(self):
|
|
mac_key = b"Y" * 32
|
|
enc_key = b"X" * 32
|
|
iv = 0
|
|
data = b"foo" * 10
|
|
header = b"\x12\x34\x56"
|
|
# encrypt-then-mac
|
|
cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, iv, header_len=3, aad_offset=1)
|
|
hdr_mac_iv_cdata = cs.encrypt(data, header=header)
|
|
hdr = hdr_mac_iv_cdata[0:3]
|
|
mac = hdr_mac_iv_cdata[3:35]
|
|
iv = hdr_mac_iv_cdata[35:43]
|
|
cdata = hdr_mac_iv_cdata[43:]
|
|
self.assert_equal(bin_to_hex(hdr), "123456")
|
|
self.assert_equal(bin_to_hex(mac), "7659a915d9927072ef130258052351a17ef882692893c3850dd798c03d2dd138")
|
|
self.assert_equal(bin_to_hex(iv), "0000000000000000")
|
|
self.assert_equal(bin_to_hex(cdata), "c6efb702de12498f34a2c2bbc8149e759996d08bf6dc5c610aefc0c3a466")
|
|
self.assert_equal(cs.next_iv(), 2)
|
|
# auth-then-decrypt
|
|
cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, header_len=len(header), aad_offset=1)
|
|
pdata = cs.decrypt(hdr_mac_iv_cdata)
|
|
self.assert_equal(data, pdata)
|
|
self.assert_equal(cs.next_iv(), 2)
|
|
# auth-failure due to corruption (corrupted aad)
|
|
cs = AES256_CTR_HMAC_SHA256(mac_key, enc_key, header_len=len(header), aad_offset=1)
|
|
hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:1] + b"\0" + hdr_mac_iv_cdata[2:]
|
|
self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted))
|
|
|
|
def test_AE(self):
|
|
# used in legacy-like layout (1 type byte, no aad)
|
|
key = b"X" * 32
|
|
iv_int = 0
|
|
data = b"foo" * 10
|
|
header = b"\x23" + iv_int.to_bytes(12, "big")
|
|
tests = [
|
|
# (ciphersuite class, exp_mac, exp_cdata)
|
|
(
|
|
AES256_OCB,
|
|
"b6909c23c9aaebd9abbe1ff42097652d",
|
|
"877ce46d2f62dee54699cebc3ba41d9ab613f7c486778c1b3636664b1493",
|
|
),
|
|
(
|
|
CHACHA20_POLY1305,
|
|
"fd08594796e0706cde1e8b461e3e0555",
|
|
"a093e4b0387526f085d3c40cca84a35230a5c0dd766453b77ba38bcff775",
|
|
),
|
|
]
|
|
for cs_cls, exp_mac, exp_cdata in tests:
|
|
# print(repr(cs_cls))
|
|
# encrypt/mac
|
|
cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)
|
|
hdr_mac_iv_cdata = cs.encrypt(data, header=header)
|
|
hdr = hdr_mac_iv_cdata[0:1]
|
|
iv = hdr_mac_iv_cdata[1:13]
|
|
mac = hdr_mac_iv_cdata[13:29]
|
|
cdata = hdr_mac_iv_cdata[29:]
|
|
self.assert_equal(bin_to_hex(hdr), "23")
|
|
self.assert_equal(bin_to_hex(mac), exp_mac)
|
|
self.assert_equal(bin_to_hex(iv), "000000000000000000000000")
|
|
self.assert_equal(bin_to_hex(cdata), exp_cdata)
|
|
self.assert_equal(cs.next_iv(), 1)
|
|
# auth/decrypt
|
|
cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)
|
|
pdata = cs.decrypt(hdr_mac_iv_cdata)
|
|
self.assert_equal(data, pdata)
|
|
self.assert_equal(cs.next_iv(), 1)
|
|
# auth-failure due to corruption (corrupted data)
|
|
cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)
|
|
hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:29] + b"\0" + hdr_mac_iv_cdata[30:]
|
|
self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted))
|
|
|
|
def test_AEAD(self):
|
|
# test with aad
|
|
key = b"X" * 32
|
|
iv_int = 0
|
|
data = b"foo" * 10
|
|
header = b"\x12\x34\x56" + iv_int.to_bytes(12, "big")
|
|
tests = [
|
|
# (ciphersuite class, exp_mac, exp_cdata)
|
|
(
|
|
AES256_OCB,
|
|
"f2748c412af1c7ead81863a18c2c1893",
|
|
"877ce46d2f62dee54699cebc3ba41d9ab613f7c486778c1b3636664b1493",
|
|
),
|
|
(
|
|
CHACHA20_POLY1305,
|
|
"b7e7c9a79f2404e14f9aad156bf091dd",
|
|
"a093e4b0387526f085d3c40cca84a35230a5c0dd766453b77ba38bcff775",
|
|
),
|
|
]
|
|
for cs_cls, exp_mac, exp_cdata in tests:
|
|
# print(repr(cs_cls))
|
|
# encrypt/mac
|
|
cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)
|
|
hdr_mac_iv_cdata = cs.encrypt(data, header=header)
|
|
hdr = hdr_mac_iv_cdata[0:3]
|
|
iv = hdr_mac_iv_cdata[3:15]
|
|
mac = hdr_mac_iv_cdata[15:31]
|
|
cdata = hdr_mac_iv_cdata[31:]
|
|
self.assert_equal(bin_to_hex(hdr), "123456")
|
|
self.assert_equal(bin_to_hex(mac), exp_mac)
|
|
self.assert_equal(bin_to_hex(iv), "000000000000000000000000")
|
|
self.assert_equal(bin_to_hex(cdata), exp_cdata)
|
|
self.assert_equal(cs.next_iv(), 1)
|
|
# auth/decrypt
|
|
cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)
|
|
pdata = cs.decrypt(hdr_mac_iv_cdata)
|
|
self.assert_equal(data, pdata)
|
|
self.assert_equal(cs.next_iv(), 1)
|
|
# auth-failure due to corruption (corrupted aad)
|
|
cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=1)
|
|
hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:1] + b"\0" + hdr_mac_iv_cdata[2:]
|
|
self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted))
|
|
|
|
def test_AEAD_with_more_AAD(self):
|
|
# test giving extra aad to the .encrypt() and .decrypt() calls
|
|
key = b"X" * 32
|
|
iv_int = 0
|
|
data = b"foo" * 10
|
|
header = b"\x12\x34"
|
|
tests = [AES256_OCB, CHACHA20_POLY1305]
|
|
for cs_cls in tests:
|
|
# encrypt/mac
|
|
cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=0)
|
|
hdr_mac_iv_cdata = cs.encrypt(data, header=header, aad=b"correct_chunkid")
|
|
# successful auth/decrypt (correct aad)
|
|
cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=0)
|
|
pdata = cs.decrypt(hdr_mac_iv_cdata, aad=b"correct_chunkid")
|
|
self.assert_equal(data, pdata)
|
|
# unsuccessful auth (incorrect aad)
|
|
cs = cs_cls(key, iv_int, header_len=len(header), aad_offset=0)
|
|
self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata, aad=b"incorrect_chunkid"))
|
|
|
|
|
|
def test_decrypt_key_file_argon2_chacha20_poly1305():
|
|
plain = b"hello"
|
|
# echo -n "hello, pass phrase" | argon2 saltsaltsaltsalt -id -t 1 -k 8 -p 1 -l 32 -r
|
|
key = bytes.fromhex("a1b0cba145c154fbd8960996c5ce3428e9920cfe53c84ef08b4102a70832bcec")
|
|
ae_cipher = CHACHA20_POLY1305(key=key, iv=0, header_len=0, aad_offset=0)
|
|
|
|
envelope = ae_cipher.encrypt(plain)
|
|
|
|
encrypted = msgpack.packb(
|
|
{
|
|
"version": 1,
|
|
"salt": b"salt" * 4,
|
|
"argon2_time_cost": 1,
|
|
"argon2_memory_cost": 8,
|
|
"argon2_parallelism": 1,
|
|
"argon2_type": b"id",
|
|
"algorithm": "argon2 chacha20-poly1305",
|
|
"data": envelope,
|
|
}
|
|
)
|
|
key = CHPOKeyfileKey(None)
|
|
|
|
decrypted = key.decrypt_key_file(encrypted, "hello, pass phrase")
|
|
|
|
assert decrypted == plain
|
|
|
|
|
|
def test_decrypt_key_file_pbkdf2_sha256_aes256_ctr_hmac_sha256():
|
|
plain = b"hello"
|
|
salt = b"salt" * 4
|
|
passphrase = "hello, pass phrase"
|
|
key = FlexiKey.pbkdf2(passphrase, salt, 1, 32)
|
|
hash = hmac_sha256(key, plain)
|
|
data = AES(key, b"\0" * 16).encrypt(plain)
|
|
encrypted = msgpack.packb(
|
|
{"version": 1, "algorithm": "sha256", "iterations": 1, "salt": salt, "data": data, "hash": hash}
|
|
)
|
|
key = CHPOKeyfileKey(None)
|
|
|
|
decrypted = key.decrypt_key_file(encrypted, passphrase)
|
|
|
|
assert decrypted == plain
|
|
|
|
|
|
@unittest.mock.patch("getpass.getpass")
|
|
def test_repo_key_detect_does_not_raise_integrity_error(getpass, monkeypatch):
|
|
"""https://github.com/borgbackup/borg/pull/6469#discussion_r832670411
|
|
|
|
This is a regression test for a bug I introduced and fixed:
|
|
|
|
Traceback (most recent call last):
|
|
File "/home/user/borg-master/src/borg/testsuite/crypto.py", line 384,
|
|
in test_repo_key_detect_does_not_raise_integrity_error
|
|
RepoKey.detect(repository, manifest_data=None)
|
|
File "/home/user/borg-master/src/borg/crypto/key.py", line 402, in detect
|
|
if not key.load(target, passphrase):
|
|
File "/home/user/borg-master/src/borg/crypto/key.py", line 654, in load
|
|
success = self._load(key_data, passphrase)
|
|
File "/home/user/borg-master/src/borg/crypto/key.py", line 418, in _load
|
|
data = self.decrypt_key_file(cdata, passphrase)
|
|
File "/home/user/borg-master/src/borg/crypto/key.py", line 444, in decrypt_key_file
|
|
return self.decrypt_key_file_argon2(encrypted_key, passphrase)
|
|
File "/home/user/borg-master/src/borg/crypto/key.py", line 470, in decrypt_key_file_argon2
|
|
return ae_cipher.decrypt(encrypted_key.data)
|
|
File "src/borg/crypto/low_level.pyx", line 302, in borg.crypto.low_level.AES256_CTR_BASE.decrypt
|
|
self.mac_verify(<const unsigned char *> idata.buf+aoffset, alen,
|
|
File "src/borg/crypto/low_level.pyx", line 382, in borg.crypto.low_level.AES256_CTR_HMAC_SHA256.mac_verify
|
|
raise IntegrityError('MAC Authentication failed')
|
|
borg.crypto.low_level.IntegrityError: MAC Authentication failed
|
|
|
|
1. FlexiKey.decrypt_key_file() is supposed to signal the decryption failure by returning None
|
|
2. FlexiKey.detect() relies on that interface - it tries an empty passphrase before prompting the user
|
|
3. my initial implementation of decrypt_key_file_argon2() was simply passing through the IntegrityError()
|
|
from AES256_CTR_BASE.decrypt()
|
|
"""
|
|
repository = MagicMock(id=b"repository_id")
|
|
getpass.return_value = "hello, pass phrase"
|
|
monkeypatch.setenv("BORG_DISPLAY_PASSPHRASE", "no")
|
|
AESOCBRepoKey.create(repository, args=MagicMock(key_algorithm="argon2"))
|
|
repository.load_key.return_value = repository.save_key.call_args.args[0]
|
|
|
|
AESOCBRepoKey.detect(repository, manifest_data=None)
|