def unlock_manifest( manifest_filename: str, private_key_filename: str, load: Callable[[str, IOIter], IOIter], options: OptionsDict, ) -> Manifest: """ Load a manifest into local storage and unencrypt it :param manifest_filename: the name of the manifest to unlock :param private_key_filename: the private key file in PEM format used to encrypt the manifest's keypair :param load: the _load function from the backup store :param options: backup store options :returns: the requested Manifest """ local_manifest_filename = path_join(get_scratch_dir(), manifest_filename) logger.debug(f'Unlocking manifest at {local_manifest_filename}') # First use the private key to read the AES key and nonce used to encrypt the manifest key_pair = b'' if options['use_encryption']: key_pair = get_manifest_keypair(manifest_filename, private_key_filename, load) # Now use the key and nonce to decrypt the manifest with IOIter() as encrypted_local_manifest, \ IOIter(local_manifest_filename, check_mtime=False) as local_manifest: load(manifest_filename, encrypted_local_manifest) decrypt_and_unpack(encrypted_local_manifest, local_manifest, key_pair, options) return Manifest(local_manifest_filename)
def test_decrypt_and_unpack_no_compression_no_encryption( caplog, mock_open_streams): orig, new, _ = mock_open_streams decrypt_and_unpack(orig, new, b'', dict(use_compression=False, use_encryption=False)) assert new._fd.getvalue() == orig._fd.getvalue() assert count_matching_log_lines('read 2 bytes from /orig', caplog) == 4 assert count_matching_log_lines('wrote 2 bytes to /new', caplog) == 4
def load( self, src: str, dest: IOIter, key_pair: Optional[bytes], ) -> IOIter: """ Wrapper around the _load function that converts the SHA to a path """ src = sha_to_path(src) with IOIter() as encrypted_load_file: self._load(src, encrypted_load_file) decrypt_and_unpack(encrypted_load_file, dest, key_pair, self.options) dest.fd.seek(0) return dest
def test_decrypt_and_unpack_no_encryption(caplog, mock_open_streams): orig, new, _ = mock_open_streams orig_contents = orig._fd.getvalue() cobj = zlib.compressobj() orig._fd.write(cobj.compress(orig_contents[:4])) orig._fd.write(cobj.compress(orig_contents[4:])) orig._fd.write(cobj.flush()) decrypt_and_unpack(orig, new, b'', dict(use_compression=True, use_encryption=False)) assert new._fd.getvalue() == orig_contents assert count_matching_log_lines('read 2 bytes from /orig', caplog) == 7 assert count_matching_log_lines('wrote 1 bytes to /new', caplog) == 1 assert count_matching_log_lines('wrote 2 bytes to /new', caplog) == 2 assert count_matching_log_lines('wrote 4 bytes to /new', caplog) == 1
def test_decrypt_and_unpack_bad_signature(caplog, mock_open_streams): orig, new, _ = mock_open_streams orig_contents = orig._fd.getvalue() cipher = Cipher(AES(TMP_KEY), CTR(TMP_NONCE), backend=default_backend()).encryptor() ct = cipher.update(zlib.compress(orig_contents)) hmac = HMAC(TMP_KEY, SHA256(), default_backend()) hmac.update(ct) signature = hmac.finalize() orig._fd.write(ct) with pytest.raises(BackupCorruptedError): decrypt_and_unpack( orig, new, TMP_KEYPAIR + signature[:-2] + b'f', dict(use_compression=True, use_encryption=True), )
def test_decrypt_and_unpack_no_compression(caplog, mock_open_streams): orig, new, _ = mock_open_streams orig_contents = orig._fd.getvalue() cipher = Cipher(AES(TMP_KEY), CTR(TMP_NONCE), backend=default_backend()).encryptor() ct = cipher.update(orig_contents) hmac = HMAC(TMP_KEY, SHA256(), default_backend()) hmac.update(ct) signature = hmac.finalize() orig._fd.write(ct) decrypt_and_unpack( orig, new, TMP_KEYPAIR + signature, dict(use_compression=False, use_encryption=True), ) assert new._fd.getvalue() == orig_contents assert count_matching_log_lines('read 2 bytes from /orig', caplog) == 4 assert count_matching_log_lines('wrote 2 bytes to /new', caplog) == 4