"""Reference implementation of the `Gemina specification`_.
.. _Gemina specification: https://github.com/andreas19/gemina-spec
For more details see the section
`Description <https://github.com/andreas19/gemina-spec#description>`_.
The initialization vector for CBC, the keys, and the salt for HMAC
are created with :func:`os.urandom`.
The ``key`` argument for the functions :func:`encrypt_with_key`,
:func:`decrypt_with_key`, and :func:`verify_with_key` should be created
with :func:`create_secret_key()`.
"""
import os
from enum import Enum
from importlib.resources import read_text
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers.algorithms import AES
from cryptography.hazmat.primitives.ciphers.modes import CBC
from cryptography.hazmat.primitives.hmac import HMAC
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.padding import PKCS7
from salmagundi.utils import check_type
__version__ = read_text(__package__, 'VERSION').strip()
__all__ = ['DecryptError', 'Version', 'create_secret_key', 'decrypt_with_key',
'decrypt_with_password', 'encrypt_with_key', 'encrypt_with_password',
'verify_with_key', 'verify_with_password']
_MAC_HASH = _KDF_HASH = SHA256()
_IV_LEN = AES.block_size // 8 # 16 bytes
_MAC_LEN = _MAC_HASH.digest_size # 32 bytes
_SALT_LEN = 16 # bytes
_VERSION_LEN = 1 # byte
[docs]
class Version(Enum):
"""Version enum."""
V1 = (b'\x8a', 16, 16, 100000) #: version 1
V2 = (b'\x8b', 16, _MAC_LEN, 100000) #: version 2
V3 = (b'\x8c', 24, _MAC_LEN, 100000) #: version 3
V4 = (b'\x8d', 32, _MAC_LEN, 100000) #: version 4
V5 = (b'\x8e', 32, _MAC_LEN, 600000) #: version 5
def __new__(cls, version_byte, enc_key_len, mac_key_len, iterations):
"""Create new Version."""
obj = object.__new__(cls)
obj._value_ = version_byte
obj._enc_key_len = enc_key_len # bytes
obj._mac_key_len = mac_key_len # bytes
obj._iterations = iterations
return obj
def __repr__(self):
return '<%s.%s>' % (self.__class__.__name__, self.name)
[docs]
class DecryptError(Exception):
"""Raised if data could not be decrypted."""
def _verify(data, mac_key):
h = HMAC(mac_key, _MAC_HASH)
h.update(data[:-_MAC_LEN])
try:
h.verify(data[-_MAC_LEN:])
return True
except InvalidSignature:
return False
def _encrypt(enc_key, mac_key, salt, data, version):
iv = os.urandom(_IV_LEN)
padder = PKCS7(AES.block_size).padder()
padded_data = padder.update(data) + padder.finalize()
encryptor = Cipher(AES(enc_key), CBC(iv)).encryptor()
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
all_data = b''.join((version.value, salt, iv, ciphertext))
h = HMAC(mac_key, _MAC_HASH)
h.update(all_data)
hmac = h.finalize()
return all_data + hmac
def _decrypt(enc_key, mac_key, salt, data):
if not _verify(data, mac_key):
raise DecryptError('signature could not be verified') from None
pos = _VERSION_LEN + len(salt) + _IV_LEN
iv = data[_VERSION_LEN + len(salt):pos]
decryptor = Cipher(AES(enc_key), CBC(iv)).decryptor()
try:
padded_plaintext = (decryptor.update(data[pos:-_MAC_LEN]) +
decryptor.finalize())
unpadder = PKCS7(AES.block_size).unpadder()
return unpadder.update(padded_plaintext) + unpadder.finalize()
except ValueError:
raise DecryptError('data could not be decrypted') from None
def _check_data(data, with_salt):
if not len(data):
return None, None
try:
version = Version(data[:_VERSION_LEN])
except ValueError:
return None, None
min_len = _VERSION_LEN + 2 * _IV_LEN + _MAC_LEN
if with_salt:
min_len += _SALT_LEN
salt_len = _SALT_LEN
else:
salt_len = 0
if len(data) < min_len:
return None, None
return version, data[_VERSION_LEN:_VERSION_LEN + salt_len]
[docs]
def create_secret_key(*, version=Version.V1):
"""Create a secret key.
It can be used with the functions :func:`encrypt_with_key`,
:func:`decrypt_with_key`, and :func:`verify_with_key`.
:return: secret key
:rtype: bytes
"""
return os.urandom(version._enc_key_len + version._mac_key_len)
def _derive_keys(password, salt, version):
key = PBKDF2HMAC(_KDF_HASH,
length=version._enc_key_len + version._mac_key_len,
salt=salt, iterations=version._iterations).derive(password)
return key[:version._enc_key_len], key[version._enc_key_len:]
[docs]
def encrypt_with_password(password, data, *, version=Version.V1):
"""Encrypt data using a password.
The data will be encrypted with a key derived from the
password and signed.
:param bytes password: the password
:param bytes data: the data to encrypt
:return: encrypted data
:rtype: bytes
:raises TypeError: if ``password`` or ``data`` are not ``bytes``
"""
check_type(password, bytes, 'password')
check_type(data, bytes, 'data')
salt = os.urandom(_SALT_LEN)
enc_key, mac_key = _derive_keys(password, salt, version)
return _encrypt(enc_key, mac_key, salt, data, version)
[docs]
def decrypt_with_password(password, data):
"""Decrypt data using a password.
The data must have been encrypted with :func:`encrypt_with_password`.
:param bytes password: the password
:param bytes data: the encrypted data
:return: decrypted data
:rtype: bytes
:raises TypeError: if ``password`` or ``data`` are not ``bytes``
:raises DecryptError: if data could not be decrypted
"""
check_type(password, bytes, 'password')
check_type(data, bytes, 'data')
version, salt = _check_data(data, True)
if not version:
raise DecryptError('unknown version or not enough data')
enc_key, mac_key = _derive_keys(password, salt, version)
return _decrypt(enc_key, mac_key, salt, data)
[docs]
def verify_with_password(password, data):
"""Verify the encrypted data.
This function verifies the authenticity and the integrity of the data
with the key derived from the password. This is also done during decryption.
The data must have been encrypted with :func:`encrypt_with_password`.
:param bytes password: the password
:param bytes data: the encrypted data
:return: ``True`` if password, authenticity and integrity are okay
:rtype: bool
:raises TypeError: if ``password`` or ``data`` are not ``bytes``
"""
check_type(password, bytes, 'password')
check_type(data, bytes, 'data')
version, salt = _check_data(data, True)
if not version:
return False
_, mac_key = _derive_keys(password, salt, version)
return _verify(data, mac_key)
def _check_key_size(key, version):
if len(key) != version._enc_key_len + version._mac_key_len:
raise ValueError('incorrect secret key size')
[docs]
def encrypt_with_key(key, data, *, version=Version.V1):
"""Encrypt data using a secret key.
:param bytes key: the secret key
:param bytes data: the data to encrypt
:return: encrypted data
:rtype: bytes
:raises TypeError: if ``key`` or ``data`` are not ``bytes``
:raises ValueError: if size of ``key`` is not correct
"""
check_type(key, bytes, 'secret key')
check_type(data, bytes, 'data')
_check_key_size(key, version)
enc_key, mac_key = key[:version._enc_key_len], key[version._enc_key_len:]
return _encrypt(enc_key, mac_key, b'', data, version)
[docs]
def decrypt_with_key(key, data):
"""Decrypt data using a secret key.
The data must have been encrypted with :func:`encrypt_with_key`.
:param bytes key: the secret key
:param bytes data: the encrypted data
:return: decrypted data
:rtype: bytes
:raises TypeError: if ``key`` or ``data`` are not ``bytes``
:raises ValueError: if size of ``key`` is not correct
:raises DecryptError: if data could not be decrypted
"""
check_type(key, bytes, 'secret key')
check_type(data, bytes, 'data')
version, _ = _check_data(data, False)
if not version:
raise DecryptError('unknown version or not enough data')
_check_key_size(key, version)
enc_key, mac_key = key[:version._enc_key_len], key[version._enc_key_len:]
return _decrypt(enc_key, mac_key, b'', data)
[docs]
def verify_with_key(key, data):
"""Verify the encrypted data.
This function verifies the authenticity and the integrity of the data
with the given key. This is also done during decryption.
The data must have been encrypted with :func:`encrypt_with_key`.
:param bytes key: the secret key
:param bytes data: the encrypted data
:return: ``True`` if secret key, authenticity and integrity are okay
:rtype: bool
:raises TypeError: if ``key`` or ``data`` are not ``bytes``
:raises ValueError: if size of ``key`` is not correct
"""
check_type(key, bytes, 'secret key')
check_type(data, bytes, 'data')
version, _ = _check_data(data, False)
if not version:
return False
_check_key_size(key, version)
return _verify(data, key[version._enc_key_len:])