# -*- coding: utf-8 -*-
"""privatekey.py - Private Key : generate, save, load, decrypt, sign
Class:
* PrivateKey
"""
import base64
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import dsa
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives.asymmetric import ed448
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import utils as asymutils
from . import files
from . import utils
from .config import Base
from .config import PrivateKeyConfig
[docs]class PrivateKey(Base):
"""PrivateKey class - extends Base
Usage (minimum requirements):
* initialize : privk = PrivateKey() or privk = PrivateKey(PrivateKey())
* generate key: privk.gen(alg)
* save key: privk.save(filepath)
* load key: privk.load(filepath)
* decrypt: privk.decrypt(ciphertext)
* sign: privk.sign(message)
"""
def __init__(self, **kwargs):
"""PrivateKey class initiator
Args:
config (PrivateKeyConfig, optional): The configuration.
key (PrivateKey, optional): The private key. An instance of RSAPrivateKey.
or other cryptography private key object.
"""
super().__init__(**kwargs)
# configuration
if not hasattr(self, "config"):
self._config = kwargs.pop("config", PrivateKeyConfig())
# key object (cryptography compatible)
if not hasattr(self, "key"):
self._key = kwargs.pop("key", None)
# Generate
[docs] def gen(
self,
alg=None,
key_size=None,
public_exponent=None,
curve=None,
):
"""Generate the private key
Args:
alg (str): The key algorithm. RSA, EC, ED448, ED25519 and DSA are supported.
Defaults to None.
key_size (int, optional): Key size. Used in DSA and RSA.
Defaults to None.
public_exponent (int, optional): Public Exponent. Used in RSA.
Defaults to None.
curve (str): The name of the elliptic curve.
Defaults to None.
"""
# Defaults
if alg is None:
alg = self._config.alg.upper()
# Generate based on the algorithm
if alg == "RSA":
self.gen_rsa(key_size, public_exponent)
elif alg == "DSA":
self.gen_dsa(key_size)
elif alg == "ED448":
self.gen_ed448()
elif alg == "ED25519":
self.gen_ed25519()
elif alg == "EC":
self.gen_ec(curve)
else:
# Not implemented - use RSA
pass
[docs] def gen_rsa(
self,
key_size=None,
public_exponent=None,
):
"""Generate a RSA private key
Args:
key_size (int, optional): Key size.
Defaults to None.
public_exponent (int, optional): Public Exponent.
Defaults to None.
"""
# Default config
if key_size is None:
key_size = self._config.rsa_key_size
if public_exponent is None:
public_exponent = self._config.rsa_public_exponent
# Generate the key
self._key = rsa.generate_private_key(
public_exponent=public_exponent,
key_size=key_size,
)
[docs] def gen_dsa(self, key_size=None):
"""Generate a DSA private key
Args:
key_size (int, optional): Key size.
Defaults to None.
"""
# Default config
if key_size is None:
key_size = self._config.dsa_key_size
# Generate the key
self._key = dsa.generate_private_key(
key_size=key_size,
)
[docs] def gen_ed448(self):
"""Generate an ED448 private key"""
# Generate the key
self._key = ed448.Ed448PrivateKey.generate()
[docs] def gen_ed25519(self):
"""Generate an ED25519 private key"""
# Generate the key
self._key = ed25519.Ed25519PrivateKey.generate()
[docs] def gen_ec(self, curve=None):
"""Generate an Elliptic Curve private key
Args:
curve (str): The name of the elliptic curve.
Defaults to None.
"""
# Default config
if curve is None:
curve = self._config.elliptic_curve
# Generate the key
self._key = ec.generate_private_key(utils.ellipctic_curve(curve))
# Load
[docs] def load(self, path, encoding=None, passphrase=None):
"""Load the private key
Args:
path(str): The file path of the key to be loaded.
Defaults to None.
encoding (str, optional): Encoding PEM, DER or OpenSSH.
Defaults to None.
passphrase (str, optional): The passphrase. Only for encrypted PEM or
openSSH files.
Default to None.
"""
# passphrase
if passphrase is not None:
pwd = utils.convert_passphrase(passphrase)
else:
pwd = None
# encoding
if encoding is None:
encoding = self._config.encoding
if encoding == "PEM":
lines = files.read(path)
self._key = serialization.load_pem_private_key(lines, pwd)
elif encoding == "DER":
lines = files.read(path)
self._key = serialization.load_der_private_key(lines, pwd)
elif encoding == "OpenSSH":
lines = files.read(path)
self._key = serialization.load_ssh_private_key(lines, pwd)
else:
self._key = files.read(path)
[docs] def load_pem(self, path, passphrase=None):
"""Load a PEM Private Key
Args:
path(str): The file path of the private key to be loaded.
passphrase (str, optional): The passphrase.
Defaults to None.
"""
self.load(path, "PEM", passphrase)
[docs] def load_der(self, path, passphrase=None):
"""Load a DER Private Key
Args:
path(str): The file path of the private key to be loaded.
passphrase (str, optional): The passphrase.
"""
self.load(path, "DER", passphrase)
# Encode
def _encode(
self,
encoding=None,
file_format=None,
passphrase=None,
):
"""Encode the private key to a given format
Notes:
* SSH format requires PEM encoding.
* PKCS8 is the default (Traditional openSSL style is kept as legacy)
Args:
encoding (str, optional): Encoding PEM, DER or OpenSSH.
Defaults to None.
file_format (str, optional): Format : PKCS8, PKCS1 or OpenSSH.
Defaults to None.
passphrase (str, optional): The passphrase. Only for PEM.
Defaults to None.
Returns:
bytes: The encoded and formatted key.
"""
# Defaults
if encoding is None:
encoding = self._config.encoding
if file_format is None:
file_format = self._config.file_format
# Encode
data = self._key.private_bytes(
encoding=utils.file_encoding(encoding),
format=utils.private_format(file_format),
encryption_algorithm=utils.private_alg(passphrase),
)
return data
# Save
[docs] def save(
self,
path,
encoding=None,
file_format=None,
passphrase=None,
file_mode=None,
force=False,
):
"""Save the private key
Args:
path (str): The file path where the private key will be saved.
encoding (str, optional): Encoding PEM, DER or OpenSSH.
Defaults to None.
file_format (str, optional): Format : PKCS8, PKCS or OpenSSH.
Defaults to None.
passphrase (str, optional): The passphrase.
Defaults to None.
file_mode (byte, optional): The file mode (chmod).
Defaults to None.
force (bool, optional): Force to replace file if already exists.
Defaults to False.
Returns:
bool: True if successful. False if already exists and not forced
to overwrite.
"""
# encode
data = self._encode(encoding, file_format, passphrase)
# early return no overwriting if exists and not force
if files.file_exists(path) and (not force):
return False
# write the key content
if encoding in ["OpenSSH"]:
files.write(path, data, istext=True)
else:
files.write(path, data)
# set the chmod
if file_mode is not None:
files.set_chmod(path, file_mode)
else:
files.set_chmod(path, self._config.file_mode)
# return the filepath
return True
[docs] def save_pem(
self,
path,
file_format=None,
passphrase=None,
file_mode=None,
force=False,
):
"""Save a PEM private key
Args:
path (str): The file path where the private key will be saved.
file_format (str, optional): Format : PKCS8, PKCS1 or OpenSSH.
Defaults to None.
passphrase (str, optional): The passphrase.
Defaults to None.
file_mode (byte, optional): The file mode (chmod).
Defaults to None.
force (bool, optional): Force to replace file if already exists.
Defaults to False.
Returns:
bool: True if successful. False if already exists and not forced
to overwrite.
"""
return self.save(path, "PEM", file_format, passphrase, file_mode, force)
[docs] def save_der(
self,
path,
file_format=None,
passphrase=None,
file_mode=None,
force=False,
):
"""Save a DER private key
Args:
path (str): The file path where the private key will be saved.
file_format (str, optional): Format : PKCS8, PKCS1 or OpenSSH.
Defaults to None.
passphrase (str, optional): The passphrase.
Defaults to None.
file_mode (byte, optional): The file mode (chmod).
Defaults to None.
force (bool, optional): Force to replace file if already exists.
Defaults to False.
Returns:
bool: True if successful. False if already exists and not forced
to overwrite.
"""
return self.save(path, "DER", file_format, passphrase, file_mode, force)
@property
def key(self):
"""Get the key attribute
Returns:
Cryptography Private Key: An instance of an alg PrivateKey from Cryptography
(e.g. RSAPrivateKey).
"""
return self._key
@key.setter
def key(self, key):
"""Set the key with a pre-existing Cryptography Private Key
Args:
key (Cryptography Private Key): An instance of an alg PrivateKey
from Cryptography.
"""
self._key = key
@property
def keytext(self):
"""Returns the key in PEM PKCS8 format
Returns:
str: the key.
"""
encoded = self._encode("PEM", "PKCS8")
return encoded.decode("UTF-8")
# Decryption
[docs] def decrypt(
self,
ciphertext,
padding=None,
text=False,
):
"""Decrypt the encrypted message using the private key
The decrypted message can be represented in binary or text format.
If text, it is decoded to UTF-8.
Args:
ciphertext (base64): The ciphertext to decrypt in base 64.
padding (AsymmetricPadding, optional): An instance of AsymmetricPadding.
Defaults to None.
text (bool, optional): Flag indicating if the output should be treated
as text.
Defaults to False.
Returns:
bytes or str: The plaintext.
"""
# Defaults
if padding is None:
padding = utils.oaep_mgf1_padding(self._config.hash_alg)
# Decode from Base64 input
bdecoded = base64.b64decode(ciphertext)
# Decrypt
decrypted = self.key.decrypt(bdecoded, padding)
# Return str or bytes
if text:
return decrypted.decode("utf-8")
else:
return decrypted
# signature
[docs] def sign(
self,
message,
hash_alg=None,
padding=None,
pre_hashed=False,
):
"""Sign the message using the private key
The message to sign is represented in binary or text format.
If text, it is encoded in UTF-8.
Supports RSA, DSA, ED448, ED25519, Elliptic Curve (with ECDSA) Private Keys.
Args:
message (bytes or str): The message to sign.
hash_alg (str) – the hash algorithm.
Defaults to None.
padding (AsymmetricPadding, optional): An instance of AsymmetricPadding.
Not in DSA.
Defaults to None.
pre_hashed (bool, optional): Flag indicating the the message is a digest
from pre-hashed values (message too large).
Defaults to False
Returns:
str: The signature in base64 format.
"""
# Defaults
if hash_alg is None:
hash_alg = self._config.hash_alg
if padding is None:
padding = utils.pss_mgf1_padding(hash_alg)
# Treat the message as str or bytes
if isinstance(message, str):
msg = message.encode("utf-8")
else:
msg = message
# Obtain the hash algorithm
if pre_hashed:
alg = asymutils.Prehashed(utils.hash_algorithm(hash_alg))
else:
alg = utils.hash_algorithm(hash_alg)
# Sign the message
if isinstance(self.key, rsa.RSAPrivateKey):
signature = self.key.sign(msg, padding, alg)
elif isinstance(self.key, dsa.DSAPrivateKey):
signature = self.key.sign(msg, alg)
elif isinstance(self.key, ed448.Ed448PrivateKey):
signature = self.key.sign(msg)
elif isinstance(self.key, ed25519.Ed25519PrivateKey):
signature = self.key.sign(msg)
elif isinstance(self.key, ec.EllipticCurvePrivateKey):
signature = self.key.sign(msg, ec.ECDSA(alg))
else:
# NOTIMPLEMENTED
return None
# Return a base 64 representation of the signature
return base64.b64encode(signature)