Skip to content

Commit

Permalink
Merge pull request #384 from inikolcev/separate-aes-ctr
Browse files Browse the repository at this point in the history
Separate CTR code and replace it in AES-CCM and AES-GCM
  • Loading branch information
tomato42 committed Mar 11, 2020
2 parents 27157e0 + 84c57db commit cfdfb7c
Show file tree
Hide file tree
Showing 14 changed files with 920 additions and 91 deletions.
19 changes: 12 additions & 7 deletions tests/tlstest.py
Original file line number Diff line number Diff line change
Expand Up @@ -921,10 +921,13 @@ def connect():
continue
if cipher in ("aes128gcm", "aes256gcm") and \
implementation not in ("pycrypto",
"python"):
"python", "openssl"):
continue
if cipher in ("chacha20-poly1305_draft00", "chacha20-poly1305",
"aes128ccm", "aes128ccm_8", "aes256ccm", "aes256ccm_8") \
if cipher in ("aes128ccm", "aes128ccm_8",
"aes256ccm", "aes256ccm_8") and \
implementation not in ("python", "openssl"):
continue
if cipher in ("chacha20-poly1305_draft00", "chacha20-poly1305") \
and implementation not in ("python", ):
continue

Expand Down Expand Up @@ -2211,11 +2214,13 @@ def server_bind(self):
continue
if cipher in ("aes128gcm", "aes256gcm") and \
implementation not in ("pycrypto",
"python"):
"python", "openssl"):
continue
if cipher in ("aes128ccm", "aes128ccm_8",
"aes256ccm", "aes256ccm_8") and \
implementation not in ("python", "openssl"):
continue
if cipher in ("chacha20-poly1305_draft00", "chacha20-poly1305",
"aes128ccm", "aes128ccm_8",
"aes256ccm", "aes256ccm_8") \
if cipher in ("chacha20-poly1305_draft00", "chacha20-poly1305") \
and implementation not in ("python", ):
continue

Expand Down
10 changes: 7 additions & 3 deletions tlslite/utils/aes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ class AES(object):
def __init__(self, key, mode, IV, implementation):
if len(key) not in (16, 24, 32):
raise AssertionError()
if mode != 2:
raise AssertionError()
if len(IV) != 16:
if mode not in [2, 6]:
raise AssertionError()
if mode == 2:
if len(IV) != 16:
raise AssertionError()
if mode == 6:
if len(IV) > 16:
raise AssertionError()
self.isBlockCipher = True
self.isAEAD = False
self.block_size = 16
Expand Down
75 changes: 19 additions & 56 deletions tlslite/utils/aesccm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
#

from __future__ import division
from tlslite.utils.cryptomath import numberToByteArray, divceil
from tlslite.utils.python_aes import Python_AES
import sys
import array
from tlslite.utils.cryptomath import numberToByteArray
from tlslite.utils import python_aes


class AESCCM(object):
Expand All @@ -18,6 +16,9 @@ def __init__(self, key, implementation, rawAesEncrypt, tag_length=16):
self.isAEAD = True
self.key = key
self.tagLength = tag_length
self.nonceLength = 12
self.implementation = implementation

if len(self.key) == 16 and self.tagLength == 8:
self.name = "aes128ccm_8"
elif len(self.key) == 16 and self.tagLength == 16:
Expand All @@ -27,10 +28,10 @@ def __init__(self, key, implementation, rawAesEncrypt, tag_length=16):
else:
assert len(self.key) == 32 and self.tagLength == 16
self.name = "aes256ccm"
self._rawAesEncrypt = rawAesEncrypt
self.implementation = implementation
self.nonceLength = 12
self._cbc = Python_AES(self.key, 2, bytearray(b'\x00' * 16))

self._ctr = python_aes.new(self.key, 6, bytearray(b'\x00' * 16))
self._cbc = python_aes.new(self.key, 2, bytearray(b'\x00' * 16))


def _cbcmac_calc(self, nonce, aad, msg):
L = 15 - len(nonce)
Expand Down Expand Up @@ -89,25 +90,19 @@ def seal(self, nonce, msg, aad):
raise ValueError("Bad nonce length")

L = 15 - len(nonce)
auth_value = bytearray(self.tagLength)

# We construct the key stream blocks.
# S_0 is not used for encrypting the message, it is only used
# to compute the authentication value.
# S_1..S_n are used to encrypt the message.

flags = L - 1
s_0 = self._rawAesEncrypt(bytearray([flags]) +
nonce + numberToByteArray(0, L))

s_n = self._construct_s_n(msg, flags, nonce, L)

enc_msg = self._xor(msg, s_n)
s_0 = bytearray([flags]) + nonce + numberToByteArray(0, L)

mac = self._cbcmac_calc(nonce, aad, msg)

for i in range(self.tagLength):
auth_value[i] = mac[i] ^ s_0[i]
self._ctr.counter = s_0
auth_value = self._ctr.encrypt(mac)
enc_msg = self._ctr.encrypt(msg)

ciphertext = enc_msg + auth_value
return ciphertext
Expand All @@ -122,61 +117,29 @@ def open(self, nonce, ciphertext, aad):
return None

L = 15 - len(nonce)
received_mac = bytearray(self.tagLength)
flags = L - 1

# Same construction as in seal function

s_0 = self._rawAesEncrypt(bytearray([flags]) +
nonce + numberToByteArray(0, L))
s_0 = bytearray([flags]) + nonce + numberToByteArray(0, L)

s_n = self._construct_s_n(ciphertext, flags, nonce, L)
auth_value = ciphertext[-self.tagLength:]

msg = self._xor(ciphertext, s_n)
# We decrypt the auth value
self._ctr.counter = s_0
received_mac = self._ctr.decrypt(auth_value)
msg = self._ctr.decrypt(ciphertext)
msg = msg[:-self.tagLength]

auth_value = ciphertext[-self.tagLength:]
computed_mac = self._cbcmac_calc(nonce, aad, msg)

# We decrypt the auth value
for i in range(self.tagLength):
received_mac[i] = auth_value[i] ^ s_0[i]

# Compare the mac vlaue is the same as the one we computed
if received_mac != computed_mac:
return None
return msg

def _construct_s_n(self, ciphertext, flags, nonce, L):
s_n = bytearray()
counter_lmt = divceil(len(ciphertext), 16)
for i in range(1, int(counter_lmt) + 1):
s_n += self._rawAesEncrypt(bytearray([flags]) +
nonce + numberToByteArray(i, L))
return s_n

if sys.version_info[0] >= 3:
def _xor(self, inp, s_n):
inp_added = -((8 - (len(inp) % 8)) % 8) or None
self._pad_with_zeroes(inp, 8)
msg = self._use_memoryview(inp, s_n)[:inp_added]
inp[:] = inp[:inp_added]
return msg
else:
def _xor(self, inp, s_n):
msg = bytearray(i ^ j for i, j in zip(inp, s_n))
return msg

@staticmethod
def _pad_with_zeroes(data, size):
if len(data) % size != 0:
zeroes_to_add = size - (len(data) % size)
data += b'\x00' * zeroes_to_add

@staticmethod
def _use_memoryview(msg, s_n):
msg_mv = memoryview(msg).cast('Q')
s_n_mv = memoryview(s_n).cast('Q')
enc_arr = array.array('Q', (i ^ j for i, j in zip(msg_mv, s_n_mv)))
enc_msg = bytearray(enc_arr.tobytes())
return enc_msg
21 changes: 7 additions & 14 deletions tlslite/utils/aesgcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# look-up table.

from __future__ import division
from tlslite.utils import python_aes
from .constanttime import ct_compare_digest
from .cryptomath import bytesToNumber, numberToByteArray

Expand All @@ -35,8 +36,10 @@ def __init__(self, key, implementation, rawAesEncrypt):
self.name = "aes256gcm"
else:
raise AssertionError()
self.key = key

self._rawAesEncrypt = rawAesEncrypt
self._ctr = python_aes.new(self.key, 6, bytearray(b'\x00' * 16))

# The GCM key is AES(0).
h = bytesToNumber(self._rawAesEncrypt(bytearray(16)))
Expand All @@ -53,18 +56,6 @@ def __init__(self, key, implementation, rawAesEncrypt):
self._productTable[self._reverseBits(i+1)] = \
self._gcmAdd(self._productTable[self._reverseBits(i)], h)

def _rawAesCtrEncrypt(self, counter, inp):
"""
Encrypts (or decrypts) plaintext with AES-CTR. counter is modified.
"""
out = bytearray(len(inp))
rawAesEncrypt = self._rawAesEncrypt
for i in range(0, len(out), 16):
mask = rawAesEncrypt(counter)
for j in range(i, min(len(out), i + 16)):
out[j] = inp[j] ^ mask[j-i]
self._inc32(counter)
return out

def _auth(self, ciphertext, ad, tagMask):
y = 0
Expand Down Expand Up @@ -125,7 +116,8 @@ def seal(self, nonce, plaintext, data):

# The counter starts at 2 for the actual encryption.
counter[-1] = 2
ciphertext = self._rawAesCtrEncrypt(counter, plaintext)
self._ctr.counter = counter
ciphertext = self._ctr.encrypt(plaintext)

tag = self._auth(ciphertext, data, tagMask)

Expand Down Expand Up @@ -158,7 +150,8 @@ def open(self, nonce, ciphertext, data):

# The counter starts at 2 for the actual decryption.
counter[-1] = 2
return self._rawAesCtrEncrypt(counter, ciphertext)
self._ctr.counter = counter
return self._ctr.decrypt(ciphertext)

@staticmethod
def _reverseBits(i):
Expand Down
36 changes: 32 additions & 4 deletions tlslite/utils/cipherfactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from tlslite.utils import python_chacha20_poly1305
from tlslite.utils import python_rc4
from tlslite.utils import python_tripledes
from tlslite.utils import openssl_aesccm
from tlslite.utils import openssl_aesgcm

from tlslite.utils import cryptomath

Expand Down Expand Up @@ -53,7 +55,27 @@ def createAES(key, IV, implList=None):
elif impl == "pycrypto" and cryptomath.pycryptoLoaded:
return pycrypto_aes.new(key, 2, IV)
elif impl == "python":
return python_aes.new(key, 2, IV)
return python_aes.new(key, 2, IV)
raise NotImplementedError()

def createAESCTR(key, IV, implList=None):
"""Create a new AESCTR object.
:type key: str
:param key: A 16, 24, or 32 byte string.
:type IV: str
:param IV: A 8 or 12 byte string
:rtype: tlslite.utils.AES
:returns: An AES object.
"""
if implList is None:
implList = ["python"]

for impl in implList:
if impl == "python":
return python_aes.new(key, 6, IV)
raise NotImplementedError()

def createAESGCM(key, implList=None):
Expand All @@ -66,9 +88,11 @@ def createAESGCM(key, implList=None):
:returns: An AESGCM object.
"""
if implList is None:
implList = ["pycrypto", "python"]
implList = ["openssl", "pycrypto", "python"]

for impl in implList:
if impl == "openssl" and cryptomath.m2cryptoLoaded:
return openssl_aesgcm.new(key)
if impl == "pycrypto" and cryptomath.pycryptoLoaded:
return pycrypto_aesgcm.new(key)
if impl == "python":
Expand All @@ -86,9 +110,11 @@ def createAESCCM(key, implList=None):
"""

if implList is None:
implList = ["python"]
implList = ["openssl", "python"]

for impl in implList:
if impl == "openssl" and cryptomath.m2cryptoLoaded:
return openssl_aesccm.new(key)
if impl == "python":
return python_aesccm.new(key)

Expand All @@ -105,9 +131,11 @@ def createAESCCM_8(key, implList=None):
"""

if implList is None:
implList = ["python"]
implList = ["openssl", "python"]

for impl in implList:
if impl == "openssl" and cryptomath.m2cryptoLoaded:
return openssl_aesccm.new(key, 8)
if impl == "python":
return python_aesccm.new(key, 8)

Expand Down
58 changes: 57 additions & 1 deletion tlslite/utils/openssl_aes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
def new(key, mode, IV):
# IV argument name is a part of the interface
# pylint: disable=invalid-name
return OpenSSL_AES(key, mode, IV)
if mode == 2:
return OpenSSL_AES(key, mode, IV)
elif mode == 6:
return OpenSSL_CTR(key, mode, IV)
else:
raise NotImplementedError()


class OpenSSL_AES(AES):

Expand Down Expand Up @@ -58,3 +64,53 @@ def decrypt(self, ciphertext):
def __del__(self):
if self._context is not None:
m2.cipher_ctx_free(self._context)


class OpenSSL_CTR(AES):

def __init__(self, key, mode, IV):
# IV argument/field names are a part of the interface
# pylint: disable=invalid-name
AES.__init__(self, key, mode, IV, "openssl")
self._IV = IV
self.key = key
self._context = None
self._encrypt = None
if len(key) not in (16, 24, 32):
raise AssertionError()

@property
def counter(self):
return self._IV

@counter.setter
def counter(self, ctr):
if self._context is not None:
m2.cipher_ctx_free(self._context)
self._IV = ctr
self._init_context()

def _init_context(self, encrypt=True):
if len(self.key) == 16:
cipherType = m2.aes_128_ctr()
if len(self.key) == 24:
cipherType = m2.aes_192_ctr()
if len(self.key) == 32:
cipherType = m2.aes_256_ctr()
self._context = m2.cipher_ctx_new()
m2.cipher_init(self._context, cipherType, self.key, self._IV,
int(encrypt))
m2.cipher_set_padding(self._context, 0)
self._encrypt = encrypt

def encrypt(self, plaintext):
ciphertext = m2.cipher_update(self._context, plaintext)
return bytearray(ciphertext)

def decrypt(self, ciphertext):
plaintext = m2.cipher_update(self._context, ciphertext)
return bytearray(plaintext)

def __del__(self):
if self._context is not None:
m2.cipher_ctx_free(self._context)
Loading

0 comments on commit cfdfb7c

Please sign in to comment.