# -*- coding: utf-8 -*-
"""publickey.py - Public Key : generate, save, load, encrypt, verify
Class:
* PublicKey
"""
import base64
from cryptography.exceptions import InvalidSignature
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 PublicKeyConfig
[docs]class PublicKey(Base):
"""Public Key Class - extends Base
Usage:
* initialize : pubk = PublicKey(PublicKey()) or pk = PublicKey()
or pubk = PublicKey(private_key=PrivateKey())
* generate the key: pubk.gen()
* save key: pubk.save(filepath)
* load key: pubk.load(filepath)
* decrypt: pubk.decrypt(ciphertext)
* verify: pubk.verify(signature, message)
"""
def __init__(self, **kwargs):
"""PublicKey class initiator
Args:
config (PublicKeyConfig, optional): The configuration.
key (Cryptography PublicKey, optional) : The public key.
An instance of Cryptography PublicKey.
Defaults to None.
private_key (PrivateKey, optional): The private key.
An instance of PrivateKey.
Defaults to None.
"""
super().__init__(**kwargs)
# configuration
if not hasattr(self, "config"):
self._config = kwargs.pop("config", PublicKeyConfig())
# key object (cryptography compatible)
if not hasattr(self, "key"):
self._key = kwargs.pop("key", None)
# private_key object
if not hasattr(self, "private_key"):
self._private_key = kwargs.pop("private_key", None)
# Generate
[docs] def gen(self, alg=None, private_key=None):
"""Generate the Public Key
Args:
alg (str): The key algorithm. RSA, EC, ED448, ED25519 and DSA
are supported.
Defaults to None.
private_key (PrivateKey, optional): The private key.
An instance of PrivateKey.
Defaults to None.
"""
# Defaults
if alg is None:
alg = self._config.alg.upper()
if private_key is not None:
self._private_key = private_key
# Generate the public key based on the algorithm
if alg == "RSA":
self._key = self.private_key.key.public_key()
elif alg == "DSA":
self._key = self.private_key.key.public_key()
elif alg == "ED448":
self._key = self.private_key.key.public_key()
elif alg == "ED25519":
self._key = self.private_key.key.public_key()
elif alg == "EC":
self._key = self.private_key.key.public_key()
else:
# Not implemented - Tries to read public_key()
self._key = self.private_key.key.public_key()
# Load
[docs] def load(
self,
path,
encoding=None,
):
"""Load the public key
Args:
path(str): The file path of the public key to be loaded.
encoding (str, optional): Encoding PEM, DER, openSSH, X962, SMIME.
Defaults to None.
"""
# Default encoding
if encoding is None:
encoding = self._config.encoding
# serialize based on encoding
if encoding == "PEM":
lines = files.read(path)
self._key = serialization.load_pem_public_key(lines)
elif encoding == "DER":
lines = files.read(path)
self._key = serialization.load_der_public_key(lines)
elif encoding == "OpenSSH":
lines = files.read(path)
self._key = serialization.load_ssh_public_key(lines)
elif encoding == "X962":
self._key = files.read(path)
elif encoding == "SMIME":
self._key = files.read(path, istext=True)
else:
self._key = files.read(path)
[docs] def load_pem(self, path):
"""Load a PEM Public Key
Args:
path(str): The file path of the public key to be loaded.
"""
self.load(path, "PEM")
[docs] def load_der(self, path):
"""Load a DER Public Key
Args:
path(str): The file path of the public key to be loaded.
"""
self.load(path, "DER")
# Encode
def _encode(
self,
encoding=None,
file_format=None,
):
"""Encode the public key to a given format
Notes:
* SSH format requires PEM encoding.
* Default SubjectPublicKeyInfo format (None) requires PEM or DER 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 : SubjectPublicKeyInfo, PKCS1
or OpenSSH.
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.public_bytes(
encoding=utils.file_encoding(encoding),
format=utils.public_format(file_format),
)
return data
# Save
[docs] def save(
self,
path,
encoding=None,
file_format=None,
file_mode=None,
force=False,
):
"""Save the public key to file
Notes:
* SSH format requires PEM encoding.
* Default SubjectPublicKeyInfo format (None) requires PEM or DER encoding
* PKCS8 is the default (Traditional openSSL style is kept as legacy)
Args:
path (str): The file path where the public key will be saved.
encoding (str, optional): Encoding PEM, DER or OpenSSH.
Defaults to None.
file_format (str, optional): Format : SubjectPublicKeyInfo, PKCS1
or OpenSSH.
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.
"""
# encoding
data = self._encode(encoding, file_format)
# early return no overwriting if exists and not forced
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,
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 : SubjectPublicKeyInfo, PKCS1
or OpenSSH.
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, file_mode, force)
[docs] def save_der(
self,
path,
file_format=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 : SubjectPublicKeyInfo, PKCS1
or OpenSSH.
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, file_mode, force)
@property
def key(self):
"""Get the key attribute
Returns:
Cryptography Public Key: An instance of PublicKey from Cryptography.
"""
return self._key
@key.setter
def key(self, key):
"""Set the key with a pre-existing Cryptography Public Key
Args:
key (Cryptography Public Key): An instance of PublicKey from Cryptography.
"""
self._key = key
@property
def private_key(self):
"""Get the private_key attribute
Returns:
PrivateKey : An instance of PrivateKey.
"""
return self._private_key
@private_key.setter
def private_key(self, key):
"""Set the key with a pre-existing private key
Args:
key (PrivateKey): An instance of PrivateKey.
"""
self._private_key = key
@property
def keytext(self):
"""Returns the key in PEM SubjectPublicKeyInfo format
Returns:
str: the key.
"""
encoded = self._encode("PEM", "SubjectPublicKeyInfo")
return encoded.decode("UTF-8")
# Encrypt
[docs] def encrypt(self, plaintext, padding=None):
"""Encrypt the message using the public key
The plaintext can be binary or text format.
If text, it is encoded in UTF-8.
Args:
plaintext(bytes or str): The plaintext to encrypt.
padding(AsymmetricPadding, optional): An instance of AsymmetricPadding.
Defaults to None.
Returns:
base64: The encrypted message in base 64 format.
"""
# Defaults
if padding is None:
padding = utils.oaep_mgf1_padding(self._config.hash_alg)
else:
padding = None
# Input as bytes or str
if isinstance(plaintext, str):
msg = plaintext.encode("utf-8")
else:
msg = plaintext
# generate and return the encrypted text in base 64 format
return base64.b64encode(self._key.encrypt(msg, padding))
# Verify
[docs] def verify(
self,
signature,
message,
hash_alg=None,
padding=None,
pre_hashed=False,
):
"""Sign the message using the public key
The message to verify can be binary or text format.
If text, it is encoded in UTF-8.
Supports RSA, DSA, ED448, ED25519, Elliptic Curve (with ECDSA) Private Keys.
Args:
signature (base64): The signature in base64 format.
message (bytes or str): The message to verify.
hash_alg (str, optional) – 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.
Raises:
bool: False if the signature does not validate, else True.
"""
# Defaults
if hash_alg is None:
hash_alg = self._config.hash_alg
if padding is None:
padding = utils.pss_mgf1_padding(hash_alg)
# Decode the signature from base64 format
sig = base64.b64decode(signature)
verif = None
try:
# handles both str and bytes
if isinstance(message, str):
msg = message.encode("utf-8")
else:
msg = message
# pick the correct algorithm
if pre_hashed:
alg = asymutils.Prehashed(utils.hash_algorithm(hash_alg))
else:
alg = utils.hash_algorithm(hash_alg)
if isinstance(self.key, rsa.RSAPublicKey):
self._key.verify(sig, msg, padding, alg)
elif isinstance(self.key, dsa.DSAPublicKey):
self._key.verify(sig, msg, alg)
elif isinstance(self.key, ed448.Ed448PublicKey):
self._key.verify(sig, msg)
elif isinstance(self.key, ed25519.Ed25519PublicKey):
self._key.verify(sig, msg)
elif isinstance(self.key, ec.EllipticCurvePublicKey):
self._key.verify(sig, msg, ec.ECDSA(alg))
else:
# NOTIMPLEMENTED
return None
verif = True
except InvalidSignature:
verif = False
return verif