Explorar o código

add support for AES-OCB and chacha20-poly1305

also: use AEAD base class
Thomas Waldmann %!s(int64=8) %!d(string=hai) anos
pai
achega
15490d520d

+ 9 - 1
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 <string.h>
 #include <openssl/opensslv.h>
@@ -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

+ 5 - 1
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 <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

+ 44 - 8
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(<unsigned char *> 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

+ 99 - 3
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 @@ class CryptoTestCase(BaseTestCase):
         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 @@ class CryptoTestCase(BaseTestCase):
         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 @@ class CryptoTestCase(BaseTestCase):
         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