diff --git a/.gitignore b/.gitignore index a770529..ddbe28b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,11 @@ test test_* hmacenv __pycache__ -public_key.pem *.pem decrypted.txt encrypted.txt +signed.txt *.enc *.dec *.part +public_ecdsa_key.txt \ No newline at end of file diff --git a/README.md b/README.md index bcbbe8e..98941c0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # hmacrypt -encryption / decryption using RSA deterministic Key Derivation from password+hmac_secret by an hardware key. +encryption / decryption / signing / verifying using RSA and ECDSA a deterministic Key Derivation from password+hmac_secret by an hardware key. ## Requirements (BEFORE everything else) & Credits @@ -30,14 +30,15 @@ On Ubuntu 23.10 (untested on other platforms and flavors): ## FAQ - A .keyfile without the passphrase AND the hardware key is not usable and won't be recoverable -- Remember to always "conda activate ./hmaenv" prior to running anything from here +- Remember to always "conda activate ./hmacenv" prior to running anything from here ## Features - 2fa encryption/decryption using RSA deterministic Key Derivation +- 2fa signing/verifying using ECDSA deterministic Key Derivation - Possibility to use the same or different keyfiles to enhance security - Consequently, supports for a wide range of hardware keys as long as they are compatible with libfido2 -- Low footprint: requires a single python library and a single system library +- Low footprint: requires just two (or three) python library and a single system library - Readable: you are free and encouraged to tinker with this library ## What is this @@ -72,6 +73,8 @@ The examples provided allows everybody to use this library programmatically. You can also use the standalone tools in the root folder to encrypt / decrypt files and strings. +The tools allows you to sign and verify strings. + Please note that the tools are just an utility and may not be suited for large data inputs. ### Documentation @@ -92,11 +95,24 @@ Or the path you used for the library. *If you REALLY have to change the src directory name, please correct the various paths inside.* -#### inferKeys + +#### inferECDSAKeys Definition: - def inferKeys(hidePrivate=False, savePublic=False, keyfilePath="src/bins/.keyfile") + def inferECDSAKeys(hidePrivate=False, savePublic=False, keyfilePath="src/bins/.keyfile") + +Parameters: + +- hidePrivate (boolean, default to False); if True, does not return the private key +- savePublic (boolean, default to False); if True, saves the public key to a file +- keyfilePath (string, default to the bins path); allows the usage of different keyfiles, for example to use a different hmac_secret + +#### inferRSAKeys + +Definition: + + def inferRSAKeys(hidePrivate=False, savePublic=False, keyfilePath="src/bins/.keyfile") Parameters: @@ -104,6 +120,27 @@ Parameters: - savePublic (boolean, default to False); if True, saves the PEM encoded public key to a file - keyfilePath (string, default to the bins path); allows the usage of different keyfiles, for example to use a different hmac_secret +#### self_sign + +Definition: + + def self_sign(message): + +Parameters: + +- message (string); the message to sign + +#### self_verify + +Definition: + + def self_verify(signature, message): + +Parameters: + +- signature (bytes); the signature to verify +- message (string); the message that is supposed to be corresponding to the signature + #### self_encrypt Definition: @@ -162,6 +199,9 @@ The script encodes and encrypt a string using the same inferred RSA keypair as a The script creates, encodes and encrypts a simple text file, then decrypts it using the same keypair as above. +#### string_sign_and_verify.py + +The script sign and verify a given string using the keypair derived from the hmac_secret + password + key ## Known Issues diff --git a/examples/string_sign_and_verify.py b/examples/string_sign_and_verify.py new file mode 100644 index 0000000..79274f9 --- /dev/null +++ b/examples/string_sign_and_verify.py @@ -0,0 +1,25 @@ +#!./hmacenv/bin/python + +import src.hmacrypt as hmacrypt +import sys +import os + +# Getting and requiring exactly 1 argument +if len(sys.argv) != 2: + print("Usage: python3 string_decryptor.py ") + sys.exit(1) +stringToSign = sys.argv[1].encode() + + +signed = hmacrypt.self_sign(stringToSign) + +with open("signed.txt", "wb+") as signedFile: + signedFile.write(signed) + +print(signed) + +# Verify +with open("signed.txt", "rb") as f: + signed = f.read() +verified = hmacrypt.self_verify(signed, stringToSign) +print(verified) diff --git a/public_ecdsa_key_saver b/public_ecdsa_key_saver new file mode 100755 index 0000000..3b11f3f --- /dev/null +++ b/public_ecdsa_key_saver @@ -0,0 +1,3 @@ +#!./hmacenv/bin/python +import src.hmacrypt as hmacrypt +hmacrypt.inferECDSAKeys(hidePrivate=True, savePublic=True) \ No newline at end of file diff --git a/public_key_saver b/public_key_saver deleted file mode 100755 index 12ce8af..0000000 --- a/public_key_saver +++ /dev/null @@ -1,3 +0,0 @@ -#!./hmacenv/bin/python -import src.hmacrypt as hmacrypt -hmacrypt.inferKeys(hidePrivate=True, savePublic=True) \ No newline at end of file diff --git a/public_rsa_key_saver b/public_rsa_key_saver new file mode 100755 index 0000000..8d8c97e --- /dev/null +++ b/public_rsa_key_saver @@ -0,0 +1,3 @@ +#!./hmacenv/bin/python +import src.hmacrypt as hmacrypt +hmacrypt.inferRSAKeys(hidePrivate=True, savePublic=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a2e4211..8d09a44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ ByteSplitter pycrypto +ecdsa diff --git a/src/hmacrypt.py b/src/hmacrypt.py index 679426f..7aec5f6 100644 --- a/src/hmacrypt.py +++ b/src/hmacrypt.py @@ -1,10 +1,9 @@ import subprocess +from src.libs.seedable_rsa import decrypt, encrypt, generate_rsa_key +from src.libs.seedable_ecdsa import generate_ecdsa_key, sign, verify -from src.libs.seedable_rsa import decrypt, encrypt, generate_key - - -# INFO This method is the core of the whole process as it derives a RSA keypair from the stored secret and the hardware key -def inferKeys(hidePrivate=False, savePublic=False, keyfilePath="src/bins/.keyfile"): +# INFO This method derives the HMAC secret from the hardware key and the stored secret +def getHMACSecret(keyfilePath="src/bins/.keyfile"): """Infer keys from the secret stored in the hardware key""" hmac_secret_raw = subprocess.check_output( ["src/bins/hmac_secret_regenerate", keyfilePath] @@ -14,7 +13,40 @@ def inferKeys(hidePrivate=False, savePublic=False, keyfilePath="src/bins/.keyfil # Divide lines and keep last one hmac_secret = hmac_secret_dirty.splitlines()[-1] hmac_secret = hmac_secret.strip() - secret_key = generate_key(hmac_secret) # RSA Key (2048) derivation + return hmac_secret + +# INFO This method derives an ECDSA keypair from the stored secret and the hardware key +def inferECDSAKeys(hidePrivate=False, savePublic=False): + hmac_secret = getHMACSecret() + key_pair = generate_ecdsa_key(hmac_secret) + # We use them in memory, we never save them + # Privacy should be possible here + if hidePrivate: + private_key = "REDACTED" + else: + private_key = key_pair[0] + public_key = key_pair[1] + # Saving public key is permitted + if savePublic: + with open("public_ecdsa_key.pem", "wb+") as f: + f.write(public_key.to_pem()) + return private_key, public_key + +def self_sign(message): + """Sign a message with the private key""" + private_key, public_key = inferECDSAKeys() + signature = sign(private_key, message) + return signature + +def self_verify(signature, message): + """Verify a message with the public key""" + private_key, public_key = inferECDSAKeys(hidePrivate=True) + return verify(public_key, signature, message) + +# INFO This method derives a RSA keypair from the stored secret and the hardware key +def inferRSAKeys(hidePrivate=False, savePublic=False): + hmac_secret = getHMACSecret() + secret_key = generate_rsa_key(hmac_secret) # RSA Key (2048) derivation # We use them in memory, we never save them # Privacy should be possible here if hidePrivate: @@ -24,7 +56,7 @@ def inferKeys(hidePrivate=False, savePublic=False, keyfilePath="src/bins/.keyfil public_key = secret_key.publickey().exportKey("PEM") # Saving public key is permitted if savePublic: - with open("public_key.pem", "wb") as f: + with open("public_rsa_key.pem", "wb") as f: f.write(public_key) return private_key, public_key @@ -38,14 +70,14 @@ def inferKeys(hidePrivate=False, savePublic=False, keyfilePath="src/bins/.keyfil def self_encrypt(secret, encoded=False): """Encrypt secret with public key""" - private_key, public_key = inferKeys() + private_key, public_key = inferRSAKeys(hidePrivate=True) secret = encrypt(secret, public_key, encoded) return secret def self_decrypt(encrypted): """Decrypt secret with private key""" - private_key, public_key = inferKeys() + private_key, public_key = inferRSAKeys() secret = decrypt(encrypted, private_key) return secret @@ -55,7 +87,7 @@ def self_decrypt(encrypted): def self_encrypt_file(filepath, outpath): """Encrypt file with public key""" - private_key, public_key = inferKeys(hidePrivate=True) + private_key, public_key = inferRSAKeys(hidePrivate=True) with open(filepath, "rb") as f: filebytes = f.read() encrypted = encrypt(filebytes, public_key, encoded=True) @@ -66,7 +98,7 @@ def self_encrypt_file(filepath, outpath): def self_decrypt_file(filepath, outpath): """Decrypt file with private key""" - private_key, public_key = inferKeys() + private_key, public_key = inferRSAKeys() with open(filepath, "rb") as f: filebytes = f.read() decrypted = decrypt(filebytes, private_key) @@ -80,7 +112,7 @@ def self_decrypt_file(filepath, outpath): # Self testing if __name__ == "__main__": - private_key, public_key = inferKeys() + private_key, public_key = inferRSAKeys() secret = encrypt("secret message", public_key) print(secret) decrypted = decrypt(secret, private_key) diff --git a/src/libs/seedable_ecdsa.py b/src/libs/seedable_ecdsa.py new file mode 100644 index 0000000..133b5b6 --- /dev/null +++ b/src/libs/seedable_ecdsa.py @@ -0,0 +1,26 @@ +import ecdsa +from hashlib import sha256 + +# Sign +def sign(sk, message): + """Sign a message using a private key""" + # Sign a message using a private key + signature = sk.sign(message, hashfunc=sha256) + return signature + +# Verify +def verify(vk, signature, message): + """Verify a message using a public key""" + # Verify a message using a public key + return vk.verify(signature, message, hashfunc=sha256) + +# Generate a key pair using a seed +def generate_ecdsa_key(seed): + """Generate a key pair using a seed""" + seed = seed.encode() + hashed = sha256(seed).digest() + # Generate a key pair using a seed + sk = ecdsa.SigningKey.from_string(hashed, curve=ecdsa.SECP256k1) + vk = sk.get_verifying_key() + return sk, vk + diff --git a/src/libs/seedable_rsa.py b/src/libs/seedable_rsa.py index b15fd6c..0ddb43e 100644 --- a/src/libs/seedable_rsa.py +++ b/src/libs/seedable_rsa.py @@ -30,7 +30,7 @@ def decrypt(encrypted_message, private_key): return None -def generate_key(seed, nbytes=2048): +def generate_rsa_key(seed, nbytes=2048): # based on https://stackoverflow.com/questions/18264314/#answer-18266970 seed_128 = HMAC.new( bytes( diff --git a/string_signer b/string_signer new file mode 100755 index 0000000..a718829 --- /dev/null +++ b/string_signer @@ -0,0 +1,20 @@ +#!./hmacenv/bin/python + +import src.hmacrypt as hmacrypt +import sys +import os + +# Getting and requiring exactly 1 argument +if len(sys.argv) != 2: + print("Usage: python3 string_signer ") + sys.exit(1) + +stringToSign = sys.argv[1].encode() + + +signed = hmacrypt.self_sign(stringToSign) + +with open("signed.txt", "wb+") as signedFile: + signedFile.write(signed) + +print(signed) diff --git a/string_verifier b/string_verifier new file mode 100755 index 0000000..78b26f1 --- /dev/null +++ b/string_verifier @@ -0,0 +1,19 @@ +#!./hmacenv/bin/python + +import src.hmacrypt as hmacrypt +import sys +import os + +# Getting and requiring exactly 1 argument +if len(sys.argv) != 3: + print("Usage: python3 string_verifier ") + sys.exit(1) + +message = sys.argv[1].encode() +signaturePath = sys.argv[2].encode() + +with open(signaturePath, "rb") as f: + signature = f.read() + +verified = hmacrypt.self_verify(signature, message) +print(verified)