Skip to content

refactoring Algorithm definition components into separate encryption, kdf, and authentication algorithm suites #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 166 additions & 90 deletions src/aws_encryption_sdk/identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,116 +25,189 @@
USER_AGENT_SUFFIX = 'AwsEncryptionSdkPython-KMSMasterKey/{}'.format(__version__)


def _kdf_input_len_check(data_key_len, kdf_type, kdf_input_len):
"""Validates that data_key_len and kdf_input_len have the correct relationship.
class EncryptionSuite(Enum):
"""Static definition of encryption algorithm details.

.. warning:: These members must only be used as part of an AlgorithmSuite.

:param algorithm: Encryption algorithm to use
:type algorithm: cryptography.io ciphers algorithm object
:param mode: Encryption mode in which to operate
:type mode: cryptography.io ciphers modes object
:param int data_key_length: Number of bytes in envelope encryption data key
:param int iv_length: Number of bytes in IV
:param int auth_length: Number of bytes in auth data (tag)
:param int auth_key_length: Number of bytes in auth key (not currently supported by any algorithms)
"""

AES_128_GCM_IV12_TAG16 = (algorithms.AES, modes.GCM, 16, 12, 16)
AES_192_GCM_IV12_TAG16 = (algorithms.AES, modes.GCM, 24, 12, 16)
AES_256_GCM_IV12_TAG16 = (algorithms.AES, modes.GCM, 32, 12, 16)

def __init__(self, algorithm, mode, data_key_length, iv_length, auth_length, auth_key_length=0):
"""Prepare a new EncryptionSuite."""
self.algorithm = algorithm
self.mode = mode
self.data_key_length = data_key_length
self.iv_length = iv_length
self.auth_length = self.tag_len = auth_length
# Auth keys are not currently supported
self.auth_key_length = auth_key_length

def valid_kdf(self, kdf):
"""Determine whether a KDFSuite can be used with this EncryptionSuite.

:param kdf: KDFSuite to evaluate
:type kdf: aws_encryption_sdk.identifiers.KDFSuite
:rtype: bool
"""
if kdf.input_length is None:
return True

if self.data_key_length > kdf.input_length(self):
raise InvalidAlgorithmError(
'Invalid Algorithm definition: data_key_len must not be greater than kdf_input_len'
)

return True

:param int data_key_len: Number of bytes in key
:param kdf_type: KDF algorithm to use
:param kdf_type: cryptography.io KDF object
:param int kdf_input_len: Length of input data to feed into KDF function

class KDFSuite(Enum):
"""Static definition of key derivation algorithm details.

.. warning:: These members must only be used as part of an AlgorithmSuite.

:param algorithm: KDF algorithm to use
:type algorithm: cryptography.io KDF object
:param int input_length: Number of bytes of input data to feed into KDF function
:param hash_algorithm: Hash algorithm to use in KDF
:type hash_algorithm: cryptography.io hashes object
"""
if kdf_type is None and data_key_len != kdf_input_len:
raise InvalidAlgorithmError(
'Invalid Algorithm definition: data_key_len must equal kdf_input_len for non-KDF algorithms'
)
elif data_key_len > kdf_input_len:
raise InvalidAlgorithmError(
'Invalid Algorithm definition: data_key_len must not be greater than kdf_input_len'
)

NONE = (None, None, None)
HKDF_SHA256 = (hkdf.HKDF, None, hashes.SHA256)
HKDF_SHA384 = (hkdf.HKDF, None, hashes.SHA384)

def __init__(self, algorithm, input_length, hash_algorithm):
"""Prepare a new KDFSuite."""
self.algorithm = algorithm
self._input_length = input_length
self.hash_algorithm = hash_algorithm

def input_length(self, encryption):
"""Determine the correct KDF input value length for this KDFSuite when used with
a specific EncryptionSuite.

:param encryption: EncryptionSuite to use
:type encryption: aws_encryption_sdk.identifiers.EncryptionSuite
:rtype: int
"""
# type: (EncryptionSuite) -> bool
if self._input_length is None:
return encryption.data_key_length

return self._input_length

class Algorithm(Enum): # pylint: disable=too-many-instance-attributes
"""IDs of cryptographic algorithms this library knows about.

class AuthenticationSuite(Enum):
"""Static definition of authentication algorithm details.

.. warning:: These members must only be used as part of an AlgorithmSuite.

:param algorithm: Information needed by signing algorithm to define behavior
:type algorithm: may vary (currently only ECC curve object)
:param hash_algorithm: Hash algorithm to use in signature
:type hash_algorithm: cryptography.io hashes object
:param int signature_lenth: Number of bytes in signature
"""

NONE = (None, None, 0)
SHA256_ECDSA_P256 = (ec.SECP256R1, hashes.SHA256, 71)
SHA256_ECDSA_P384 = (ec.SECP384R1, hashes.SHA384, 103)

def __init__(self, algorithm, hash_algorithm, signature_length):
"""Prepare a new AuthenticationSuite."""
self.algorithm = algorithm
self.hash_algorithm = hash_algorithm
self.signature_length = signature_length


class AlgorithmSuite(Enum): # pylint: disable=too-many-instance-attributes
"""Static combinations of encryption, KDF, and authentication algorithms.

.. warning:: No AlgorithmSuites except those defined here are supported.

:param int algorithm_id: KMS Encryption Algorithm ID
:param encryption_algorithm: Encryption algorithm to use
:type encryption_algorithm: cryptography.io ciphers algorithm object
:param encryption_mode: Encryption mode in which to operate
:type encryption_mode: cryptography.io ciphers modes object
:param int iv_len: Number of bytes in IV
:param int auth_len: Number of bytes in auth data (tag)
:param int auth_key_len: Number of bytes in auth key (not currently supported by any algorithms)
:param int data_key_len: Number of bytes in envelope encryption data key
:param kdf_type: KDF algorithm to use
:type kdf_type: cryptography.io KDF object
:param int kdf_input_len: Number of bytes of input data to feed into KDF function
:param kdf_hash_type: Hash algorithm to use in KDF
:type kdf_hash_type: cryptography.io hashes object
:param signing_algorithm_info: Information needed by signing algorithm to define behavior
:type signing_algorithm_info: may vary (currently only ECC curve object)
:param signature_hash_type: Hash algorithm to use in signature
:type signature_hash_type: cryptography.io hashes object
:param int signature_len: Number of bytes in signature
:param encryption_suite: EncryptionSuite to use with this AlgorithmSuite
:type encryption_suite: aws_encryption_sdk.identifiers.EncryptionSuite
:param kdf_suite: KDFSuite to use with this AlgorithmSuite
:type kdf_suite: aws_encryption_sdk.identifiers.KDFSuite
:param authentication_suite: AuthenticationSuite to use with this AlgorithmSuite
:type authentication_suite: aws_encryption_sdk.identifiers.AuthenticationSuite
"""

__rlookup__ = {} # algorithm_id -> Algorithm
__rlookup__ = {} # algorithm_id -> AlgorithmSuite

AES_128_GCM_IV12_TAG16 = (0x0014, algorithms.AES, modes.GCM, 12, 16, 0, 16, None, 16, None, None, None, 0)
AES_192_GCM_IV12_TAG16 = (0x0046, algorithms.AES, modes.GCM, 12, 16, 0, 24, None, 24, None, None, None, 0)
AES_256_GCM_IV12_TAG16 = (0x0078, algorithms.AES, modes.GCM, 12, 16, 0, 32, None, 32, None, None, None, 0)
AES_128_GCM_IV12_TAG16_HKDF_SHA256 = (
0x0114, algorithms.AES, modes.GCM, 12, 16, 0, 16, hkdf.HKDF, 16, hashes.SHA256, None, None, 0
)
AES_192_GCM_IV12_TAG16_HKDF_SHA256 = (
0x0146, algorithms.AES, modes.GCM, 12, 16, 0, 24, hkdf.HKDF, 24, hashes.SHA256, None, None, 0
)
AES_256_GCM_IV12_TAG16_HKDF_SHA256 = (
0x0178, algorithms.AES, modes.GCM, 12, 16, 0, 32, hkdf.HKDF, 32, hashes.SHA256, None, None, 0
)
AES_128_GCM_IV12_TAG16 = (0x0014, EncryptionSuite.AES_128_GCM_IV12_TAG16)
AES_192_GCM_IV12_TAG16 = (0x0046, EncryptionSuite.AES_192_GCM_IV12_TAG16)
AES_256_GCM_IV12_TAG16 = (0x0078, EncryptionSuite.AES_256_GCM_IV12_TAG16)
AES_128_GCM_IV12_TAG16_HKDF_SHA256 = (0x0114, EncryptionSuite.AES_128_GCM_IV12_TAG16, KDFSuite.HKDF_SHA256)
AES_192_GCM_IV12_TAG16_HKDF_SHA256 = (0x0146, EncryptionSuite.AES_192_GCM_IV12_TAG16, KDFSuite.HKDF_SHA256)
AES_256_GCM_IV12_TAG16_HKDF_SHA256 = (0x0178, EncryptionSuite.AES_256_GCM_IV12_TAG16, KDFSuite.HKDF_SHA256)
AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256 = (
0x0214, algorithms.AES, modes.GCM, 12, 16, 0, 16, hkdf.HKDF, 16, hashes.SHA256, ec.SECP256R1, hashes.SHA256, 71
0x0214, EncryptionSuite.AES_128_GCM_IV12_TAG16, KDFSuite.HKDF_SHA256, AuthenticationSuite.SHA256_ECDSA_P256
)
AES_192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384 = (
0x0346, algorithms.AES, modes.GCM, 12, 16, 0, 24, hkdf.HKDF, 24, hashes.SHA384, ec.SECP384R1, hashes.SHA384, 103
0x0346, EncryptionSuite.AES_192_GCM_IV12_TAG16, KDFSuite.HKDF_SHA384, AuthenticationSuite.SHA256_ECDSA_P384
)
AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384 = (
0x0378, algorithms.AES, modes.GCM, 12, 16, 0, 32, hkdf.HKDF, 32, hashes.SHA384, ec.SECP384R1, hashes.SHA384, 103
0x0378, EncryptionSuite.AES_256_GCM_IV12_TAG16, KDFSuite.HKDF_SHA384, AuthenticationSuite.SHA256_ECDSA_P384
)

def __init__( # pylint: disable=too-many-arguments
def __init__(
self,
algorithm_id,
encryption_algorithm,
encryption_mode,
iv_len,
auth_len,
auth_key_len,
data_key_len,
kdf_type,
kdf_input_len,
kdf_hash_type,
signing_algorithm_info,
signing_hash_type,
signature_len
algorithm_id, # type: int
encryption, # type: EncryptionSuite
kdf=KDFSuite.NONE, # type: Optional[KDFSuite]
authentication=AuthenticationSuite.NONE, # type: Optional[AuthenticationSuite]
allowed=True # type: bool
):
"""Prepares new Algorithm."""
_kdf_input_len_check(
data_key_len=data_key_len,
kdf_type=kdf_type,
kdf_input_len=kdf_input_len
)
# type: (...) -> None
"""Prepare a new AlgorithmSuite."""
self.algorithm_id = algorithm_id
self.encryption_algorithm = encryption_algorithm
self.encryption_mode = encryption_mode
self.iv_len = iv_len
# Auth keys are not currently supported
self.auth_key_len = auth_key_len
self.auth_len = self.tag_len = auth_len
self.data_key_len = data_key_len
self.kdf_type = kdf_type
self.kdf_input_len = kdf_input_len
self.kdf_hash_type = kdf_hash_type
self.signing_algorithm_info = signing_algorithm_info
self.signing_hash_type = signing_hash_type
self.signature_len = signature_len
# All algorithms in this enum are allowed for now.
# This might change in the future.
self.allowed = True
self.encryption = encryption
self.encryption.valid_kdf(kdf)
self.kdf = kdf
self.authentication = authentication
self.allowed = allowed

# Encryption Values
self.encryption_algorithm = self.encryption.algorithm
self.encryption_mode = self.encryption.mode
self.data_key_len = self.encryption.data_key_length
self.iv_len = self.encryption.iv_length
self.auth_key_len = self.encryption.auth_key_length
self.auth_len = self.tag_len = self.encryption.auth_length

# KDF Values
self.kdf_type = self.kdf.algorithm
self.kdf_hash_type = self.kdf.hash_algorithm

# Authentication Values
self.signing_algorithm_info = self.authentication.algorithm
self.signing_hash_type = self.authentication.hash_algorithm
self.signature_len = self.authentication.signature_length

self.__rlookup__[algorithm_id] = self

@property
def kdf_input_len(self):
"""Determine the correct KDF input value length for this algorithm suite."""
return self.kdf.input_length(self.encryption)

@classmethod
def get_by_id(cls, algorithm_id):
"""Returns the correct member based on the algorithm_id value.
"""Return the correct member based on the algorithm_id value.

:param algorithm_id: Value of algorithm_id field with which to retrieve Algorithm
:type algorithm_id: int
Expand All @@ -144,12 +217,15 @@ def get_by_id(cls, algorithm_id):
return cls.__rlookup__[algorithm_id]

def id_as_bytes(self):
"""Returns the algorithm suite ID as a 2-byte array"""
"""Return the algorithm suite ID as a 2-byte array"""
return struct.pack('>H', self.algorithm_id)

def safe_to_cache(self):
"""Determines whether encryption materials for this algorithm suite should be cached."""
return self.kdf_type is not None
"""Determine whether encryption materials for this algorithm suite should be cached."""
return self.kdf is not KDFSuite.NONE


Algorithm = AlgorithmSuite


class EncryptionType(Enum):
Expand Down
38 changes: 23 additions & 15 deletions test/unit/test_identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
"""Unit test suite for aws_encryption_sdk.identifiers"""
from mock import Mock
import pytest

from aws_encryption_sdk.exceptions import InvalidAlgorithmError
from aws_encryption_sdk.identifiers import _kdf_input_len_check, Algorithm
from aws_encryption_sdk.identifiers import Algorithm, EncryptionSuite, KDFSuite


@pytest.mark.parametrize('check_algorithm, safe_to_cache', (
Expand All @@ -35,23 +36,30 @@ def test_algorithm_safe_to_cache(check_algorithm, safe_to_cache):
assert not check_algorithm.safe_to_cache()


def test_kdf_input_len_check_valid():
_kdf_input_len_check(
data_key_len=5,
kdf_type=5,
kdf_input_len=5
)
@pytest.mark.parametrize('suite', [suite for suite in EncryptionSuite])
def test_encryption_suite_invalid_kdf(suite):
mock_kdf = Mock()
mock_kdf.input_length.return_value = 1
with pytest.raises(InvalidAlgorithmError) as excinfo:
suite.valid_kdf(mock_kdf)

excinfo.match(r'Invalid Algorithm definition: data_key_len must not be greater than kdf_input_len')

def test_kdf_input_len_check_invalid_no_kdf():
with pytest.raises(InvalidAlgorithmError) as excinfo:
_kdf_input_len_check(data_key_len=2, kdf_type=None, kdf_input_len=5)

excinfo.match(r'Invalid Algorithm definition: data_key_len must equal kdf_input_len for non-KDF algorithms')
def build_valid_kdf_checks():
checks = []
for suite in EncryptionSuite:
checks.append((suite, KDFSuite.NONE, True))
checks.append((suite, KDFSuite.HKDF_SHA256, True))
checks.append((suite, KDFSuite.HKDF_SHA384, True))
return checks


def test_kdf_input_len_check_invalid_with_kdf():
with pytest.raises(InvalidAlgorithmError) as excinfo:
_kdf_input_len_check(data_key_len=5, kdf_type=5, kdf_input_len=2)
@pytest.mark.parametrize('encryption, kdf, expected', build_valid_kdf_checks())
def test_encryption_suite_valid_kdf(encryption, kdf, expected):
actual = encryption.valid_kdf(kdf)

excinfo.match(r'Invalid Algorithm definition: data_key_len must not be greater than kdf_input_len')
if expected:
assert actual
else:
assert not actual