key.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import getpass
  2. import os.path
  3. import re
  4. import tempfile
  5. from binascii import hexlify, unhexlify
  6. import pytest
  7. from ..crypto.key import Passphrase, PasswordRetriesExceeded, bin_to_hex
  8. from ..crypto.key import PlaintextKey, PassphraseKey, AuthenticatedKey, RepoKey, KeyfileKey, \
  9. Blake2KeyfileKey, Blake2RepoKey, Blake2AuthenticatedKey
  10. from ..crypto.key import ID_HMAC_SHA_256, ID_BLAKE2b_256
  11. from ..crypto.key import TAMRequiredError, TAMInvalid, TAMUnsupportedSuiteError, UnsupportedManifestError
  12. from ..crypto.key import identify_key
  13. from ..crypto.low_level import bytes_to_long, num_aes_blocks
  14. from ..helpers import IntegrityError
  15. from ..helpers import Location
  16. from ..helpers import StableDict
  17. from ..helpers import get_security_dir
  18. from ..helpers import msgpack
  19. class TestKey:
  20. class MockArgs:
  21. location = Location(tempfile.mkstemp()[1])
  22. keyfile2_key_file = """
  23. BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000
  24. hqppdGVyYXRpb25zzgABhqCkaGFzaNoAIMyonNI+7Cjv0qHi0AOBM6bLGxACJhfgzVD2oq
  25. bIS9SFqWFsZ29yaXRobaZzaGEyNTakc2FsdNoAINNK5qqJc1JWSUjACwFEWGTdM7Nd0a5l
  26. 1uBGPEb+9XM9p3ZlcnNpb24BpGRhdGHaANAYDT5yfPpU099oBJwMomsxouKyx/OG4QIXK2
  27. hQCG2L2L/9PUu4WIuKvGrsXoP7syemujNfcZws5jLp2UPva4PkQhQsrF1RYDEMLh2eF9Ol
  28. rwtkThq1tnh7KjWMG9Ijt7/aoQtq0zDYP/xaFF8XXSJxiyP5zjH5+spB6RL0oQHvbsliSh
  29. /cXJq7jrqmrJ1phd6dg4SHAM/i+hubadZoS6m25OQzYAW09wZD/phG8OVa698Z5ed3HTaT
  30. SmrtgJL3EoOKgUI9d6BLE4dJdBqntifo""".strip()
  31. keyfile2_cdata = unhexlify(re.sub(r'\W', '', """
  32. 0055f161493fcfc16276e8c31493c4641e1eb19a79d0326fad0291e5a9c98e5933
  33. 00000000000003e8d21eaf9b86c297a8cd56432e1915bb
  34. """))
  35. keyfile2_id = unhexlify('c3fbf14bc001ebcc3cd86e696c13482ed071740927cd7cbe1b01b4bfcee49314')
  36. keyfile_blake2_key_file = """
  37. BORG_KEY 0000000000000000000000000000000000000000000000000000000000000000
  38. hqlhbGdvcml0aG2mc2hhMjU2pGRhdGHaAZBu680Do3CmfWzeMCwe48KJi3Vps9mEDy7MKF
  39. TastsEhiAd1RQMuxfZpklkLeddMMWk+aPtFiURRFb02JLXV5cKRC1o2ZDdiNa0nao+o6+i
  40. gUjjsea9TAu25t3vxh8uQWs5BuKRLBRr0nUgrSd0IYMUgn+iVbLJRzCCssvxsklkwQxN3F
  41. Y+MvBnn8kUXSeoSoQ2l0fBHzq94Y7LMOm/owMam5URnE8/UEc6ZXBrbyX4EXxDtUqJcs+D
  42. i451thtlGdigDLpvf9nyK66mjiCpPCTCgtlzq0Pe1jcdhnsUYLg+qWzXZ7e2opEZoC6XxS
  43. 3DIuBOxG3Odqj9IKB+6/kl94vz98awPWFSpYcLZVWu7sIP38ZkUK+ad5MHTo/LvTuZdFnd
  44. iqKzZIDUJl3Zl1WGmP/0xVOmfIlznkCZy4d3SMuujwIcqQ5kDvwDRPpdhBBk+UWQY5vFXk
  45. kR1NBNLSTyhAzu3fiUmFl0qZ+UWPRkGAEBy/NuoEibrWwab8BX97cATyvnmOqYkU9PT0C6
  46. l2l9E4bPpGhhc2jaACDnIa8KgKv84/b5sjaMgSZeIVkuKSLJy2NN8zoH8lnd36ppdGVyYX
  47. Rpb25zzgABhqCkc2FsdNoAIEJLlLh7q74j3q53856H5GgzA1HH+aW5bA/as544+PGkp3Zl
  48. cnNpb24B""".strip()
  49. keyfile_blake2_cdata = bytes.fromhex('04fdf9475cf2323c0ba7a99ddc011064f2e7d039f539f2e448'
  50. '0e6f5fc6ff9993d604040404040404098c8cee1c6db8c28947')
  51. # Verified against b2sum. Entire string passed to BLAKE2, including the padded 64 byte key contained in
  52. # keyfile_blake2_key_file above is
  53. # 19280471de95185ec27ecb6fc9edbb4f4db26974c315ede1cd505fab4250ce7cd0d081ea66946c
  54. # 95f0db934d5f616921efbd869257e8ded2bd9bd93d7f07b1a30000000000000000000000000000
  55. # 000000000000000000000000000000000000000000000000000000000000000000000000000000
  56. # 00000000000000000000007061796c6f6164
  57. # p a y l o a d
  58. keyfile_blake2_id = bytes.fromhex('d8bc68e961c79f99be39061589e5179b2113cd9226e07b08ddd4a1fef7ce93fb')
  59. @pytest.fixture
  60. def keys_dir(self, request, monkeypatch, tmpdir):
  61. monkeypatch.setenv('BORG_KEYS_DIR', str(tmpdir))
  62. return tmpdir
  63. @pytest.fixture(params=(
  64. PlaintextKey,
  65. AuthenticatedKey,
  66. KeyfileKey,
  67. RepoKey,
  68. Blake2KeyfileKey,
  69. Blake2RepoKey,
  70. Blake2AuthenticatedKey,
  71. ))
  72. def key(self, request, monkeypatch):
  73. monkeypatch.setenv('BORG_PASSPHRASE', 'test')
  74. return request.param.create(self.MockRepository(), self.MockArgs())
  75. class MockRepository:
  76. class _Location:
  77. raw = processed = '/some/place'
  78. def canonical_path(self):
  79. return self.processed
  80. _location = _Location()
  81. id = bytes(32)
  82. id_str = bin_to_hex(id)
  83. def get_free_nonce(self):
  84. return None
  85. def commit_nonce_reservation(self, next_unreserved, start_nonce):
  86. pass
  87. def save_key(self, data):
  88. self.key_data = data
  89. def load_key(self):
  90. return self.key_data
  91. def test_plaintext(self):
  92. key = PlaintextKey.create(None, None)
  93. chunk = b'foo'
  94. assert hexlify(key.id_hash(chunk)) == b'2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'
  95. assert chunk == key.decrypt(key.id_hash(chunk), key.encrypt(chunk))
  96. def test_keyfile(self, monkeypatch, keys_dir):
  97. monkeypatch.setenv('BORG_PASSPHRASE', 'test')
  98. key = KeyfileKey.create(self.MockRepository(), self.MockArgs())
  99. assert bytes_to_long(key.enc_cipher.iv, 8) == 0
  100. manifest = key.encrypt(b'ABC')
  101. assert key.extract_nonce(manifest) == 0
  102. manifest2 = key.encrypt(b'ABC')
  103. assert manifest != manifest2
  104. assert key.decrypt(None, manifest) == key.decrypt(None, manifest2)
  105. assert key.extract_nonce(manifest2) == 1
  106. iv = key.extract_nonce(manifest)
  107. key2 = KeyfileKey.detect(self.MockRepository(), manifest)
  108. assert bytes_to_long(key2.enc_cipher.iv, 8) >= iv + num_aes_blocks(len(manifest) - KeyfileKey.PAYLOAD_OVERHEAD)
  109. # Key data sanity check
  110. assert len({key2.id_key, key2.enc_key, key2.enc_hmac_key}) == 3
  111. assert key2.chunk_seed != 0
  112. chunk = b'foo'
  113. assert chunk == key2.decrypt(key.id_hash(chunk), key.encrypt(chunk))
  114. def test_keyfile_nonce_rollback_protection(self, monkeypatch, keys_dir):
  115. monkeypatch.setenv('BORG_PASSPHRASE', 'test')
  116. repository = self.MockRepository()
  117. with open(os.path.join(get_security_dir(repository.id_str), 'nonce'), "w") as fd:
  118. fd.write("0000000000002000")
  119. key = KeyfileKey.create(repository, self.MockArgs())
  120. data = key.encrypt(b'ABC')
  121. assert key.extract_nonce(data) == 0x2000
  122. assert key.decrypt(None, data) == b'ABC'
  123. def test_keyfile_kfenv(self, tmpdir, monkeypatch):
  124. keyfile = tmpdir.join('keyfile')
  125. monkeypatch.setenv('BORG_KEY_FILE', str(keyfile))
  126. monkeypatch.setenv('BORG_PASSPHRASE', 'testkf')
  127. assert not keyfile.exists()
  128. key = KeyfileKey.create(self.MockRepository(), self.MockArgs())
  129. assert keyfile.exists()
  130. chunk = b'ABC'
  131. chunk_id = key.id_hash(chunk)
  132. chunk_cdata = key.encrypt(chunk)
  133. key = KeyfileKey.detect(self.MockRepository(), chunk_cdata)
  134. assert chunk == key.decrypt(chunk_id, chunk_cdata)
  135. keyfile.remove()
  136. with pytest.raises(FileNotFoundError):
  137. KeyfileKey.detect(self.MockRepository(), chunk_cdata)
  138. def test_keyfile2(self, monkeypatch, keys_dir):
  139. with keys_dir.join('keyfile').open('w') as fd:
  140. fd.write(self.keyfile2_key_file)
  141. monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase')
  142. key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
  143. assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata) == b'payload'
  144. def test_keyfile2_kfenv(self, tmpdir, monkeypatch):
  145. keyfile = tmpdir.join('keyfile')
  146. with keyfile.open('w') as fd:
  147. fd.write(self.keyfile2_key_file)
  148. monkeypatch.setenv('BORG_KEY_FILE', str(keyfile))
  149. monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase')
  150. key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
  151. assert key.decrypt(self.keyfile2_id, self.keyfile2_cdata) == b'payload'
  152. def test_keyfile_blake2(self, monkeypatch, keys_dir):
  153. with keys_dir.join('keyfile').open('w') as fd:
  154. fd.write(self.keyfile_blake2_key_file)
  155. monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase')
  156. key = Blake2KeyfileKey.detect(self.MockRepository(), self.keyfile_blake2_cdata)
  157. assert key.decrypt(self.keyfile_blake2_id, self.keyfile_blake2_cdata) == b'payload'
  158. def test_passphrase(self, keys_dir, monkeypatch):
  159. monkeypatch.setenv('BORG_PASSPHRASE', 'test')
  160. key = PassphraseKey.create(self.MockRepository(), None)
  161. assert bytes_to_long(key.enc_cipher.iv, 8) == 0
  162. assert hexlify(key.id_key) == b'793b0717f9d8fb01c751a487e9b827897ceea62409870600013fbc6b4d8d7ca6'
  163. assert hexlify(key.enc_hmac_key) == b'b885a05d329a086627412a6142aaeb9f6c54ab7950f996dd65587251f6bc0901'
  164. assert hexlify(key.enc_key) == b'2ff3654c6daf7381dbbe718d2b20b4f1ea1e34caa6cc65f6bb3ac376b93fed2a'
  165. assert key.chunk_seed == -775740477
  166. manifest = key.encrypt(b'ABC')
  167. assert key.extract_nonce(manifest) == 0
  168. manifest2 = key.encrypt(b'ABC')
  169. assert manifest != manifest2
  170. assert key.decrypt(None, manifest) == key.decrypt(None, manifest2)
  171. assert key.extract_nonce(manifest2) == 1
  172. iv = key.extract_nonce(manifest)
  173. key2 = PassphraseKey.detect(self.MockRepository(), manifest)
  174. assert bytes_to_long(key2.enc_cipher.iv, 8) == iv + num_aes_blocks(len(manifest) - PassphraseKey.PAYLOAD_OVERHEAD)
  175. assert key.id_key == key2.id_key
  176. assert key.enc_hmac_key == key2.enc_hmac_key
  177. assert key.enc_key == key2.enc_key
  178. assert key.chunk_seed == key2.chunk_seed
  179. chunk = b'foo'
  180. assert hexlify(key.id_hash(chunk)) == b'818217cf07d37efad3860766dcdf1d21e401650fed2d76ed1d797d3aae925990'
  181. assert chunk == key2.decrypt(key2.id_hash(chunk), key.encrypt(chunk))
  182. def _corrupt_byte(self, key, data, offset):
  183. data = bytearray(data)
  184. data[offset] ^= 1
  185. with pytest.raises(IntegrityError):
  186. key.decrypt(b'', data)
  187. def test_decrypt_integrity(self, monkeypatch, keys_dir):
  188. with keys_dir.join('keyfile').open('w') as fd:
  189. fd.write(self.keyfile2_key_file)
  190. monkeypatch.setenv('BORG_PASSPHRASE', 'passphrase')
  191. key = KeyfileKey.detect(self.MockRepository(), self.keyfile2_cdata)
  192. data = self.keyfile2_cdata
  193. for i in range(len(data)):
  194. self._corrupt_byte(key, data, i)
  195. with pytest.raises(IntegrityError):
  196. data = bytearray(self.keyfile2_cdata)
  197. id = bytearray(key.id_hash(data)) # corrupt chunk id
  198. id[12] = 0
  199. key.decrypt(id, data)
  200. def test_roundtrip(self, key):
  201. repository = key.repository
  202. plaintext = b'foo'
  203. encrypted = key.encrypt(plaintext)
  204. identified_key_class = identify_key(encrypted)
  205. assert identified_key_class == key.__class__
  206. loaded_key = identified_key_class.detect(repository, encrypted)
  207. decrypted = loaded_key.decrypt(None, encrypted)
  208. assert decrypted == plaintext
  209. def test_decrypt_decompress(self, key):
  210. plaintext = b'123456789'
  211. encrypted = key.encrypt(plaintext)
  212. assert key.decrypt(None, encrypted, decompress=False) != plaintext
  213. assert key.decrypt(None, encrypted) == plaintext
  214. def test_assert_id(self, key):
  215. plaintext = b'123456789'
  216. id = key.id_hash(plaintext)
  217. key.assert_id(id, plaintext)
  218. id_changed = bytearray(id)
  219. id_changed[0] ^= 1
  220. with pytest.raises(IntegrityError):
  221. key.assert_id(id_changed, plaintext)
  222. plaintext_changed = plaintext + b'1'
  223. with pytest.raises(IntegrityError):
  224. key.assert_id(id, plaintext_changed)
  225. def test_authenticated_encrypt(self, monkeypatch):
  226. monkeypatch.setenv('BORG_PASSPHRASE', 'test')
  227. key = AuthenticatedKey.create(self.MockRepository(), self.MockArgs())
  228. assert AuthenticatedKey.id_hash is ID_HMAC_SHA_256.id_hash
  229. assert len(key.id_key) == 32
  230. plaintext = b'123456789'
  231. authenticated = key.encrypt(plaintext)
  232. # 0x07 is the key TYPE, 0x0100 identifies LZ4 compression, 0x90 is part of LZ4 and means that an uncompressed
  233. # block of length nine follows (the plaintext).
  234. assert authenticated == b'\x07\x01\x00\x90' + plaintext
  235. def test_blake2_authenticated_encrypt(self, monkeypatch):
  236. monkeypatch.setenv('BORG_PASSPHRASE', 'test')
  237. key = Blake2AuthenticatedKey.create(self.MockRepository(), self.MockArgs())
  238. assert Blake2AuthenticatedKey.id_hash is ID_BLAKE2b_256.id_hash
  239. assert len(key.id_key) == 128
  240. plaintext = b'123456789'
  241. authenticated = key.encrypt(plaintext)
  242. # 0x06 is the key TYPE, 0x0100 identifies LZ4 compression, 0x90 is part of LZ4 and means that an uncompressed
  243. # block of length nine follows (the plaintext).
  244. assert authenticated == b'\x06\x01\x00\x90' + plaintext
  245. class TestPassphrase:
  246. def test_passphrase_new_verification(self, capsys, monkeypatch):
  247. monkeypatch.setattr(getpass, 'getpass', lambda prompt: "12aöäü")
  248. monkeypatch.setenv('BORG_DISPLAY_PASSPHRASE', 'no')
  249. Passphrase.new()
  250. out, err = capsys.readouterr()
  251. assert "12" not in out
  252. assert "12" not in err
  253. monkeypatch.setenv('BORG_DISPLAY_PASSPHRASE', 'yes')
  254. passphrase = Passphrase.new()
  255. out, err = capsys.readouterr()
  256. assert "313261c3b6c3a4c3bc" not in out
  257. assert "313261c3b6c3a4c3bc" in err
  258. assert passphrase == "12aöäü"
  259. monkeypatch.setattr(getpass, 'getpass', lambda prompt: "1234/@=")
  260. Passphrase.new()
  261. out, err = capsys.readouterr()
  262. assert "1234/@=" not in out
  263. assert "1234/@=" in err
  264. def test_passphrase_new_empty(self, capsys, monkeypatch):
  265. monkeypatch.delenv('BORG_PASSPHRASE', False)
  266. monkeypatch.setattr(getpass, 'getpass', lambda prompt: "")
  267. with pytest.raises(PasswordRetriesExceeded):
  268. Passphrase.new(allow_empty=False)
  269. out, err = capsys.readouterr()
  270. assert "must not be blank" in err
  271. def test_passphrase_new_retries(self, monkeypatch):
  272. monkeypatch.delenv('BORG_PASSPHRASE', False)
  273. ascending_numbers = iter(range(20))
  274. monkeypatch.setattr(getpass, 'getpass', lambda prompt: str(next(ascending_numbers)))
  275. with pytest.raises(PasswordRetriesExceeded):
  276. Passphrase.new()
  277. def test_passphrase_repr(self):
  278. assert "secret" not in repr(Passphrase("secret"))
  279. class TestTAM:
  280. @pytest.fixture
  281. def key(self, monkeypatch):
  282. monkeypatch.setenv('BORG_PASSPHRASE', 'test')
  283. return KeyfileKey.create(TestKey.MockRepository(), TestKey.MockArgs())
  284. def test_unpack_future(self, key):
  285. blob = b'\xc1\xc1\xc1\xc1foobar'
  286. with pytest.raises(UnsupportedManifestError):
  287. key.unpack_and_verify_manifest(blob)
  288. blob = b'\xc1\xc1\xc1'
  289. with pytest.raises((ValueError, msgpack.UnpackException)):
  290. key.unpack_and_verify_manifest(blob)
  291. def test_missing_when_required(self, key):
  292. blob = msgpack.packb({})
  293. with pytest.raises(TAMRequiredError):
  294. key.unpack_and_verify_manifest(blob)
  295. def test_missing(self, key):
  296. blob = msgpack.packb({})
  297. key.tam_required = False
  298. unpacked, verified = key.unpack_and_verify_manifest(blob)
  299. assert unpacked == {}
  300. assert not verified
  301. def test_unknown_type_when_required(self, key):
  302. blob = msgpack.packb({
  303. 'tam': {
  304. 'type': 'HMAC_VOLLBIT',
  305. },
  306. })
  307. with pytest.raises(TAMUnsupportedSuiteError):
  308. key.unpack_and_verify_manifest(blob)
  309. def test_unknown_type(self, key):
  310. blob = msgpack.packb({
  311. 'tam': {
  312. 'type': 'HMAC_VOLLBIT',
  313. },
  314. })
  315. key.tam_required = False
  316. unpacked, verified = key.unpack_and_verify_manifest(blob)
  317. assert unpacked == {}
  318. assert not verified
  319. @pytest.mark.parametrize('tam, exc', (
  320. ({}, TAMUnsupportedSuiteError),
  321. ({'type': b'\xff'}, TAMUnsupportedSuiteError),
  322. (None, TAMInvalid),
  323. (1234, TAMInvalid),
  324. ))
  325. def test_invalid(self, key, tam, exc):
  326. blob = msgpack.packb({
  327. 'tam': tam,
  328. })
  329. with pytest.raises(exc):
  330. key.unpack_and_verify_manifest(blob)
  331. @pytest.mark.parametrize('hmac, salt', (
  332. ({}, bytes(64)),
  333. (bytes(64), {}),
  334. (None, bytes(64)),
  335. (bytes(64), None),
  336. ))
  337. def test_wrong_types(self, key, hmac, salt):
  338. data = {
  339. 'tam': {
  340. 'type': 'HKDF_HMAC_SHA512',
  341. 'hmac': hmac,
  342. 'salt': salt
  343. },
  344. }
  345. tam = data['tam']
  346. if hmac is None:
  347. del tam['hmac']
  348. if salt is None:
  349. del tam['salt']
  350. blob = msgpack.packb(data)
  351. with pytest.raises(TAMInvalid):
  352. key.unpack_and_verify_manifest(blob)
  353. def test_round_trip(self, key):
  354. data = {'foo': 'bar'}
  355. blob = key.pack_and_authenticate_metadata(data)
  356. assert blob.startswith(b'\x82')
  357. unpacked = msgpack.unpackb(blob)
  358. assert unpacked[b'tam'][b'type'] == b'HKDF_HMAC_SHA512'
  359. unpacked, verified = key.unpack_and_verify_manifest(blob)
  360. assert verified
  361. assert unpacked[b'foo'] == b'bar'
  362. assert b'tam' not in unpacked
  363. @pytest.mark.parametrize('which', (b'hmac', b'salt'))
  364. def test_tampered(self, key, which):
  365. data = {'foo': 'bar'}
  366. blob = key.pack_and_authenticate_metadata(data)
  367. assert blob.startswith(b'\x82')
  368. unpacked = msgpack.unpackb(blob, object_hook=StableDict)
  369. assert len(unpacked[b'tam'][which]) == 64
  370. unpacked[b'tam'][which] = unpacked[b'tam'][which][0:32] + bytes(32)
  371. assert len(unpacked[b'tam'][which]) == 64
  372. blob = msgpack.packb(unpacked)
  373. with pytest.raises(TAMInvalid):
  374. key.unpack_and_verify_manifest(blob)