diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py index 6c8f6ad37..9c608882c 100644 --- a/src/borg/crypto/key.py +++ b/src/borg/crypto/key.py @@ -358,8 +358,10 @@ class AESKeyBase(KeyBase): def encrypt(self, chunk): data = self.compressor.compress(chunk) - self.nonce_manager.ensure_reservation(self.cipher.block_count(len(data))) - return self.cipher.encrypt(data, header=self.TYPE_STR) + next_nonce = int.from_bytes(self.cipher.next_iv(), byteorder='big') + next_nonce = self.nonce_manager.ensure_reservation(next_nonce, self.cipher.block_count(len(data))) + iv = next_nonce.to_bytes(self.cipher.iv_len, byteorder='big') + return self.cipher.encrypt(data, header=self.TYPE_STR, iv=iv) def decrypt(self, id, data, decompress=True): if not (data[0] == self.TYPE or @@ -401,7 +403,7 @@ class AESKeyBase(KeyBase): manifest_blocks = num_cipher_blocks(len(manifest_data)) nonce = self.cipher.extract_iv(manifest_data) + manifest_blocks self.cipher.set_iv(nonce.to_bytes(16, byteorder='big')) - self.nonce_manager = NonceManager(self.repository, self.cipher, nonce) + self.nonce_manager = NonceManager(self.repository, nonce) class Passphrase(str): diff --git a/src/borg/crypto/nonces.py b/src/borg/crypto/nonces.py index 8b4819e28..39ec3d723 100644 --- a/src/borg/crypto/nonces.py +++ b/src/borg/crypto/nonces.py @@ -14,9 +14,8 @@ NONCE_SPACE_RESERVATION = 2**28 # This in units of AES blocksize (16 bytes) class NonceManager: - def __init__(self, repository, cipher, manifest_nonce): + def __init__(self, repository, manifest_nonce): self.repository = repository - self.cipher = cipher self.end_of_nonce_reservation = None self.manifest_nonce = manifest_nonce self.nonce_file = os.path.join(get_security_dir(self.repository.id_str), 'nonce') @@ -47,7 +46,15 @@ class NonceManager: def commit_repo_nonce_reservation(self, next_unreserved, start_nonce): self.repository.commit_nonce_reservation(next_unreserved, start_nonce) - def ensure_reservation(self, nonce_space_needed): + def ensure_reservation(self, nonce, nonce_space_needed): + """ + Call this before doing encryption, give current, yet unused, integer IV as + and the amount of subsequent (counter-like) IVs needed as . + Return value is the IV (counter) integer you shall use for encryption. + + Note: this method may return the you gave, if a reservation for it exists or + can be established, so make sure you give a unused nonce. + """ # Nonces may never repeat, even if a transaction aborts or the system crashes. # Therefore a part of the nonce space is reserved before any nonce is used for encryption. # As these reservations are committed to permanent storage before any nonce is used, this protects @@ -64,20 +71,17 @@ class NonceManager: if self.end_of_nonce_reservation: # we already got a reservation, if nonce_space_needed still fits everything is ok - next_nonce_bytes = self.cipher.next_iv() - next_nonce = int.from_bytes(next_nonce_bytes, byteorder='big') + next_nonce = nonce assert next_nonce <= self.end_of_nonce_reservation if next_nonce + nonce_space_needed <= self.end_of_nonce_reservation: - self.cipher.set_iv(next_nonce_bytes) - return + return next_nonce repo_free_nonce = self.get_repo_free_nonce() local_free_nonce = self.get_local_free_nonce() free_nonce_space = max(x for x in (repo_free_nonce, local_free_nonce, self.manifest_nonce, self.end_of_nonce_reservation) if x is not None) reservation_end = free_nonce_space + nonce_space_needed + NONCE_SPACE_RESERVATION assert reservation_end < MAX_REPRESENTABLE_NONCE - next_nonce_bytes = free_nonce_space.to_bytes(16, byteorder='big') - self.cipher.set_iv(next_nonce_bytes) self.commit_repo_nonce_reservation(reservation_end, repo_free_nonce) self.commit_local_nonce_reservation(reservation_end, local_free_nonce) self.end_of_nonce_reservation = reservation_end + return free_nonce_space diff --git a/src/borg/testsuite/nonces.py b/src/borg/testsuite/nonces.py index 8ee4bfb9a..d0bc85eaf 100644 --- a/src/borg/testsuite/nonces.py +++ b/src/borg/testsuite/nonces.py @@ -33,26 +33,6 @@ class TestNonceManager: def commit_nonce_reservation(self, next_unreserved, start_nonce): pytest.fail("commit_nonce_reservation should never be called on an old repository") - class MockCipher: - def __init__(self, iv): - self.iv_set = False # placeholder, this is never a valid iv - self.iv = iv - - def set_iv(self, iv): - assert iv is not False - self.iv_set = iv - self.iv = iv - - def next_iv(self): - return self.iv - - def expect_iv_and_advance(self, expected_iv, advance): - expected_iv = expected_iv.to_bytes(16, byteorder='big') - iv_set = self.iv_set - assert iv_set == expected_iv - self.iv_set = False - self.iv = advance.to_bytes(16, byteorder='big') - def setUp(self): self.repository = None @@ -67,74 +47,70 @@ class TestNonceManager: def test_empty_cache_and_old_server(self, monkeypatch): monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) - cipher = self.MockCipher(0x2000) self.repository = self.MockOldRepository() - manager = NonceManager(self.repository, cipher, 0x2000) - manager.ensure_reservation(19) - cipher.expect_iv_and_advance(0x2000, 0x2013) + manager = NonceManager(self.repository, 0x2000) + next_nonce = manager.ensure_reservation(0x2000, 19) + assert next_nonce == 0x2000 assert self.cache_nonce() == "0000000000002033" def test_empty_cache(self, monkeypatch): monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) - cipher = self.MockCipher(0x2000) self.repository = self.MockRepository() self.repository.next_free = 0x2000 - manager = NonceManager(self.repository, cipher, 0x2000) - manager.ensure_reservation(19) - cipher.expect_iv_and_advance(0x2000, 0x2013) + manager = NonceManager(self.repository, 0x2000) + next_nonce = manager.ensure_reservation(0x2000, 19) + assert next_nonce == 0x2000 assert self.cache_nonce() == "0000000000002033" def test_empty_nonce(self, monkeypatch): monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) - cipher = self.MockCipher(0x2000) self.repository = self.MockRepository() self.repository.next_free = None - manager = NonceManager(self.repository, cipher, 0x2000) - manager.ensure_reservation(19) - cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + manager = NonceManager(self.repository, 0x2000) + next_nonce = manager.ensure_reservation(0x2000, 19) + assert next_nonce == 0x2000 assert self.cache_nonce() == "0000000000002033" assert self.repository.next_free == 0x2033 # enough space in reservation - manager.ensure_reservation(13) - cipher.expect_iv_and_advance(0x2013, 0x2000 + 19 + 13) + next_nonce = manager.ensure_reservation(0x2013, 13) + assert next_nonce == 0x2013 assert self.cache_nonce() == "0000000000002033" assert self.repository.next_free == 0x2033 # just barely enough space in reservation - manager.ensure_reservation(19) - cipher.expect_iv_and_advance(0x2020, 0x2000 + 19 + 13 + 19) + next_nonce = manager.ensure_reservation(0x2020, 19) + assert next_nonce == 0x2020 assert self.cache_nonce() == "0000000000002033" assert self.repository.next_free == 0x2033 # no space in reservation - manager.ensure_reservation(16) - cipher.expect_iv_and_advance(0x2033, 0x2000 + 19 + 13 + 19 + 16) + next_nonce = manager.ensure_reservation(0x2033, 16) + assert next_nonce == 0x2033 assert self.cache_nonce() == "0000000000002063" assert self.repository.next_free == 0x2063 # spans reservation boundary - manager.ensure_reservation(64) - cipher.expect_iv_and_advance(0x2063, 0x2000 + 19 + 13 + 19 + 16 + 64) + next_nonce = manager.ensure_reservation(0x2043, 64) + assert next_nonce == 0x2063 assert self.cache_nonce() == "00000000000020c3" assert self.repository.next_free == 0x20c3 def test_sync_nonce(self, monkeypatch): monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) - cipher = self.MockCipher(0x2000) self.repository = self.MockRepository() self.repository.next_free = 0x2000 self.set_cache_nonce("0000000000002000") - manager = NonceManager(self.repository, cipher, 0x2000) - manager.ensure_reservation(19) - cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + manager = NonceManager(self.repository, 0x2000) + next_nonce = manager.ensure_reservation(0x2000, 19) + assert next_nonce == 0x2000 assert self.cache_nonce() == "0000000000002033" assert self.repository.next_free == 0x2033 @@ -142,14 +118,13 @@ class TestNonceManager: def test_server_just_upgraded(self, monkeypatch): monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) - cipher = self.MockCipher(0x2000) self.repository = self.MockRepository() self.repository.next_free = None self.set_cache_nonce("0000000000002000") - manager = NonceManager(self.repository, cipher, 0x2000) - manager.ensure_reservation(19) - cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + manager = NonceManager(self.repository, 0x2000) + next_nonce = manager.ensure_reservation(0x2000, 19) + assert next_nonce == 0x2000 assert self.cache_nonce() == "0000000000002033" assert self.repository.next_free == 0x2033 @@ -157,13 +132,12 @@ class TestNonceManager: def test_transaction_abort_no_cache(self, monkeypatch): monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) - cipher = self.MockCipher(0x1000) self.repository = self.MockRepository() self.repository.next_free = 0x2000 - manager = NonceManager(self.repository, cipher, 0x2000) - manager.ensure_reservation(19) - cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + manager = NonceManager(self.repository, 0x2000) + next_nonce = manager.ensure_reservation(0x1000, 19) + assert next_nonce == 0x2000 assert self.cache_nonce() == "0000000000002033" assert self.repository.next_free == 0x2033 @@ -171,27 +145,25 @@ class TestNonceManager: def test_transaction_abort_old_server(self, monkeypatch): monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) - cipher = self.MockCipher(0x1000) self.repository = self.MockOldRepository() self.set_cache_nonce("0000000000002000") - manager = NonceManager(self.repository, cipher, 0x2000) - manager.ensure_reservation(19) - cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + manager = NonceManager(self.repository, 0x2000) + next_nonce = manager.ensure_reservation(0x1000, 19) + assert next_nonce == 0x2000 assert self.cache_nonce() == "0000000000002033" def test_transaction_abort_on_other_client(self, monkeypatch): monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) - cipher = self.MockCipher(0x1000) self.repository = self.MockRepository() self.repository.next_free = 0x2000 self.set_cache_nonce("0000000000001000") - manager = NonceManager(self.repository, cipher, 0x2000) - manager.ensure_reservation(19) - cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + manager = NonceManager(self.repository, 0x2000) + next_nonce = manager.ensure_reservation(0x1000, 19) + assert next_nonce == 0x2000 assert self.cache_nonce() == "0000000000002033" assert self.repository.next_free == 0x2033 @@ -199,14 +171,13 @@ class TestNonceManager: def test_interleaved(self, monkeypatch): monkeypatch.setattr(nonces, 'NONCE_SPACE_RESERVATION', 0x20) - cipher = self.MockCipher(0x2000) self.repository = self.MockRepository() self.repository.next_free = 0x2000 self.set_cache_nonce("0000000000002000") - manager = NonceManager(self.repository, cipher, 0x2000) - manager.ensure_reservation(19) - cipher.expect_iv_and_advance(0x2000, 0x2000 + 19) + manager = NonceManager(self.repository, 0x2000) + next_nonce = manager.ensure_reservation(0x2000, 19) + assert next_nonce == 0x2000 assert self.cache_nonce() == "0000000000002033" assert self.repository.next_free == 0x2033 @@ -215,13 +186,13 @@ class TestNonceManager: self.repository.next_free = 0x4000 # enough space in reservation - manager.ensure_reservation(12) - cipher.expect_iv_and_advance(0x2013, 0x2000 + 19 + 12) + next_nonce = manager.ensure_reservation(0x2013, 12) + assert next_nonce == 0x2013 assert self.cache_nonce() == "0000000000002033" assert self.repository.next_free == 0x4000 # spans reservation boundary - manager.ensure_reservation(21) - cipher.expect_iv_and_advance(0x4000, 0x4000 + 21) + next_nonce = manager.ensure_reservation(0x201f, 21) + assert next_nonce == 0x4000 assert self.cache_nonce() == "0000000000004035" assert self.repository.next_free == 0x4035