mirror of
https://github.com/borgbackup/borg.git
synced 2024-12-25 17:27:31 +00:00
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:
parent
0f6f278b0f
commit
5c66fa4caa
3 changed files with 48 additions and 40 deletions
|
@ -713,28 +713,43 @@ class AEADKeyBase(KeyBase):
|
|||
"""
|
||||
Chunks are encrypted and authenticated using some AEAD ciphersuite
|
||||
|
||||
Payload layout: TYPE(1) + SESSIONID(24) + NONCE(12) + MAC(16) + CIPHERTEXT
|
||||
^------------- AAD ---------------^
|
||||
Layout: suite:4 keytype:4 reserved:8 messageIV:48 sessionID:192 auth_tag:128 payload:... [bits]
|
||||
^-------------------- 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
|
||||
|
||||
logically_encrypted = True
|
||||
|
||||
def encrypt(self, chunk):
|
||||
# to encrypt new data in this session we use always self.cipher and self.sessionid
|
||||
data = self.compressor.compress(chunk)
|
||||
header = self.TYPE_STR + self.sessionid
|
||||
reserved = b'\0'
|
||||
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)
|
||||
|
||||
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)
|
||||
sessionid = data[1:13] # XXX
|
||||
self.init_ciphers(salt=salt, context=context, iv=iv) # XXX
|
||||
iv_48bit = data[2:8]
|
||||
sessionid = data[8:32]
|
||||
iv = int.from_bytes(iv_48bit, 'big')
|
||||
cipher = self._get_cipher(sessionid, iv)
|
||||
try:
|
||||
payload = self.cipher.decrypt(data)
|
||||
payload = cipher.decrypt(data)
|
||||
except IntegrityError as e:
|
||||
raise IntegrityError(f"Chunk {bin_to_hex(id)}: Could not decrypt [{str(e)}]")
|
||||
if not decompress:
|
||||
|
@ -753,15 +768,26 @@ def init_from_random_data(self):
|
|||
if self.chunk_seed & 0x80000000:
|
||||
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(
|
||||
ikm=self.enc_key + self.enc_hmac_key,
|
||||
salt=salt,
|
||||
info=b'borg-crypto-' + context, # XXX
|
||||
salt=sessionid,
|
||||
info=b'borg-session-key-' + self.CIPHERSUITE.__name__.encode(),
|
||||
output_length=32
|
||||
)
|
||||
self.cipher = self.CIPHERSUITE(key=key, header_len=1+24, aad_offset=0) # XXX
|
||||
self.cipher.set_iv(iv)
|
||||
return key
|
||||
|
||||
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):
|
||||
|
|
|
@ -424,7 +424,7 @@ ctypedef const EVP_CIPHER * (* CIPHER)()
|
|||
|
||||
cdef class _AEAD_BASE:
|
||||
# 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 EVP_CIPHER_CTX *ctx
|
||||
|
@ -432,7 +432,7 @@ cdef class _AEAD_BASE:
|
|||
cdef int cipher_blk_len
|
||||
cdef int iv_len
|
||||
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 unsigned char iv[12]
|
||||
cdef long long blocks
|
||||
|
@ -448,12 +448,12 @@ cdef class _AEAD_BASE:
|
|||
|
||||
:param key: 256bit encrypt-then-mac key
|
||||
: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
|
||||
"""
|
||||
assert isinstance(key, bytes) and len(key) == 32
|
||||
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
|
||||
self.aad_offset = aad_offset
|
||||
self.mac_len = 16
|
||||
|
@ -483,8 +483,7 @@ cdef class _AEAD_BASE:
|
|||
if block_count > 2**32:
|
||||
raise ValueError('too much data, would overflow internal 32bit counter')
|
||||
cdef int ilen = len(data)
|
||||
cdef int hl = len(header)
|
||||
cdef int hlen = hl + self.iv_len
|
||||
cdef int hlen = len(header)
|
||||
assert hlen == self.header_len_expected
|
||||
cdef int aoffset = self.aad_offset
|
||||
cdef int alen = hlen - aoffset
|
||||
|
@ -498,11 +497,9 @@ cdef class _AEAD_BASE:
|
|||
cdef Py_buffer hdata = ro_buffer(header)
|
||||
try:
|
||||
offset = 0
|
||||
for i in range(hl):
|
||||
for i in range(hlen):
|
||||
odata[offset+i] = header[i]
|
||||
offset = hl
|
||||
self.store_iv(odata+offset, self.iv)
|
||||
offset = hlen
|
||||
offset += hlen
|
||||
offset += self.mac_len
|
||||
rc = EVP_EncryptInit_ex(self.ctx, self.cipher(), NULL, NULL, NULL)
|
||||
if not rc:
|
||||
|
@ -543,7 +540,6 @@ cdef class _AEAD_BASE:
|
|||
raise ValueError('too much data, would overflow internal 32bit counter')
|
||||
cdef int ilen = len(envelope)
|
||||
cdef int hlen = self.header_len_expected
|
||||
cdef int hl = hlen - self.iv_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)
|
||||
|
@ -555,11 +551,9 @@ cdef class _AEAD_BASE:
|
|||
try:
|
||||
if not EVP_DecryptInit_ex(self.ctx, self.cipher(), NULL, NULL, NULL):
|
||||
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):
|
||||
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')
|
||||
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')
|
||||
|
@ -604,18 +598,6 @@ cdef class _AEAD_BASE:
|
|||
iv = int.from_bytes(self.iv[:self.iv_len], byteorder='big')
|
||||
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):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
|
@ -94,7 +94,7 @@ def test_AE(self):
|
|||
key = b'X' * 32
|
||||
iv = 0
|
||||
data = b'foo' * 10
|
||||
header = b'\x23'
|
||||
header = b'\x23' + iv.to_bytes(12, 'big')
|
||||
tests = [
|
||||
# (ciphersuite class, exp_mac, exp_cdata)
|
||||
]
|
||||
|
@ -137,7 +137,7 @@ def test_AEAD(self):
|
|||
key = b'X' * 32
|
||||
iv = 0
|
||||
data = b'foo' * 10
|
||||
header = b'\x12\x34\x56'
|
||||
header = b'\x12\x34\x56' + iv.to_bytes(12, 'big')
|
||||
tests = [
|
||||
# (ciphersuite class, exp_mac, exp_cdata)
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue