diff --git a/src/borg/crypto/_crypto_helpers.c b/src/borg/crypto/_crypto_helpers.c index 588e5f1c6..0a433bb5f 100644 --- a/src/borg/crypto/_crypto_helpers.c +++ b/src/borg/crypto/_crypto_helpers.c @@ -1,4 +1,4 @@ -/* add missing HMAC functions, so OpenSSL 1.0.x can be used like 1.1 */ +/* some helpers, so our code also works with OpenSSL 1.0.x */ #include #include @@ -24,4 +24,12 @@ void HMAC_CTX_free(HMAC_CTX *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 index e26815c6b..bb9afc418 100644 --- a/src/borg/crypto/_crypto_helpers.h +++ b/src/borg/crypto/_crypto_helpers.h @@ -1,11 +1,15 @@ -/* add missing HMAC functions, so OpenSSL 1.0.x can be used like 1.1 */ +/* some helpers, so our code also works with OpenSSL 1.0.x */ #include #include +#include #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/low_level.pyx b/src/borg/crypto/low_level.pyx index d854c9313..e4f1c3186 100644 --- a/src/borg/crypto/low_level.pyx +++ b/src/borg/crypto/low_level.pyx @@ -1,7 +1,5 @@ """An AEAD style OpenSSL wrapper -Note: AES-GCM mode needs OpenSSL >= 1.0.1d due to bug fixes in OpenSSL. - API: encrypt(data, header=b'', aad_offset=0) -> envelope @@ -79,6 +77,8 @@ cdef extern from "openssl/evp.h": const EVP_CIPHER *EVP_aes_256_ctr() 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) @@ -124,12 +124,20 @@ cdef extern from "openssl/hmac.h": 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 @@ -331,9 +339,13 @@ cdef class AES256_CTR_HMAC_SHA256: iv_out[i] = iv[8+i] -cdef class AES256_GCM: - # Layout: HEADER + GMAC 16 + IV 12 + CT +ctypedef const EVP_CIPHER * (* CIPHER)() + +cdef class _AEAD_BASE: + # Layout: HEADER + MAC 16 + IV 12 + CT + + cdef CIPHER cipher cdef EVP_CIPHER_CTX *ctx cdef unsigned char *enc_key cdef unsigned char iv[12] @@ -376,7 +388,7 @@ cdef class AES256_GCM: offset += hlen offset += 16 self.store_iv(odata+offset, self.iv) - rc = EVP_EncryptInit_ex(self.ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) + rc = EVP_EncryptInit_ex(self.ctx, self.cipher(), NULL, NULL, NULL) if not rc: raise CryptoError('EVP_EncryptInit_ex failed') if not EVP_CIPHER_CTX_ctrl(self.ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL): @@ -422,7 +434,7 @@ cdef class AES256_GCM: cdef int offset cdef Py_buffer idata = ro_buffer(envelope) try: - if not EVP_DecryptInit_ex(self.ctx, EVP_aes_256_gcm(), NULL, NULL, NULL): + if not EVP_DecryptInit_ex(self.ctx, self.cipher(), NULL, NULL, NULL): raise CryptoError('EVP_DecryptInit_ex failed') iv = self.fetch_iv( idata.buf+hlen+16) self.set_iv(iv) @@ -445,7 +457,7 @@ cdef class AES256_GCM: rc = EVP_DecryptFinal_ex(self.ctx, odata+offset, &olen) if rc <= 0: # a failure here means corrupted or tampered tag (mac) or data. - raise IntegrityError('GCM Authentication / EVP_DecryptFinal_ex failed') + raise IntegrityError('Authentication / EVP_DecryptFinal_ex failed') offset += olen self.blocks += num_aes_blocks(offset) return odata[:offset] @@ -454,7 +466,7 @@ cdef class AES256_GCM: PyBuffer_Release(&idata) def set_iv(self, iv): - self.blocks = 0 # number of AES blocks encrypted with this IV + self.blocks = 0 # number of cipher blocks encrypted with this IV for i in range(12): self.iv[i] = iv[i] @@ -476,6 +488,30 @@ cdef class AES256_GCM: iv_out[i] = iv[i] +cdef class AES256_GCM(_AEAD_BASE): + def __init__(self, mac_key, enc_key, iv=None): + if OPENSSL_VERSION_NUMBER < 0x10001040: + raise ValueError('AES GCM requires OpenSSL >= 1.0.1d. Detected: OpenSSL %08x' % OPENSSL_VERSION_NUMBER) + self.cipher = EVP_aes_256_gcm + super().__init__(mac_key, enc_key, iv=iv) + + +cdef class AES256_OCB(_AEAD_BASE): + def __init__(self, mac_key, enc_key, iv=None): + if OPENSSL_VERSION_NUMBER < 0x10100000: + raise ValueError('AES OCB requires OpenSSL >= 1.1.0. Detected: OpenSSL %08x' % OPENSSL_VERSION_NUMBER) + self.cipher = EVP_aes_256_ocb + super().__init__(mac_key, enc_key, iv=iv) + + +cdef class CHACHA20_POLY1305(_AEAD_BASE): + def __init__(self, mac_key, enc_key, iv=None): + if OPENSSL_VERSION_NUMBER < 0x10100000: + raise ValueError('CHACHA20-POLY1305 requires OpenSSL >= 1.1.0. Detected: OpenSSL %08x' % OPENSSL_VERSION_NUMBER) + self.cipher = EVP_chacha20_poly1305 + super().__init__(mac_key, enc_key, iv=iv) + + def hmac_sha256(key, data): cdef Py_buffer data_buf = ro_buffer(data) cdef const unsigned char *key_ptr = key diff --git a/src/borg/testsuite/crypto.py b/src/borg/testsuite/crypto.py index 4e8c17464..4582cb101 100644 --- a/src/borg/testsuite/crypto.py +++ b/src/borg/testsuite/crypto.py @@ -1,6 +1,7 @@ from binascii import hexlify, unhexlify -from ..crypto.low_level import AES256_CTR_HMAC_SHA256, AES256_GCM, IntegrityError, hmac_sha256, blake2b_256 +from ..crypto.low_level import AES256_CTR_HMAC_SHA256, AES256_GCM, AES256_OCB, CHACHA20_POLY1305, \ + IntegrityError, hmac_sha256, blake2b_256, openssl10 from ..crypto.low_level import bytes_to_long, bytes_to_int, long_to_bytes, bytes16_to_int, int_to_bytes16, increment_iv from ..crypto.low_level import hkdf_hmac_sha512 @@ -99,7 +100,7 @@ def test_AES256_CTR_HMAC_SHA256_aad(self): self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted, header_len=len(header), aad_offset=1)) - def test_AES_GCM_256_GMAC(self): + def test_AES_GCM_256(self): # gcm used in legacy-like layout (1 type byte, no aad) mac_key = None enc_key = b'X' * 32 @@ -129,7 +130,7 @@ def test_AES_GCM_256_GMAC(self): self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted, header_len=len(header), aad_offset=1)) - def test_AES_GCM_256_GMAC_aad(self): + def test_AES_GCM_256_aad(self): mac_key = None enc_key = b'X' * 32 iv = b'\0' * 12 @@ -158,6 +159,101 @@ def test_AES_GCM_256_GMAC_aad(self): self.assert_raises(IntegrityError, lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted, header_len=len(header), aad_offset=1)) + def test_AES_OCB_256(self): + if openssl10: # no OCB + return + # ocb used in legacy-like layout (1 type byte, no aad) + mac_key = None + enc_key = b'X' * 32 + iv = b'\0' * 12 + data = b'foo' * 10 + header = b'\x23' + # encrypt-then-mac + cs = AES256_OCB(mac_key, enc_key, iv) + hdr_mac_iv_cdata = cs.encrypt(data, header=header, aad_offset=1) + hdr = hdr_mac_iv_cdata[0:1] + mac = hdr_mac_iv_cdata[1:17] + iv = hdr_mac_iv_cdata[17:29] + cdata = hdr_mac_iv_cdata[29:] + self.assert_equal(hexlify(hdr), b'23') + self.assert_equal(hexlify(mac), b'b6909c23c9aaebd9abbe1ff42097652d') + self.assert_equal(hexlify(iv), b'000000000000000000000000') + self.assert_equal(hexlify(cdata), b'877ce46d2f62dee54699cebc3ba41d9ab613f7c486778c1b3636664b1493') + self.assert_equal(hexlify(cs.next_iv()), b'000000000000000000000001') + # auth-then-decrypt + cs = AES256_OCB(mac_key, enc_key) + pdata = cs.decrypt(hdr_mac_iv_cdata, header_len=len(header), aad_offset=1) + self.assert_equal(data, pdata) + self.assert_equal(hexlify(cs.next_iv()), b'000000000000000000000001') + # auth-failure due to corruption (corrupted data) + cs = AES256_OCB(mac_key, enc_key) + hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:29] + b'\0' + hdr_mac_iv_cdata[30:] + self.assert_raises(IntegrityError, + lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted, header_len=len(header), aad_offset=1)) + + def test_AES_OCB_256_aad(self): + if openssl10: # no OCB + return + mac_key = None + enc_key = b'X' * 32 + iv = b'\0' * 12 + data = b'foo' * 10 + header = b'\x12\x34\x56' + # encrypt-then-mac + cs = AES256_OCB(mac_key, enc_key, iv) + hdr_mac_iv_cdata = cs.encrypt(data, header=header, aad_offset=1) + hdr = hdr_mac_iv_cdata[0:3] + mac = hdr_mac_iv_cdata[3:19] + iv = hdr_mac_iv_cdata[19:31] + cdata = hdr_mac_iv_cdata[31:] + self.assert_equal(hexlify(hdr), b'123456') + self.assert_equal(hexlify(mac), b'f2748c412af1c7ead81863a18c2c1893') + self.assert_equal(hexlify(iv), b'000000000000000000000000') + self.assert_equal(hexlify(cdata), b'877ce46d2f62dee54699cebc3ba41d9ab613f7c486778c1b3636664b1493') + self.assert_equal(hexlify(cs.next_iv()), b'000000000000000000000001') + # auth-then-decrypt + cs = AES256_OCB(mac_key, enc_key) + pdata = cs.decrypt(hdr_mac_iv_cdata, header_len=len(header), aad_offset=1) + self.assert_equal(data, pdata) + self.assert_equal(hexlify(cs.next_iv()), b'000000000000000000000001') + # auth-failure due to corruption (corrupted aad) + cs = AES256_OCB(mac_key, enc_key) + hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:1] + b'\0' + hdr_mac_iv_cdata[2:] + self.assert_raises(IntegrityError, + lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted, header_len=len(header), aad_offset=1)) + + def test_CHACHA20_POLY1305(self): + if openssl10: # no CHACHA20, no POLY1305 + return + # used in legacy-like layout (1 type byte, no aad) + mac_key = None + enc_key = b'X' * 32 + iv = b'\0' * 12 + data = b'foo' * 10 + header = b'\x23' + # encrypt-then-mac + cs = CHACHA20_POLY1305(mac_key, enc_key, iv) + hdr_mac_iv_cdata = cs.encrypt(data, header=header, aad_offset=1) + hdr = hdr_mac_iv_cdata[0:1] + mac = hdr_mac_iv_cdata[1:17] + iv = hdr_mac_iv_cdata[17:29] + cdata = hdr_mac_iv_cdata[29:] + self.assert_equal(hexlify(hdr), b'23') + self.assert_equal(hexlify(mac), b'fd08594796e0706cde1e8b461e3e0555') + self.assert_equal(hexlify(iv), b'000000000000000000000000') + self.assert_equal(hexlify(cdata), b'a093e4b0387526f085d3c40cca84a35230a5c0dd766453b77ba38bcff775') + self.assert_equal(hexlify(cs.next_iv()), b'000000000000000000000001') + # auth-then-decrypt + cs = CHACHA20_POLY1305(mac_key, enc_key) + pdata = cs.decrypt(hdr_mac_iv_cdata, header_len=len(header), aad_offset=1) + self.assert_equal(data, pdata) + self.assert_equal(hexlify(cs.next_iv()), b'000000000000000000000001') + # auth-failure due to corruption (corrupted data) + cs = CHACHA20_POLY1305(mac_key, enc_key) + hdr_mac_iv_cdata_corrupted = hdr_mac_iv_cdata[:29] + b'\0' + hdr_mac_iv_cdata[30:] + self.assert_raises(IntegrityError, + lambda: cs.decrypt(hdr_mac_iv_cdata_corrupted, header_len=len(header), aad_offset=1)) + def test_hmac_sha256(self): # RFC 4231 test vectors key = b'\x0b' * 20