diff --git a/duniterpy/key/base58.py b/duniterpy/key/base58.py index 5e8a67147da602a72c93f72704d47634c719d131..a4f97a8d3e63c4d5bc37bfaf77d62c31e9ee2802 100644 --- a/duniterpy/key/base58.py +++ b/duniterpy/key/base58.py @@ -1,12 +1,25 @@ +from typing import Union + import base58 + from ..helpers import ensure_str, ensure_bytes class Base58Encoder(object): @staticmethod - def encode(data): + def encode(data: Union[str, bytes]) -> str: + """ + Return Base58 string from data + + :param data: Bytes or string data + """ return ensure_str(base58.b58encode(ensure_bytes(data))) @staticmethod - def decode(data): + def decode(data: str) -> bytes: + """ + Decode Base58 string data and return bytes + + :param data: Base58 string + """ return base58.b58decode(data) diff --git a/duniterpy/key/encryption_key.py b/duniterpy/key/encryption_key.py index 4caa2aeb6f3c449c967df2bbd1b208dd408f8ecd..1ee32fe54d1d9a88ce518319651472cce97da77c 100644 --- a/duniterpy/key/encryption_key.py +++ b/duniterpy/key/encryption_key.py @@ -3,13 +3,14 @@ duniter public and private keys @author: inso """ +from typing import Union import libnacl.public from pylibscrypt import scrypt + from .base58 import Base58Encoder from ..helpers import ensure_bytes - SEED_LENGTH = 32 # Length of the key crypto_sign_BYTES = 64 SCRYPT_PARAMS = {'N': 4096, @@ -19,36 +20,83 @@ SCRYPT_PARAMS = {'N': 4096, class SecretKey(libnacl.public.SecretKey): - def __init__(self, salt, password): + """ + Raw Public Key Encryption Class + """ + + def __init__(self, salt: Union[str, bytes], password: Union[str, bytes]) -> None: + """ + Create SecretKey key pair instance from salt and password credentials + + :param salt: Salt credential + :param password: Password credential + """ salt = ensure_bytes(salt) password = ensure_bytes(password) seed = scrypt(password, salt, - SCRYPT_PARAMS['N'], SCRYPT_PARAMS['r'], SCRYPT_PARAMS['p'], - SEED_LENGTH) + SCRYPT_PARAMS['N'], SCRYPT_PARAMS['r'], SCRYPT_PARAMS['p'], + SEED_LENGTH) super().__init__(seed) self.public_key = PublicKey(Base58Encoder.encode(self.pk)) - def encrypt(self, pubkey, noonce, text): + def encrypt(self, pubkey: str, nonce: Union[str, bytes], text: Union[str, bytes]) -> str: + """ + Encrypt message text with the public key of the recipient and a noonce + + The nonce must be a 24 character string (you can use libnacl.utils.rand_nonce() to get one) + and unique for each encrypted message. + + Return base58 encoded encrypted message + + :param pubkey: Base58 encoded public key of the recipient + :param nonce: Unique nonce + :param text: Message to encrypt + :return: + """ text_bytes = ensure_bytes(text) - noonce_bytes = ensure_bytes(noonce) + nonce_bytes = ensure_bytes(nonce) recipient_pubkey = PublicKey(pubkey) - crypt_bytes = libnacl.public.Box(self, recipient_pubkey).encrypt(text_bytes, noonce_bytes) + crypt_bytes = libnacl.public.Box(self, recipient_pubkey).encrypt(text_bytes, nonce_bytes) return Base58Encoder.encode(crypt_bytes[24:]) - def decrypt(self, pubkey, noonce, text): + def decrypt(self, pubkey: str, nonce: Union[str, bytes], text: str) -> str: + """ + Decrypt encrypted message text with recipient public key and the unique nonce used by the sender. + + :param pubkey: Public key of the recipient + :param nonce: Unique nonce used by the sender + :param text: Encrypted message + :return: + """ sender_pubkey = PublicKey(pubkey) - noonce_bytes = ensure_bytes(noonce) + nonce_bytes = ensure_bytes(nonce) encrypt_bytes = Base58Encoder.decode(text) - decrypt_bytes = libnacl.public.Box(self, sender_pubkey).decrypt(encrypt_bytes, noonce_bytes) + decrypt_bytes = libnacl.public.Box(self, sender_pubkey).decrypt(encrypt_bytes, nonce_bytes) return decrypt_bytes.decode('utf-8') class PublicKey(libnacl.public.PublicKey): - def __init__(self, pubkey): + def __init__(self, pubkey: str) -> None: + """ + Create instance of libnacl ed25519 sign PublicKey from a base58 public key + + :param pubkey: Base58 public key + """ key = Base58Encoder.decode(pubkey) super().__init__(key) - def base58(self): + def base58(self) -> str: + """ + Return a base58 encoded string of the public key + """ return Base58Encoder.encode(self.pk) + def encrypt_seal(self, message: Union[str, bytes]) -> bytes: + """ + Encrypt message with a curve25519 version of the ed25519 public key + + :param message: Message to encrypt + """ + curve25519_public_key = libnacl.crypto_sign_ed25519_pk_to_curve25519(self.pk) + return libnacl.crypto_box_seal(ensure_bytes(message), curve25519_public_key) diff --git a/duniterpy/key/signing_key.py b/duniterpy/key/signing_key.py index aac85a22d216bed37ed7b7122d8e7acc1804a199..ae5b2384e3e68e0c855bf5ff2843ae06a89f5dc6 100644 --- a/duniterpy/key/signing_key.py +++ b/duniterpy/key/signing_key.py @@ -3,9 +3,11 @@ duniter public and private keys @author: inso """ +from typing import Optional import libnacl.sign from pylibscrypt import scrypt + from .base58 import Base58Encoder from ..helpers import ensure_bytes @@ -14,27 +16,27 @@ crypto_sign_BYTES = 64 class ScryptParams: - def __init__(self, N, r, p): + def __init__(self, n: int, r: int, p: int) -> None: """ Init a ScryptParams instance with crypto parameters - :param int N: scrypt param N - :param int r: scrypt param r - :param int p: scrypt param p + :param n: scrypt param N + :param r: scrypt param r + :param p: scrypt param p """ - self.N = N + self.N = n self.r = r self.p = p class SigningKey(libnacl.sign.Signer): - def __init__(self, salt, password, scrypt_params=None): + def __init__(self, salt: str, password: str, scrypt_params: Optional[ScryptParams] = None) -> None: """ Init a SigningKey object from credentials - :param str salt: Secret salt passphrase credential - :param str password: Secret password credential - :param ScryptParams scrypt_params: ScryptParams instance + :param salt: Secret salt passphrase credential + :param password: Secret password credential + :param scrypt_params: ScryptParams instance """ if scrypt_params is None: scrypt_params = ScryptParams(4096, 16, 1) @@ -47,3 +49,15 @@ class SigningKey(libnacl.sign.Signer): super().__init__(seed) self.pubkey = Base58Encoder.encode(self.vk) + + def decrypt_seal(self, message: bytes) -> str: + """ + Decrypt message with a curve25519 version of the ed25519 key pair + + :param message: Encrypted message + + :return: + """ + curve25519_public_key = libnacl.crypto_sign_ed25519_pk_to_curve25519(self.vk) + curve25519_secret_key = libnacl.crypto_sign_ed25519_sk_to_curve25519(self.sk) + return libnacl.crypto_box_seal_open(message, curve25519_public_key, curve25519_secret_key).decode('utf-8') diff --git a/examples/decrypt_binary_encrypted_message.py b/examples/decrypt_binary_encrypted_message.py new file mode 100644 index 0000000000000000000000000000000000000000..4da1ff4f155efb82f3a0bebfcdc118237597d54f --- /dev/null +++ b/examples/decrypt_binary_encrypted_message.py @@ -0,0 +1,37 @@ +import getpass +import sys + +from duniterpy.key import SigningKey + +if __name__ == '__main__': + + if len(sys.argv) < 2: + print(""" + Usage: + python decrypt_message.py ENCRYPTED_MESSAGE_FILEPATH + """) + + # capture encrypted message filepath argument + signed_message_path = sys.argv[1] + + # prompt hidden user entry + salt = getpass.getpass("Enter your passphrase (salt): ") + + # prompt hidden user entry + password = getpass.getpass("Enter your password: ") + + # Create key object + signing_key_instance = SigningKey(salt, password) + + # open encrypted message file + with open(signed_message_path, 'rb') as file_handler: + encrypted_message = file_handler.read() + + # Decrypt the message! + try: + message = signing_key_instance.decrypt_seal(encrypted_message) + print("Decrypted message:") + except ValueError as error: + message = str(error) + + print(message) diff --git a/examples/save_binary_encrypted_message.py b/examples/save_binary_encrypted_message.py new file mode 100644 index 0000000000000000000000000000000000000000..dd2e510464ecc1eb870ea317e7b4723b7af1a154 --- /dev/null +++ b/examples/save_binary_encrypted_message.py @@ -0,0 +1,22 @@ +from duniterpy.key import PublicKey + +################################################ + +ENCRYPTED_MESSAGE_FILENAME = 'duniter_encrypted_message.bin' + +if __name__ == '__main__': + # Ask public key of the recipient + pubkeyBase58 = input("Enter public key of the message recipient: ") + + # Enter the message + message = input("Enter your message: ") + + # Encrypt the message, only the recipient secret key will be able to decrypt the message + pubkey_instance = PublicKey(pubkeyBase58) + encrypted_message = pubkey_instance.encrypt_seal(message) + + # Save encrypted message in a file + with open(ENCRYPTED_MESSAGE_FILENAME, 'wb') as file_handler: + file_handler.write(encrypted_message) + + print("Encrypted message saved in file ./{0}".format(ENCRYPTED_MESSAGE_FILENAME))