mirror of https://github.com/nucypher/pyUmbral.git
362 lines
12 KiB
Python
362 lines
12 KiB
Python
import os
|
|
import base64
|
|
from typing import Callable
|
|
|
|
|
|
from nacl.secret import SecretBox
|
|
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.backends.openssl.ec import (
|
|
_EllipticCurvePublicKey, _EllipticCurvePrivateKey
|
|
)
|
|
from cryptography.hazmat.primitives import hashes
|
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
|
|
|
|
from umbral.config import default_params
|
|
from umbral.point import Point
|
|
from umbral.bignum import BigNum
|
|
from umbral.params import UmbralParameters
|
|
|
|
|
|
class UmbralPrivateKey(object):
|
|
def __init__(self, bn_key: BigNum, params: UmbralParameters=None):
|
|
"""
|
|
Initializes an Umbral private key.
|
|
"""
|
|
if params is None:
|
|
params = default_params()
|
|
|
|
self.params = params
|
|
self.bn_key = bn_key
|
|
|
|
@classmethod
|
|
def gen_key(cls, params: UmbralParameters=None):
|
|
"""
|
|
Generates a private key and returns it.
|
|
"""
|
|
if params is None:
|
|
params = default_params()
|
|
|
|
bn_key = BigNum.gen_rand(params.curve)
|
|
return cls(bn_key, params)
|
|
|
|
@classmethod
|
|
def from_bytes(cls, key_bytes: bytes, params: UmbralParameters=None,
|
|
password: bytes=None, _scrypt_cost: int=20,
|
|
decoder: Callable=None):
|
|
"""
|
|
Loads an Umbral private key from bytes.
|
|
Optionally, allows a decoder function to be passed as a param to decode
|
|
the data provided before converting to an Umbral key.
|
|
Optionally, if a password is provided it will decrypt the key using
|
|
nacl's Salsa20-Poly1305 and Scrypt key derivation.
|
|
|
|
WARNING: RFC7914 recommends that you use a 2^20 cost value for sensitive
|
|
files. Unless you changed this when you called `to_bytes`, you should
|
|
not change it here. It is NOT recommended to change the `_scrypt_cost`
|
|
value unless you know what you're doing.
|
|
"""
|
|
if params is None:
|
|
params = default_params()
|
|
|
|
if decoder:
|
|
key_bytes = decoder(key_bytes)
|
|
|
|
if password:
|
|
salt = key_bytes[-16:]
|
|
key_bytes = key_bytes[:-16]
|
|
|
|
key = Scrypt(
|
|
salt=salt,
|
|
length=SecretBox.KEY_SIZE,
|
|
n=2**_scrypt_cost,
|
|
r=8,
|
|
p=1,
|
|
backend=default_backend()
|
|
).derive(password)
|
|
|
|
key_bytes = SecretBox(key).decrypt(key_bytes)
|
|
|
|
bn_key = BigNum.from_bytes(key_bytes, params.curve)
|
|
return cls(bn_key, params)
|
|
|
|
def to_bytes(self, password: bytes=None, _scrypt_cost: int=20,
|
|
encoder: Callable=None):
|
|
"""
|
|
Returns an Umbral private key as bytes optional symmetric encryption
|
|
via nacl's Salsa20-Poly1305 and Scrypt key derivation. If a password
|
|
is provided, the user must encode it to bytes.
|
|
Optionally, allows an encoder to be passed in as a param to encode the
|
|
data before returning it.
|
|
|
|
WARNING: RFC7914 recommends that you use a 2^20 cost value for sensitive
|
|
files. It is NOT recommended to change the `_scrypt_cost` value unless
|
|
you know what you are doing.
|
|
"""
|
|
umbral_privkey = self.bn_key.to_bytes()
|
|
|
|
if password:
|
|
salt = os.urandom(16)
|
|
|
|
key = Scrypt(
|
|
salt=salt,
|
|
length=SecretBox.KEY_SIZE,
|
|
n=2**_scrypt_cost,
|
|
r=8,
|
|
p=1,
|
|
backend=default_backend()
|
|
).derive(password)
|
|
|
|
umbral_privkey = SecretBox(key).encrypt(umbral_privkey)
|
|
umbral_privkey += salt
|
|
|
|
if encoder:
|
|
umbral_privkey = encoder(umbral_privkey)
|
|
|
|
return umbral_privkey
|
|
|
|
def get_pubkey(self):
|
|
"""
|
|
Calculates and returns the public key of the private key.
|
|
"""
|
|
return UmbralPublicKey(self.bn_key * self.params.g)
|
|
|
|
def to_cryptography_privkey(self):
|
|
"""
|
|
Returns a cryptography.io EllipticCurvePrivateKey from the Umbral key.
|
|
"""
|
|
backend = default_backend()
|
|
|
|
backend.openssl_assert(self.bn_key.group != backend._ffi.NULL)
|
|
backend.openssl_assert(self.bn_key.bignum != backend._ffi.NULL)
|
|
|
|
ec_key = backend._lib.EC_KEY_new()
|
|
backend.openssl_assert(ec_key != backend._ffi.NULL)
|
|
ec_key = backend._ffi.gc(ec_key, backend._lib.EC_KEY_free)
|
|
|
|
set_group_result = backend._lib.EC_KEY_set_group(
|
|
ec_key, self.bn_key.group
|
|
)
|
|
backend.openssl_assert(set_group_result == 1)
|
|
|
|
set_privkey_result = backend._lib.EC_KEY_set_private_key(
|
|
ec_key, self.bn_key.bignum
|
|
)
|
|
backend.openssl_assert(set_privkey_result == 1)
|
|
|
|
# Get public key
|
|
point = backend._lib.EC_POINT_new(self.bn_key.group)
|
|
backend.openssl_assert(point != backend._ffi.NULL)
|
|
point = backend._ffi.gc(point, backend._lib.EC_POINT_free)
|
|
|
|
with backend._tmp_bn_ctx() as bn_ctx:
|
|
mult_result = backend._lib.EC_POINT_mul(
|
|
self.bn_key.group, point, self.bn_key.bignum, backend._ffi.NULL,
|
|
backend._ffi.NULL, bn_ctx
|
|
)
|
|
backend.openssl_assert(mult_result == 1)
|
|
|
|
set_pubkey_result = backend._lib.EC_KEY_set_public_key(ec_key, point)
|
|
backend.openssl_assert(set_pubkey_result == 1)
|
|
|
|
evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key)
|
|
return _EllipticCurvePrivateKey(backend, ec_key, evp_pkey)
|
|
|
|
|
|
class UmbralPublicKey(object):
|
|
def __init__(self, point_key, params: UmbralParameters=None):
|
|
"""
|
|
Initializes an Umbral public key.
|
|
"""
|
|
if params is None:
|
|
params = default_params()
|
|
|
|
self.params = params
|
|
|
|
if not isinstance(point_key, Point):
|
|
raise TypeError("point_key can only be a Point. Don't pass anything else.")
|
|
|
|
self.point_key = point_key
|
|
|
|
@classmethod
|
|
def from_bytes(cls, key_bytes: bytes, params: UmbralParameters=None,
|
|
decoder: Callable=None):
|
|
"""
|
|
Loads an Umbral public key from bytes.
|
|
Optionally, if an decoder function is provided it will be used to decode
|
|
the data before returning it as an Umbral key.
|
|
"""
|
|
if params is None:
|
|
params = default_params()
|
|
|
|
if decoder:
|
|
key_bytes = decoder(key_bytes)
|
|
|
|
point_key = Point.from_bytes(key_bytes, params.curve)
|
|
return cls(point_key, params)
|
|
|
|
def to_bytes(self, encoder: Callable=None):
|
|
"""
|
|
Returns an Umbral public key as bytes.
|
|
Optionally, if an encoder function is provided it will be used to encode
|
|
the data before returning it.
|
|
"""
|
|
umbral_pubkey = self.point_key.to_bytes()
|
|
|
|
if encoder:
|
|
umbral_pubkey = encoder(umbral_pubkey)
|
|
|
|
return umbral_pubkey
|
|
|
|
def get_pubkey(self):
|
|
raise NotImplementedError
|
|
|
|
def to_cryptography_pubkey(self):
|
|
"""
|
|
Returns a cryptography.io EllipticCurvePublicKey from the Umbral key.
|
|
"""
|
|
backend = default_backend()
|
|
|
|
backend.openssl_assert(self.point_key.group != backend._ffi.NULL)
|
|
backend.openssl_assert(self.point_key.ec_point != backend._ffi.NULL)
|
|
|
|
ec_key = backend._lib.EC_KEY_new()
|
|
backend.openssl_assert(ec_key != backend._ffi.NULL)
|
|
ec_key = backend._ffi.gc(ec_key, backend._lib.EC_KEY_free)
|
|
|
|
set_group_result = backend._lib.EC_KEY_set_group(
|
|
ec_key, self.point_key.group
|
|
)
|
|
backend.openssl_assert(set_group_result == 1)
|
|
|
|
set_pubkey_result = backend._lib.EC_KEY_set_public_key(
|
|
ec_key, self.point_key.ec_point
|
|
)
|
|
backend.openssl_assert(set_pubkey_result == 1)
|
|
|
|
evp_pkey = backend._ec_cdata_to_evp_pkey(ec_key)
|
|
return _EllipticCurvePublicKey(backend, ec_key, evp_pkey)
|
|
|
|
def __bytes__(self):
|
|
"""
|
|
Returns an Umbral Public key as a bytestring.
|
|
"""
|
|
return self.point_key.to_bytes()
|
|
|
|
def __repr__(self):
|
|
return "{}:{}".format(self.__class__, self.point_key.to_bytes().hex()[:15])
|
|
|
|
def __eq__(self, other):
|
|
if type(other) == bytes:
|
|
is_eq = bytes(other) == bytes(self)
|
|
elif hasattr(other, "point_key"):
|
|
is_eq = self.point_key == other.point_key
|
|
else:
|
|
is_eq = False
|
|
return is_eq
|
|
|
|
def __hash__(self):
|
|
return int.from_bytes(self, byteorder="big")
|
|
|
|
|
|
class UmbralKeyingMaterial(object):
|
|
"""
|
|
This class handles keying material for Umbral, by allowing deterministic
|
|
derivation of UmbralPrivateKeys based on labels.
|
|
Don't use this key material directly as a key.
|
|
|
|
"""
|
|
|
|
def __init__(self, keying_material: bytes=None):
|
|
"""
|
|
Initializes an UmbralKeyingMaterial.
|
|
"""
|
|
if keying_material:
|
|
if len(keying_material) < 32:
|
|
raise ValueError("UmbralKeyingMaterial must have size at least 32 bytes.")
|
|
self.keying_material = keying_material
|
|
else:
|
|
self.keying_material = os.urandom(64)
|
|
|
|
def derive_privkey_by_label(self, label: bytes, salt: bytes=None,
|
|
params: UmbralParameters=None):
|
|
"""
|
|
Derives an UmbralPrivateKey using a KDF from this instance of
|
|
UmbralKeyingMaterial, a label, and an optional salt.
|
|
"""
|
|
params = params if params is not None else default_params()
|
|
|
|
key_material = HKDF(
|
|
algorithm=hashes.BLAKE2b(64),
|
|
length=64,
|
|
salt=salt,
|
|
info=b"NuCypherKMS/KeyDerivation/"+label,
|
|
backend=default_backend()
|
|
).derive(self.keying_material)
|
|
|
|
bn_key = BigNum.hash_to_bn(key_material, params=params)
|
|
return UmbralPrivateKey(bn_key, params)
|
|
|
|
@classmethod
|
|
def from_bytes(cls, key_bytes: bytes, password: bytes=None, _scrypt_cost: int=20):
|
|
"""
|
|
Loads an UmbralKeyingMaterial from a urlsafe base64 encoded string.
|
|
Optionally, if a password is provided it will decrypt the key using
|
|
nacl's Salsa20-Poly1305 and Scrypt key derivation.
|
|
|
|
WARNING: RFC7914 recommends that you use a 2^20 cost value for sensitive
|
|
files. Unless you changed this when you called `to_bytes`, you should
|
|
not change it here. It is NOT recommended to change the `_scrypt_cost`
|
|
value unless you know what you're doing.
|
|
"""
|
|
|
|
if password:
|
|
salt = key_bytes[-16:]
|
|
key_bytes = key_bytes[:-16]
|
|
|
|
key = Scrypt(
|
|
salt=salt,
|
|
length=SecretBox.KEY_SIZE,
|
|
n=2**_scrypt_cost,
|
|
r=8,
|
|
p=1,
|
|
backend=default_backend()
|
|
).derive(password)
|
|
|
|
key_bytes = SecretBox(key).decrypt(key_bytes)
|
|
|
|
return cls(key_bytes)
|
|
|
|
def to_bytes(self, password: bytes=None, _scrypt_cost: int=20):
|
|
"""
|
|
Returns an UmbralKeyingMaterial as a urlsafe base64 encoded string with
|
|
optional symmetric encryption via nacl's Salsa20-Poly1305 and Scrypt
|
|
key derivation. If a password is provided, the user must encode it to
|
|
bytes.
|
|
|
|
WARNING: RFC7914 recommends that you use a 2^20 cost value for sensitive
|
|
files. It is NOT recommended to change the `_scrypt_cost` value unless
|
|
you know what you are doing.
|
|
"""
|
|
|
|
umbral_keying_material = self.keying_material
|
|
|
|
if password:
|
|
salt = os.urandom(16)
|
|
|
|
key = Scrypt(
|
|
salt=salt,
|
|
length=SecretBox.KEY_SIZE,
|
|
n=2**_scrypt_cost,
|
|
r=8,
|
|
p=1,
|
|
backend=default_backend()
|
|
).derive(password)
|
|
|
|
umbral_keying_material = SecretBox(key).encrypt(umbral_keying_material)
|
|
umbral_keying_material += salt
|
|
|
|
encoded_key = umbral_keying_material
|
|
return encoded_key
|