crypto: layout updates, low-level does not deal with IV

encrypt used to "patch" the IV into the header,
decrypt used to fetch it from there.

encrypt now takes the header just "as is" and
also decrypt expects that the IV is already set.
This commit is contained in:
Thomas Waldmann 2022-03-18 19:00:58 +01:00
parent 0f6f278b0f
commit 5c66fa4caa
3 changed files with 48 additions and 40 deletions

View File

@ -713,28 +713,43 @@ class AEADKeyBase(KeyBase):
""" """
Chunks are encrypted and authenticated using some AEAD ciphersuite Chunks are encrypted and authenticated using some AEAD ciphersuite
Payload layout: TYPE(1) + SESSIONID(24) + NONCE(12) + MAC(16) + CIPHERTEXT Layout: suite:4 keytype:4 reserved:8 messageIV:48 sessionID:192 auth_tag:128 payload:... [bits]
^------------- AAD ---------------^ ^-------------------- AAD ----------------------------^
Offsets:0 1 2 8 32 48 [bytes]
suite: 1010b for new AEAD crypto, 0000b is old crypto
keytype: see constants.KeyType (suite+keytype)
reserved: all-zero, for future use
messageIV: a counter starting from 0 for all new encrypted messages of one session
sessionID: 192bit random, computed once per session (the session key is derived from this)
auth_tag: authentication tag output of the AEAD cipher (computed over payload and AAD)
payload: encrypted chunk data
""" """
PAYLOAD_OVERHEAD = 1 + 24 + 12 + 16 # TYPE + SESSIONID + NONCE + MAC PAYLOAD_OVERHEAD = 1 + 1 + 6 + 24 + 16 # [bytes], see Layout
CIPHERSUITE = None # override in subclass CIPHERSUITE = None # override in subclass
logically_encrypted = True logically_encrypted = True
def encrypt(self, chunk): def encrypt(self, chunk):
# to encrypt new data in this session we use always self.cipher and self.sessionid
data = self.compressor.compress(chunk) data = self.compressor.compress(chunk)
header = self.TYPE_STR + self.sessionid reserved = b'\0'
iv = self.cipher.next_iv() iv = self.cipher.next_iv()
iv_48bit = iv.to_bytes(6, 'big')
header = self.TYPE_STR + reserved + iv_48bit + self.sessionid
return self.cipher.encrypt(data, header=header, iv=iv) return self.cipher.encrypt(data, header=header, iv=iv)
def decrypt(self, id, data, decompress=True): def decrypt(self, id, data, decompress=True):
# to decrypt existing data, we need to get a cipher configured for the sessionid and iv from header
self.assert_type(data[0], id) self.assert_type(data[0], id)
sessionid = data[1:13] # XXX iv_48bit = data[2:8]
self.init_ciphers(salt=salt, context=context, iv=iv) # XXX sessionid = data[8:32]
iv = int.from_bytes(iv_48bit, 'big')
cipher = self._get_cipher(sessionid, iv)
try: try:
payload = self.cipher.decrypt(data) payload = cipher.decrypt(data)
except IntegrityError as e: except IntegrityError as e:
raise IntegrityError(f"Chunk {bin_to_hex(id)}: Could not decrypt [{str(e)}]") raise IntegrityError(f"Chunk {bin_to_hex(id)}: Could not decrypt [{str(e)}]")
if not decompress: if not decompress:
@ -753,15 +768,26 @@ class AEADKeyBase(KeyBase):
if self.chunk_seed & 0x80000000: if self.chunk_seed & 0x80000000:
self.chunk_seed = self.chunk_seed - 0xffffffff - 1 self.chunk_seed = self.chunk_seed - 0xffffffff - 1
def init_ciphers(self, salt=b'', context=b'', iv=0): def _get_session_key(self, sessionid):
assert len(sessionid) == 24 # 192bit
key = hkdf_hmac_sha512( key = hkdf_hmac_sha512(
ikm=self.enc_key + self.enc_hmac_key, ikm=self.enc_key + self.enc_hmac_key,
salt=salt, salt=sessionid,
info=b'borg-crypto-' + context, # XXX info=b'borg-session-key-' + self.CIPHERSUITE.__name__.encode(),
output_length=32 output_length=32
) )
self.cipher = self.CIPHERSUITE(key=key, header_len=1+24, aad_offset=0) # XXX return key
self.cipher.set_iv(iv)
def _get_cipher(self, sessionid, iv):
assert isinstance(iv, int)
key = self._get_session_key(sessionid)
cipher = self.CIPHERSUITE(key=key, iv=iv, header_len=1+1+6+24, aad_offset=0)
return cipher
def init_ciphers(self, manifest_data=None, iv=0):
# in every new session we start with a fresh sessionid and at iv == 0, manifest_data and iv params are ignored
self.sessionid = os.urandom(24)
self.cipher = self._get_cipher(self.sessionid, iv=0)
class AESOCBKeyfileKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey): class AESOCBKeyfileKey(ID_HMAC_SHA_256, AEADKeyBase, FlexiKey):

View File

@ -424,7 +424,7 @@ ctypedef const EVP_CIPHER * (* CIPHER)()
cdef class _AEAD_BASE: cdef class _AEAD_BASE:
# new crypto used in borg >= 1.3 # new crypto used in borg >= 1.3
# Layout: HEADER + MAC 16 + CT (IV will be put into the header, at the end) # Layout: HEADER + MAC 16 + CT
cdef CIPHER cipher cdef CIPHER cipher
cdef EVP_CIPHER_CTX *ctx cdef EVP_CIPHER_CTX *ctx
@ -432,7 +432,7 @@ cdef class _AEAD_BASE:
cdef int cipher_blk_len cdef int cipher_blk_len
cdef int iv_len cdef int iv_len
cdef int aad_offset cdef int aad_offset
cdef int header_len_expected # includes the IV at the end cdef int header_len_expected
cdef int mac_len cdef int mac_len
cdef unsigned char iv[12] cdef unsigned char iv[12]
cdef long long blocks cdef long long blocks
@ -448,12 +448,12 @@ cdef class _AEAD_BASE:
:param key: 256bit encrypt-then-mac key :param key: 256bit encrypt-then-mac key
:param iv: 96bit initialisation vector / nonce :param iv: 96bit initialisation vector / nonce
:param header_len: expected length of header *without* IV :param header_len: expected length of header
:param aad_offset: where in the header the authenticated data starts :param aad_offset: where in the header the authenticated data starts
""" """
assert isinstance(key, bytes) and len(key) == 32 assert isinstance(key, bytes) and len(key) == 32
self.iv_len = sizeof(self.iv) self.iv_len = sizeof(self.iv)
self.header_len_expected = header_len + self.iv_len self.header_len_expected = header_len
assert aad_offset <= header_len assert aad_offset <= header_len
self.aad_offset = aad_offset self.aad_offset = aad_offset
self.mac_len = 16 self.mac_len = 16
@ -483,8 +483,7 @@ cdef class _AEAD_BASE:
if block_count > 2**32: if block_count > 2**32:
raise ValueError('too much data, would overflow internal 32bit counter') raise ValueError('too much data, would overflow internal 32bit counter')
cdef int ilen = len(data) cdef int ilen = len(data)
cdef int hl = len(header) cdef int hlen = len(header)
cdef int hlen = hl + self.iv_len
assert hlen == self.header_len_expected assert hlen == self.header_len_expected
cdef int aoffset = self.aad_offset cdef int aoffset = self.aad_offset
cdef int alen = hlen - aoffset cdef int alen = hlen - aoffset
@ -498,11 +497,9 @@ cdef class _AEAD_BASE:
cdef Py_buffer hdata = ro_buffer(header) cdef Py_buffer hdata = ro_buffer(header)
try: try:
offset = 0 offset = 0
for i in range(hl): for i in range(hlen):
odata[offset+i] = header[i] odata[offset+i] = header[i]
offset = hl offset += hlen
self.store_iv(odata+offset, self.iv)
offset = hlen
offset += self.mac_len offset += self.mac_len
rc = EVP_EncryptInit_ex(self.ctx, self.cipher(), NULL, NULL, NULL) rc = EVP_EncryptInit_ex(self.ctx, self.cipher(), NULL, NULL, NULL)
if not rc: if not rc:
@ -543,7 +540,6 @@ cdef class _AEAD_BASE:
raise ValueError('too much data, would overflow internal 32bit counter') raise ValueError('too much data, would overflow internal 32bit counter')
cdef int ilen = len(envelope) cdef int ilen = len(envelope)
cdef int hlen = self.header_len_expected cdef int hlen = self.header_len_expected
cdef int hl = hlen - self.iv_len
cdef int aoffset = self.aad_offset cdef int aoffset = self.aad_offset
cdef int alen = hlen - aoffset cdef int alen = hlen - aoffset
cdef unsigned char *odata = <unsigned char *>PyMem_Malloc(ilen + self.cipher_blk_len) cdef unsigned char *odata = <unsigned char *>PyMem_Malloc(ilen + self.cipher_blk_len)
@ -555,11 +551,9 @@ cdef class _AEAD_BASE:
try: try:
if not EVP_DecryptInit_ex(self.ctx, self.cipher(), NULL, NULL, NULL): if not EVP_DecryptInit_ex(self.ctx, self.cipher(), NULL, NULL, NULL):
raise CryptoError('EVP_DecryptInit_ex failed') raise CryptoError('EVP_DecryptInit_ex failed')
iv = self.fetch_iv(<unsigned char *> idata.buf+hl)
self.set_iv(iv)
if not EVP_CIPHER_CTX_ctrl(self.ctx, EVP_CTRL_AEAD_SET_IVLEN, self.iv_len, NULL): if not EVP_CIPHER_CTX_ctrl(self.ctx, EVP_CTRL_AEAD_SET_IVLEN, self.iv_len, NULL):
raise CryptoError('EVP_CIPHER_CTX_ctrl SET IVLEN failed') raise CryptoError('EVP_CIPHER_CTX_ctrl SET IVLEN failed')
if not EVP_DecryptInit_ex(self.ctx, NULL, NULL, self.key, iv): if not EVP_DecryptInit_ex(self.ctx, NULL, NULL, self.key, self.iv):
raise CryptoError('EVP_DecryptInit_ex failed') raise CryptoError('EVP_DecryptInit_ex failed')
if not EVP_CIPHER_CTX_ctrl(self.ctx, EVP_CTRL_AEAD_SET_TAG, self.mac_len, <unsigned char *> idata.buf + hlen): if not EVP_CIPHER_CTX_ctrl(self.ctx, EVP_CTRL_AEAD_SET_TAG, self.mac_len, <unsigned char *> idata.buf + hlen):
raise CryptoError('EVP_CIPHER_CTX_ctrl SET TAG failed') raise CryptoError('EVP_CIPHER_CTX_ctrl SET TAG failed')
@ -604,18 +598,6 @@ cdef class _AEAD_BASE:
iv = int.from_bytes(self.iv[:self.iv_len], byteorder='big') iv = int.from_bytes(self.iv[:self.iv_len], byteorder='big')
return iv + 1 return iv + 1
cdef fetch_iv(self, unsigned char * iv_in):
return iv_in[0:self.iv_len]
cdef store_iv(self, unsigned char * iv_out, unsigned char * iv):
cdef int i
for i in range(self.iv_len):
iv_out[i] = iv[i]
def extract_iv(self, envelope):
offset = self.header_len_expected - self.iv_len
return bytes_to_long(envelope[offset:offset+self.iv_len])
cdef class _AES_BASE(_AEAD_BASE): cdef class _AES_BASE(_AEAD_BASE):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -94,7 +94,7 @@ class CryptoTestCase(BaseTestCase):
key = b'X' * 32 key = b'X' * 32
iv = 0 iv = 0
data = b'foo' * 10 data = b'foo' * 10
header = b'\x23' header = b'\x23' + iv.to_bytes(12, 'big')
tests = [ tests = [
# (ciphersuite class, exp_mac, exp_cdata) # (ciphersuite class, exp_mac, exp_cdata)
] ]
@ -137,7 +137,7 @@ class CryptoTestCase(BaseTestCase):
key = b'X' * 32 key = b'X' * 32
iv = 0 iv = 0
data = b'foo' * 10 data = b'foo' * 10
header = b'\x12\x34\x56' header = b'\x12\x34\x56' + iv.to_bytes(12, 'big')
tests = [ tests = [
# (ciphersuite class, exp_mac, exp_cdata) # (ciphersuite class, exp_mac, exp_cdata)
] ]