summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorenkore <public@enkore.de>2017-07-29 12:18:38 +0200
committerGitHub <noreply@github.com>2017-07-29 12:18:38 +0200
commit7d02c7e4536a205472b99690f582d6c8edfcd36f (patch)
treef02389b47425febfdfe49910e7bfd2caf7a03485
parent8d89ee981c092770f0864ada0e81aed60366315f (diff)
parentdc4abffbc04e82aa982677ed2ad949f8ccdbe072 (diff)
Merge pull request #1034 from ThomasWaldmann/crypto-aead1.2.0dev0
new crypto code, blackbox, aead internally
-rw-r--r--setup.py3
-rw-r--r--src/borg/archive.py13
-rw-r--r--src/borg/crypto/_crypto_helpers.c35
-rw-r--r--src/borg/crypto/_crypto_helpers.h15
-rw-r--r--src/borg/crypto/key.py88
-rw-r--r--src/borg/crypto/low_level.pyx812
-rw-r--r--src/borg/crypto/nonces.py26
-rw-r--r--src/borg/helpers.py2
-rw-r--r--src/borg/selftest.py2
-rw-r--r--src/borg/testsuite/archiver.py4
-rw-r--r--src/borg/testsuite/crypto.py198
-rw-r--r--src/borg/testsuite/key.py28
-rw-r--r--src/borg/testsuite/nonces.py110
13 files changed, 1039 insertions, 297 deletions
diff --git a/setup.py b/setup.py
index dd85db6a1..db47c25c7 100644
--- a/setup.py
+++ b/setup.py
@@ -52,6 +52,7 @@ from distutils.command.clean import clean
compress_source = 'src/borg/compress.pyx'
crypto_ll_source = 'src/borg/crypto/low_level.pyx'
+crypto_helpers = 'src/borg/crypto/_crypto_helpers.c'
chunker_source = 'src/borg/chunker.pyx'
hashindex_source = 'src/borg/hashindex.pyx'
item_source = 'src/borg/item.pyx'
@@ -730,7 +731,7 @@ ext_modules = []
if not on_rtd:
ext_modules += [
Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros),
- Extension('borg.crypto.low_level', [crypto_ll_source], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros),
+ Extension('borg.crypto.low_level', [crypto_ll_source, crypto_helpers], libraries=crypto_libraries, include_dirs=include_dirs, library_dirs=library_dirs, define_macros=define_macros),
Extension('borg.hashindex', [hashindex_source]),
Extension('borg.item', [item_source]),
Extension('borg.chunker', [chunker_source]),
diff --git a/src/borg/archive.py b/src/borg/archive.py
index ff8f8729e..06470eaba 100644
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -25,6 +25,7 @@ from .cache import ChunkListEntry
from .crypto.key import key_factory
from .compress import Compressor, CompressionSpec
from .constants import * # NOQA
+from .crypto.low_level import IntegrityError as IntegrityErrorBase
from .hashindex import ChunkIndex, ChunkIndexEntry, CacheSynchronizer
from .helpers import Manifest
from .helpers import hardlinkable
@@ -1148,7 +1149,7 @@ class ArchiveChecker:
else:
try:
self.manifest, _ = Manifest.load(repository, (Manifest.Operation.CHECK,), key=self.key)
- except IntegrityError as exc:
+ except IntegrityErrorBase as exc:
logger.error('Repository manifest is corrupted: %s', exc)
self.error_found = True
del self.chunks[Manifest.MANIFEST_ID]
@@ -1211,11 +1212,11 @@ class ArchiveChecker:
chunk_id = chunk_ids_revd.pop(-1) # better efficiency
try:
encrypted_data = next(chunk_data_iter)
- except (Repository.ObjectNotFound, IntegrityError) as err:
+ except (Repository.ObjectNotFound, IntegrityErrorBase) as err:
self.error_found = True
errors += 1
logger.error('chunk %s: %s', bin_to_hex(chunk_id), err)
- if isinstance(err, IntegrityError):
+ if isinstance(err, IntegrityErrorBase):
defect_chunks.append(chunk_id)
# as the exception killed our generator, make a new one for remaining chunks:
if chunk_ids_revd:
@@ -1225,7 +1226,7 @@ class ArchiveChecker:
_chunk_id = None if chunk_id == Manifest.MANIFEST_ID else chunk_id
try:
self.key.decrypt(_chunk_id, encrypted_data)
- except IntegrityError as integrity_error:
+ except IntegrityErrorBase as integrity_error:
self.error_found = True
errors += 1
logger.error('chunk %s, integrity error: %s', bin_to_hex(chunk_id), integrity_error)
@@ -1254,7 +1255,7 @@ class ArchiveChecker:
encrypted_data = self.repository.get(defect_chunk)
_chunk_id = None if defect_chunk == Manifest.MANIFEST_ID else defect_chunk
self.key.decrypt(_chunk_id, encrypted_data)
- except IntegrityError:
+ except IntegrityErrorBase:
# failed twice -> get rid of this chunk
del self.chunks[defect_chunk]
self.repository.delete(defect_chunk)
@@ -1295,7 +1296,7 @@ class ArchiveChecker:
cdata = self.repository.get(chunk_id)
try:
data = self.key.decrypt(chunk_id, cdata)
- except IntegrityError as exc:
+ except IntegrityErrorBase as exc:
logger.error('Skipping corrupted chunk: %s', exc)
self.error_found = True
continue
diff --git a/src/borg/crypto/_crypto_helpers.c b/src/borg/crypto/_crypto_helpers.c
new file mode 100644
index 000000000..0a433bb5f
--- /dev/null
+++ b/src/borg/crypto/_crypto_helpers.c
@@ -0,0 +1,35 @@
+/* some helpers, so our code also works with OpenSSL 1.0.x */
+
+#include <string.h>
+#include <openssl/opensslv.h>
+#include <openssl/hmac.h>
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+
+HMAC_CTX *HMAC_CTX_new(void)
+{
+ HMAC_CTX *ctx = OPENSSL_malloc(sizeof(*ctx));
+ if (ctx != NULL) {
+ memset(ctx, 0, sizeof *ctx);
+ HMAC_CTX_cleanup(ctx);
+ }
+ return ctx;
+}
+
+void HMAC_CTX_free(HMAC_CTX *ctx)
+{
+ if (ctx != NULL) {
+ HMAC_CTX_cleanup(ctx);
+ OPENSSL_free(ctx);
+ }
+}
+
+const EVP_CIPHER *EVP_aes_256_ocb(void){ /* dummy, so that code compiles */
+ return NULL;
+}
+
+const EVP_CIPHER *EVP_chacha20_poly1305(void){ /* dummy, so that code compiles */
+ return NULL;
+}
+
+#endif
diff --git a/src/borg/crypto/_crypto_helpers.h b/src/borg/crypto/_crypto_helpers.h
new file mode 100644
index 000000000..bb9afc418
--- /dev/null
+++ b/src/borg/crypto/_crypto_helpers.h
@@ -0,0 +1,15 @@
+/* some helpers, so our code also works with OpenSSL 1.0.x */
+
+#include <openssl/opensslv.h>
+#include <openssl/hmac.h>
+#include <openssl/evp.h>
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L
+
+HMAC_CTX *HMAC_CTX_new(void);
+void HMAC_CTX_free(HMAC_CTX *ctx);
+
+const EVP_CIPHER *EVP_aes_256_ocb(void); /* dummy, so that code compiles */
+const EVP_CIPHER *EVP_chacha20_poly1305(void); /* dummy, so that code compiles */
+
+#endif
diff --git a/src/borg/crypto/key.py b/src/borg/crypto/key.py
index 02cfed6e7..25aae6cf7 100644
--- a/src/borg/crypto/key.py
+++ b/src/borg/crypto/key.py
@@ -11,7 +11,7 @@ from hmac import HMAC, compare_digest
import msgpack
-from borg.logger import create_logger
+from ..logger import create_logger
logger = create_logger()
@@ -25,10 +25,10 @@ from ..helpers import get_limited_unpacker
from ..helpers import bin_to_hex
from ..item import Key, EncryptedKey
from ..platform import SaveFile
-from .nonces import NonceManager
-from .low_level import AES, bytes_to_long, bytes_to_int, num_aes_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512
-PREFIX = b'\0' * 8
+from .nonces import NonceManager
+from .low_level import AES, bytes_to_long, long_to_bytes, bytes_to_int, num_cipher_blocks, hmac_sha256, blake2b_256, hkdf_hmac_sha512
+from .low_level import AES256_CTR_HMAC_SHA256, AES256_CTR_BLAKE2b
class PassphraseWrong(Error):
@@ -352,48 +352,31 @@ class AESKeyBase(KeyBase):
PAYLOAD_OVERHEAD = 1 + 32 + 8 # TYPE + HMAC + NONCE
- MAC = hmac_sha256
+ CIPHERSUITE = AES256_CTR_HMAC_SHA256
logically_encrypted = True
def encrypt(self, chunk):
data = self.compressor.compress(chunk)
- self.nonce_manager.ensure_reservation(num_aes_blocks(len(data)))
- self.enc_cipher.reset()
- data = b''.join((self.enc_cipher.iv[8:], self.enc_cipher.encrypt(data)))
- assert (self.MAC is blake2b_256 and len(self.enc_hmac_key) == 128 or
- self.MAC is hmac_sha256 and len(self.enc_hmac_key) == 32)
- hmac = self.MAC(self.enc_hmac_key, data)
- return b''.join((self.TYPE_STR, hmac, data))
+ next_iv = self.nonce_manager.ensure_reservation(self.cipher.next_iv(),
+ self.cipher.block_count(len(data)))
+ return self.cipher.encrypt(data, header=self.TYPE_STR, iv=next_iv)
def decrypt(self, id, data, decompress=True):
if not (data[0] == self.TYPE or
data[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)):
id_str = bin_to_hex(id) if id is not None else '(unknown)'
raise IntegrityError('Chunk %s: Invalid encryption envelope' % id_str)
- data_view = memoryview(data)
- hmac_given = data_view[1:33]
- assert (self.MAC is blake2b_256 and len(self.enc_hmac_key) == 128 or
- self.MAC is hmac_sha256 and len(self.enc_hmac_key) == 32)
- hmac_computed = memoryview(self.MAC(self.enc_hmac_key, data_view[33:]))
- if not compare_digest(hmac_computed, hmac_given):
- id_str = bin_to_hex(id) if id is not None else '(unknown)'
- raise IntegrityError('Chunk %s: Encryption envelope checksum mismatch' % id_str)
- self.dec_cipher.reset(iv=PREFIX + data[33:41])
- payload = self.dec_cipher.decrypt(data_view[41:])
+ try:
+ payload = self.cipher.decrypt(data)
+ except IntegrityError as e:
+ raise IntegrityError("Chunk %s: Could not decrypt [%s]" % (bin_to_hex(id), str(e)))
if not decompress:
return payload
data = self.decompress(payload)
self.assert_id(id, data)
return data
- def extract_nonce(self, payload):
- if not (payload[0] == self.TYPE or
- payload[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)):
- raise IntegrityError('Manifest: Invalid encryption envelope')
- nonce = bytes_to_long(payload[33:41])
- return nonce
-
def init_from_random_data(self, data=None):
if data is None:
data = os.urandom(100)
@@ -405,10 +388,21 @@ class AESKeyBase(KeyBase):
if self.chunk_seed & 0x80000000:
self.chunk_seed = self.chunk_seed - 0xffffffff - 1
- def init_ciphers(self, manifest_nonce=0):
- self.enc_cipher = AES(is_encrypt=True, key=self.enc_key, iv=manifest_nonce.to_bytes(16, byteorder='big'))
- self.nonce_manager = NonceManager(self.repository, self.enc_cipher, manifest_nonce)
- self.dec_cipher = AES(is_encrypt=False, key=self.enc_key)
+ def init_ciphers(self, manifest_data=None):
+ self.cipher = self.CIPHERSUITE(mac_key=self.enc_hmac_key, enc_key=self.enc_key, header_len=1, aad_offset=1)
+ if manifest_data is None:
+ nonce = 0
+ else:
+ if not (manifest_data[0] == self.TYPE or
+ manifest_data[0] == PassphraseKey.TYPE and isinstance(self, RepoKey)):
+ raise IntegrityError('Manifest: Invalid encryption envelope')
+ # manifest_blocks is a safe upper bound on the amount of cipher blocks needed
+ # to encrypt the manifest. depending on the ciphersuite and overhead, it might
+ # be a bit too high, but that does not matter.
+ manifest_blocks = num_cipher_blocks(len(manifest_data))
+ nonce = self.cipher.extract_iv(manifest_data) + manifest_blocks
+ self.cipher.set_iv(nonce)
+ self.nonce_manager = NonceManager(self.repository, nonce)
class Passphrase(str):
@@ -528,8 +522,7 @@ class PassphraseKey(ID_HMAC_SHA_256, AESKeyBase):
key.init(repository, passphrase)
try:
key.decrypt(None, manifest_data)
- num_blocks = num_aes_blocks(len(manifest_data) - 41)
- key.init_ciphers(key.extract_nonce(manifest_data) + num_blocks)
+ key.init_ciphers(manifest_data)
key._passphrase = passphrase
return key
except IntegrityError:
@@ -568,8 +561,7 @@ class KeyfileKeyBase(AESKeyBase):
else:
if not key.load(target, passphrase):
raise PassphraseWrong
- num_blocks = num_aes_blocks(len(manifest_data) - 41)
- key.init_ciphers(key.extract_nonce(manifest_data) + num_blocks)
+ key.init_ciphers(manifest_data)
key._passphrase = passphrase
return key
@@ -604,7 +596,7 @@ class KeyfileKeyBase(AESKeyBase):
assert enc_key.version == 1
assert enc_key.algorithm == 'sha256'
key = passphrase.kdf(enc_key.salt, enc_key.iterations, 32)
- data = AES(is_encrypt=False, key=key).decrypt(enc_key.data)
+ data = AES(key, b'\0'*16).decrypt(enc_key.data)
if hmac_sha256(key, data) == enc_key.hash:
return data
@@ -613,7 +605,7 @@ class KeyfileKeyBase(AESKeyBase):
iterations = PBKDF2_ITERATIONS
key = passphrase.kdf(salt, iterations, 32)
hash = hmac_sha256(key, data)
- cdata = AES(is_encrypt=True, key=key).encrypt(data)
+ cdata = AES(key, b'\0'*16).encrypt(data)
enc_key = EncryptedKey(
version=1,
salt=salt,
@@ -772,7 +764,7 @@ class Blake2KeyfileKey(ID_BLAKE2b_256, KeyfileKey):
STORAGE = KeyBlobStorage.KEYFILE
FILE_ID = 'BORG_KEY'
- MAC = blake2b_256
+ CIPHERSUITE = AES256_CTR_BLAKE2b
class Blake2RepoKey(ID_BLAKE2b_256, RepoKey):
@@ -781,7 +773,7 @@ class Blake2RepoKey(ID_BLAKE2b_256, RepoKey):
ARG_NAME = 'repokey-blake2'
STORAGE = KeyBlobStorage.REPO
- MAC = blake2b_256
+ CIPHERSUITE = AES256_CTR_BLAKE2b
class AuthenticatedKeyBase(RepoKey):
@@ -799,16 +791,9 @@ class AuthenticatedKeyBase(RepoKey):
super().save(target, passphrase)
self.logically_encrypted = False
- def extract_nonce(self, payload):
- # This is called during set-up of the AES ciphers we're not actually using for this
- # key. Therefore the return value of this method doesn't matter; it's just around
- # to not have it crash should key identification be run against a very small chunk
- # by "borg check" when the manifest is lost. (The manifest is always large enough
- # to have the original method read some garbage from bytes 33-41). (Also, the return
- # value must be larger than the 41 byte bloat of the original format).
- if payload[0] != self.TYPE:
+ def init_ciphers(self, manifest_data=None):
+ if manifest_data is not None and manifest_data[0] != self.TYPE:
raise IntegrityError('Manifest: Invalid encryption envelope')
- return 42
def encrypt(self, chunk):
data = self.compressor.compress(chunk)
@@ -816,7 +801,8 @@ class AuthenticatedKeyBase(RepoKey):
def decrypt(self, id, data, decompress=True):
if data[0] != self.TYPE:
- raise IntegrityError('Chunk %s: Invalid envelope' % bin_to_hex(id))
+ id_str = bin_to_hex(id) if id is not None else '(unknown)'
+ raise IntegrityError('Chunk %s: Invalid envelope' % id_str)
payload = memoryview(data)[1:]
if not decompress:
return payload
diff --git a/src/borg/crypto/low_level.pyx b/src/borg/crypto/low_level.pyx
index a68cd820f..da06c73e2 100644
--- a/src/borg/crypto/low_level.pyx
+++ b/src/borg/crypto/low_level.pyx
@@ -1,15 +1,51 @@
-"""A thin OpenSSL wrapper"""
+"""An AEAD style OpenSSL wrapper
+
+API:
+
+ encrypt(data, header=b'', aad_offset=0) -> envelope
+ decrypt(envelope, header_len=0, aad_offset=0) -> data
+
+Envelope layout:
+
+|<--------------------------- envelope ------------------------------------------>|
+|<------------ header ----------->|<---------- ciphersuite specific ------------->|
+|<-- not auth data -->|<-- aad -->|<-- e.g.: S(aad, iv, E(data)), iv, E(data) -->|
+
+|--- #aad_offset ---->|
+|------------- #header_len ------>|
+
+S means a cryptographic signature function (like HMAC or GMAC).
+E means a encryption function (like AES).
+iv is the initialization vector / nonce, if needed.
+
+The split of header into not authenticated data and aad (additional authenticated
+data) is done to support the legacy envelope layout as used in attic and early borg
+(where the TYPE byte was not authenticated) and avoid unneeded memcpy and string
+garbage.
+
+Newly designed envelope layouts can just authenticate the whole header.
+
+IV handling:
+
+ iv = ... # just never repeat!
+ cs = CS(hmac_key, enc_key, iv=iv)
+ envelope = cs.encrypt(data, header, aad_offset)
+ iv = cs.next_iv(len(data))
+ (repeat)
+"""
import hashlib
import hmac
from math import ceil
-from libc.stdlib cimport malloc, free
+from cpython cimport PyMem_Malloc, PyMem_Free
from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release
from cpython.bytes cimport PyBytes_FromStringAndSize
API_VERSION = '1.1_02'
+cdef extern from "openssl/crypto.h":
+ int CRYPTO_memcmp(const void *a, const void *b, size_t len)
cdef extern from "../algorithms/blake2-libselect.h":
ctypedef struct blake2b_state:
@@ -29,9 +65,14 @@ cdef extern from "openssl/evp.h":
pass
ctypedef struct ENGINE:
pass
+
const EVP_CIPHER *EVP_aes_256_ctr()
- EVP_CIPHER_CTX *EVP_CIPHER_CTX_new()
- void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *a)
+ const EVP_CIPHER *EVP_aes_256_gcm()
+ const EVP_CIPHER *EVP_aes_256_ocb()
+ const EVP_CIPHER *EVP_chacha20_poly1305()
+
+ void EVP_CIPHER_CTX_init(EVP_CIPHER_CTX *a)
+ void EVP_CIPHER_CTX_cleanup(EVP_CIPHER_CTX *a)
int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, ENGINE *impl,
const unsigned char *key, const unsigned char *iv)
@@ -44,58 +85,83 @@ cdef extern from "openssl/evp.h":
int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl)
int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl)
- EVP_MD *EVP_sha256() nogil
+ int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type, int arg, void *ptr)
+ int EVP_CTRL_GCM_GET_TAG
+ int EVP_CTRL_GCM_SET_TAG
+ int EVP_CTRL_GCM_SET_IVLEN
+
+ const EVP_MD *EVP_sha256() nogil
+ EVP_CIPHER_CTX *EVP_CIPHER_CTX_new()
+ void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *a)
cdef extern from "openssl/hmac.h":
+ ctypedef struct HMAC_CTX:
+ pass
+
+ void HMAC_CTX_init(HMAC_CTX *ctx)
+ void HMAC_CTX_cleanup(HMAC_CTX *ctx)
+
+ HMAC_CTX *HMAC_CTX_new()
+ void HMAC_CTX_free(HMAC_CTX *a)
+
+ int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int key_len, const EVP_MD *md, ENGINE *impl)
+ int HMAC_Update(HMAC_CTX *ctx, const unsigned char *data, int len)
+ int HMAC_Final(HMAC_CTX *ctx, unsigned char *md, unsigned int *len)
+
unsigned char *HMAC(const EVP_MD *evp_md,
const void *key, int key_len,
const unsigned char *data, int data_len,
unsigned char *md, unsigned int *md_len) nogil
+cdef extern from "_crypto_helpers.h":
+ long OPENSSL_VERSION_NUMBER
+
+ ctypedef struct HMAC_CTX:
+ pass
+
+ HMAC_CTX *HMAC_CTX_new()
+ void HMAC_CTX_free(HMAC_CTX *a)
+
+ const EVP_CIPHER *EVP_aes_256_ocb() # dummy
+ const EVP_CIPHER *EVP_chacha20_poly1305() # dummy
+
+
+openssl10 = OPENSSL_VERSION_NUMBER < 0x10100000
+
+
import struct
_int = struct.Struct('>I')
_long = struct.Struct('>Q')
-_2long = struct.Struct('>QQ')
bytes_to_int = lambda x, offset=0: _int.unpack_from(x, offset)[0]
bytes_to_long = lambda x, offset=0: _long.unpack_from(x, offset)[0]
long_to_bytes = lambda x: _long.pack(x)
-def bytes16_to_int(b, offset=0):
- h, l = _2long.unpack_from(b, offset)
- return (h << 64) + l
+def num_cipher_blocks(length, blocksize=16):
+ """Return the number of cipher blocks required to encrypt/decrypt <length> bytes of data.
+ For a precise computation, <blocksize> must be the used cipher's block size (AES: 16, CHACHA20: 64).
-def int_to_bytes16(i):
- max_uint64 = 0xffffffffffffffff
- l = i & max_uint64
- h = (i >> 64) & max_uint64
- return _2long.pack(h, l)
+ For a safe-upper-boundary computation, <blocksize> must be the MINIMUM of the block sizes (in
+ bytes) of ALL supported ciphers. This can be used to adjust a counter if the used cipher is not
+ known (yet).
+ The default value of blocksize must be adjusted so it reflects this minimum, so a call of this
+ function without a blocksize is "safe-upper-boundary by default".
-
-def increment_iv(iv, amount=1):
+ Padding cipher modes are not supported.
"""
- Increment the IV by the given amount (default 1).
+ return (length + blocksize - 1) // blocksize
- :param iv: input IV, 16 bytes (128 bit)
- :param amount: increment value
- :return: input_IV + amount, 16 bytes (128 bit)
- """
- assert len(iv) == 16
- iv = bytes16_to_int(iv)
- iv += amount
- iv = int_to_bytes16(iv)
- return iv
+class CryptoError(Exception):
+ """Malfunction in the crypto module."""
-def num_aes_blocks(int length):
- """Return the number of AES blocks required to encrypt/decrypt *length* bytes of data.
- Note: this is only correct for modes without padding, like AES-CTR.
- """
- return (length + 15) // 16
+
+class IntegrityError(CryptoError):
+ """Integrity checks failed. Corrupted or tampered data."""
cdef Py_buffer ro_buffer(object data) except *:
@@ -104,101 +170,641 @@ cdef Py_buffer ro_buffer(object data) except *:
return view
-cdef class AES:
- """A thin wrapper around the OpenSSL EVP cipher API
- """
+class UNENCRYPTED:
+ # Layout: HEADER + PlainText
+
+ def __init__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):
+ assert mac_key is None
+ assert enc_key is None
+ self.header_len = header_len
+ self.set_iv(iv)
+
+ def encrypt(self, data, header=b'', iv=None):
+ """
+ IMPORTANT: it is called encrypt to satisfy the crypto api naming convention,
+ but this does NOT encrypt and it does NOT compute and store a MAC either.
+ """
+ if iv is not None:
+ self.set_iv(iv)
+ assert self.iv is not None, 'iv needs to be set before encrypt is called'
+ return header + data
+
+ def decrypt(self, envelope):
+ """
+ IMPORTANT: it is called decrypt to satisfy the crypto api naming convention,
+ but this does NOT decrypt and it does NOT verify a MAC either, because data
+ is not encrypted and there is no MAC.
+ """
+ return memoryview(envelope)[self.header_len:]
+
+ def block_count(self, length):
+ return 0
+
+ def set_iv(self, iv):
+ self.iv = iv
+
+ def next_iv(self):
+ return self.iv
+
+ def extract_iv(self, envelope):
+ return 0
+
+
+cdef class AES256_CTR_BASE:
+ # Layout: HEADER + MAC 32 + IV 8 + CT (same as attic / borg < 1.2 IF HEADER = TYPE_BYTE, no AAD)
+
cdef EVP_CIPHER_CTX *ctx
- cdef int is_encrypt
- cdef unsigned char iv_orig[16]
+ cdef unsigned char *enc_key
+ cdef int cipher_blk_len
+ cdef int iv_len, iv_len_short
+ cdef int aad_offset
+ cdef int header_len
+ cdef int mac_len
+ cdef unsigned char iv[16]
cdef long long blocks
- def __cinit__(self, is_encrypt, key, iv=None):
+ @staticmethod
+ def requirements_check():
+ if OPENSSL_VERSION_NUMBER < 0x10000000:
+ raise ValueError('AES CTR requires OpenSSL >= 1.0.0. Detected: OpenSSL %08x' % OPENSSL_VERSION_NUMBER)
+
+ def __init__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):
+ self.requirements_check()
+ assert isinstance(enc_key, bytes) and len(enc_key) == 32
+ self.cipher_blk_len = 16
+ self.iv_len = sizeof(self.iv)
+ self.iv_len_short = 8
+ assert aad_offset <= header_len
+ self.aad_offset = aad_offset
+ self.header_len = header_len
+ self.mac_len = 32
+ self.enc_key = enc_key
+ if iv is not None:
+ self.set_iv(iv)
+ else:
+ self.blocks = -1 # make sure set_iv is called before encrypt
+
+ def __cinit__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):
self.ctx = EVP_CIPHER_CTX_new()
- self.is_encrypt = is_encrypt
- # Set cipher type and mode
- cipher_mode = EVP_aes_256_ctr()
- if self.is_encrypt:
- if not EVP_EncryptInit_ex(self.ctx, cipher_mode, NULL, NULL, NULL):
- raise Exception('EVP_EncryptInit_ex failed')
- else: # decrypt
- if not EVP_DecryptInit_ex(self.ctx, cipher_mode, NULL, NULL, NULL):
- raise Exception('EVP_DecryptInit_ex failed')
- self.reset(key, iv)
def __dealloc__(self):
EVP_CIPHER_CTX_free(self.ctx)
- def reset(self, key=None, iv=None):
- cdef const unsigned char *key2 = NULL
- cdef const unsigned char *iv2 = NULL
- if key:
- key2 = key
- if iv:
- iv2 = iv
- assert isinstance(iv, bytes) and len(iv) == 16
- for i in range(16):
- self.iv_orig[i] = iv[i]
- self.blocks = 0 # number of AES blocks encrypted starting with iv_orig
- # Initialise key and IV
- if self.is_encrypt:
- if not EVP_EncryptInit_ex(self.ctx, NULL, NULL, key2, iv2):
- raise Exception('EVP_EncryptInit_ex failed')
- else: # decrypt
- if not EVP_DecryptInit_ex(self.ctx, NULL, NULL, key2, iv2):
- raise Exception('EVP_DecryptInit_ex failed')
+ cdef mac_compute(self, const unsigned char *data1, int data1_len,
+ const unsigned char *data2, int data2_len,
+ const unsigned char *mac_buf):
+ raise NotImplementedError
+
+ cdef mac_verify(self, const unsigned char *data1, int data1_len,
+ const unsigned char *data2, int data2_len,
+ const unsigned char *mac_buf, const unsigned char *mac_wanted):
+ raise NotImplementedError
+
+ def encrypt(self, data, header=b'', iv=None):
+ """
+ encrypt data, compute mac over aad + iv + cdata, prepend header.
+ aad_offset is the offset into the header where aad starts.
+ """
+ if iv is not None:
+ self.set_iv(iv)
+ assert self.blocks == 0, 'iv needs to be set before encrypt is called'
+ cdef int ilen = len(data)
+ cdef int hlen = len(header)
+ assert hlen == self.header_len
+ cdef int aoffset = self.aad_offset
+ cdef int alen = hlen - aoffset
+ cdef unsigned char *odata = <unsigned char *>PyMem_Malloc(hlen + self.mac_len + self.iv_len_short +
+ ilen + self.cipher_blk_len) # play safe, 1 extra blk
+ if not odata:
+ raise MemoryError
+ cdef int olen
+ cdef int offset
+ cdef Py_buffer idata = ro_buffer(data)
+ cdef Py_buffer hdata = ro_buffer(header)
+ try:
+ offset = 0
+ for i in range(hlen):
+ odata[offset+i] = header[i]
+ offset += hlen
+ offset += self.mac_len
+ self.store_iv(odata+offset, self.iv)
+ offset += self.iv_len_short
+ rc = EVP_EncryptInit_ex(self.ctx, EVP_aes_256_ctr(), NULL, self.enc_key, self.iv)
+ if not rc:
+ raise CryptoError('EVP_EncryptInit_ex failed')
+ rc = EVP_EncryptUpdate(self.ctx, odata+offset, &olen, <const unsigned char*> idata.buf, ilen)
+ if not rc:
+ raise CryptoError('EVP_EncryptUpdate failed')
+ offset += olen
+ rc = EVP_EncryptFinal_ex(self.ctx, odata+offset, &olen)
+ if not rc:
+ raise CryptoError('EVP_EncryptFinal_ex failed')
+ offset += olen
+ self.mac_compute(<const unsigned char *> hdata.buf+aoffset, alen,
+ odata+hlen+self.mac_len, offset-hlen-self.mac_len,
+ odata+hlen)
+ self.blocks += self.block_count(ilen)
+ return odata[:offset]
+ finally:
+ PyMem_Free(odata)
+ PyBuffer_Release(&hdata)
+ PyBuffer_Release(&idata)
+
+ def decrypt(self, envelope):
+ """
+ authenticate aad + iv + cdata, decrypt cdata, ignore header bytes up to aad_offset.
+ """
+ cdef int ilen = len(envelope)
+ cdef int hlen = self.header_len
+ assert hlen == self.header_len
+ cdef int aoffset = self.aad_offset
+ cdef int alen = hlen - aoffset
+ cdef unsigned char *odata = <unsigned char *>PyMem_Malloc(ilen + self.cipher_blk_len) # play safe, 1 extra blk
+ if not odata:
+ raise MemoryError
+ cdef int olen
+ cdef int offset
+ cdef unsigned char mac_buf[32]
+ assert sizeof(mac_buf) == self.mac_len
+ cdef Py_buffer idata = ro_buffer(envelope)
+ try:
+ self.mac_verify(<const unsigned char *> idata.buf+aoffset, alen,
+ <const unsigned char *> idata.buf+hlen+self.mac_len, ilen-hlen-self.mac_len,
+ mac_buf, <const unsigned char *> idata.buf+hlen)
+ iv = self.fetch_iv(<unsigned char *> idata.buf+hlen+self.mac_len)
+ self.set_iv(iv)
+ if not EVP_DecryptInit_ex(self.ctx, EVP_aes_256_ctr(), NULL, self.enc_key, iv):
+ raise CryptoError('EVP_DecryptInit_ex failed')
+ offset = 0
+ rc = EVP_DecryptUpdate(self.ctx, odata+offset, &olen,
+ <const unsigned char*> idata.buf+hlen+self.mac_len+self.iv_len_short,
+ ilen-hlen-self.mac_len-self.iv_len_short)
+ if not rc:
+ raise CryptoError('EVP_DecryptUpdate failed')
+ offset += olen
+ rc = EVP_DecryptFinal_ex(self.ctx, odata+offset, &olen)
+ if rc <= 0:
+ raise CryptoError('EVP_DecryptFinal_ex failed')
+ offset += olen
+ self.blocks += self.block_count(offset)
+ return odata[:offset]
+ finally:
+ PyMem_Free(odata)
+ PyBuffer_Release(&idata)
+
+ def block_count(self, length):
+ return num_cipher_blocks(length, self.cipher_blk_len)
+
+ def set_iv(self, iv):
+ # set_iv needs to be called before each encrypt() call
+ if isinstance(iv, int):
+ iv = iv.to_bytes(self.iv_len, byteorder='big')
+ assert isinstance(iv, bytes) and len(iv) == self.iv_len
+ for i in range(self.iv_len):
+ self.iv[i] = iv[i]
+ self.blocks = 0 # how many AES blocks got encrypted with this IV?
+
+ def next_iv(self):
+ # call this after encrypt() to get the next iv (int) for the next encrypt() call
+ iv = int.from_bytes(self.iv[:self.iv_len], byteorder='big')
+ return iv + self.blocks
- @property
- def iv(self):
- return increment_iv(self.iv_orig[:16], self.blocks)
-
- def encrypt(self, data):
- cdef Py_buffer data_buf = ro_buffer(data)
- cdef int inl = len(data)
- cdef int ctl = 0
- cdef int outl = 0
- # note: modes that use padding, need up to one extra AES block (16b)
- cdef unsigned char *out = <unsigned char *>malloc(inl+16)
- if not out:
+ cdef fetch_iv(self, unsigned char * iv_in):
+ # fetch lower self.iv_len_short bytes of iv and add upper zero bytes
+ return b'\0' * (self.iv_len - self.iv_len_short) + iv_in[0:self.iv_len_short]
+
+ cdef store_iv(self, unsigned char * iv_out, unsigned char * iv):
+ # store only lower self.iv_len_short bytes, upper bytes are assumed to be 0
+ cdef int i
+ for i in range(self.iv_len_short):
+ iv_out[i] = iv[(self.iv_len-self.iv_len_short)+i]
+
+ def extract_iv(self, envelope):
+ offset = self.header_len + self.mac_len
+ return bytes_to_long(envelope[offset:offset+self.iv_len_short])
+
+
+cdef class AES256_CTR_HMAC_SHA256(AES256_CTR_BASE):
+ cdef HMAC_CTX *hmac_ctx
+ cdef unsigned char *mac_key
+
+ def __init__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):
+ assert isinstance(mac_key, bytes) and len(mac_key) == 32
+ self.mac_key = mac_key
+ super().__init__(mac_key, enc_key, iv=iv, header_len=header_len, aad_offset=aad_offset)
+
+ def __cinit__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):
+ self.hmac_ctx = HMAC_CTX_new()
+
+ def __dealloc__(self):
+ HMAC_CTX_free(self.hmac_ctx)
+
+ cdef mac_compute(self, const unsigned char *data1, int data1_len,
+ const unsigned char *data2, int data2_len,
+ const unsigned char *mac_buf):
+ if not HMAC_Init_ex(self.hmac_ctx, self.mac_key, self.mac_len, EVP_sha256(), NULL):
+ raise CryptoError('HMAC_Init_ex failed')
+ if not HMAC_Update(self.hmac_ctx, data1, data1_len):
+ raise CryptoError('HMAC_Update failed')
+ if not HMAC_Update(self.hmac_ctx, data2, data2_len):
+ raise CryptoError('HMAC_Update failed')
+ if not HMAC_Final(self.hmac_ctx, mac_buf, NULL):
+ raise CryptoError('HMAC_Final failed')
+
+ cdef mac_verify(self, const unsigned char *data1, int data1_len,
+ const unsigned char *data2, int data2_len,
+ const unsigned char *mac_buf, const unsigned char *mac_wanted):
+ self.mac_compute(data1, data1_len, data2, data2_len, mac_buf)
+ if CRYPTO_memcmp(mac_buf, mac_wanted, self.mac_len):
+ raise IntegrityError('MAC Authentication failed')
+
+
+cdef class AES256_CTR_BLAKE2b(AES256_CTR_BASE):
+ cdef unsigned char *mac_key
+
+ def __init__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):
+ assert isinstance(mac_key, bytes) and len(mac_key) == 128
+ self.mac_key = mac_key
+ super().__init__(mac_key, enc_key, iv=iv, header_len=header_len, aad_offset=aad_offset)
+
+ def __cinit__(self, mac_key, enc_key, iv=None, header_len=1, aad_offset=1):
+ pass
+
+ def __dealloc__(self):
+ pass
+
+ cdef mac_compute(self, const unsigned char *data1, int data1_len,
+ const unsigned char *data2, int data2_len,
+ const unsigned char *mac_buf):
+ cdef blake2b_state state
+ cdef int rc
+ rc = blake2b_init(&state, self.mac_len)
+ if rc == -1:
+ raise Exception('blake2b_init() failed')
+ with nogil:
+ rc = blake2b_update(&state, self.mac_key, 128)
+