diff --git a/ENCRYPT-ENV-SCRIPT/README.md b/ENCRYPT-ENV-SCRIPT/README.md new file mode 100644 index 00000000..a89be0cd --- /dev/null +++ b/ENCRYPT-ENV-SCRIPT/README.md @@ -0,0 +1,60 @@ +# ENCRYPTING ENVIRONMENT VARIABLE FILE SCRIPT + +## A commandline software script to encrypt or decrypt an env file, specifically to share secret encrypted environment variables using a password via git +The encryption uses the cryptography library using symmetric encryption, which means the same key used to encrypt data, is also usable for decryption. + + +**To run the software:** +``` +pip3 install -r requirements.txt +python3 envvars_script.py --help +``` + +**Output:** +``` +usage: ennvars_script.py [-h] (-e | -d) file + +File Encryption Script with a Password. + +positional arguments: + file File to encrypt/decrypt. + +optional arguments: + -h, --help show this help message and exit + -e, --encrypt To encrypt the file, only -e or --encrypt can be specified. + -d, --decrypt To decrypt the file, only -d or --decrypt can be specified. +``` + +### Encrypt a file +* Encrypting a file for example ".env". Create the file in the project root directory and save it with the env content. Run the following example command: + ``` + python3 envvars_script.py <.env> --encrypt + OR + python3 envvars_script.py <.env> -e + ``` + * replace <.env> with the right filename. +* Enter password to encrypt file. Note: password must be atleast 8 characters + +* Check file content has been encrypted with the following files created: +``` + .env.envs + .env.salt + +``` + +### Decrypt file +* To decrypt file, (use same password used in encryting the file otherwise decrypting won't work). Run the following example command: + ``` + python3 envvars_script.py <.env.envs> --decrypt + OR + python3 envvars_script.py <.env.envs> -d + ``` + * replace <.env.envs> with the right filename. +* Enter password to decrypt file. This must be the same password used in encrypting the file. + +* Check file has been decrypted and updated with the original content(before encryption) if .env existed otherwise new .env file created with content. + +**To run unit test:** +``` + python3 test_script.py +``` diff --git a/ENCRYPT-ENV-SCRIPT/__init__.py b/ENCRYPT-ENV-SCRIPT/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ENCRYPT-ENV-SCRIPT/envars_helper.py b/ENCRYPT-ENV-SCRIPT/envars_helper.py new file mode 100644 index 00000000..f978750e --- /dev/null +++ b/ENCRYPT-ENV-SCRIPT/envars_helper.py @@ -0,0 +1,208 @@ +import os +import base64 +import secrets + +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives.kdf.scrypt import Scrypt +import cryptography + + +class EncryptionHelper: + """ + A class to represent Encryption. + + Methods + ------- + load_salt(self, filename): + A method to read and return a generated salt saved in file. + derive_key(self, salt, password): + A method to derive key. + generate_key(self, password, filename, load_existing_salt=False, save_salt=False): + A method to generate key. + encrypt(self, filename, key): + A method to encrypt file. + decrypt(self, filename, key): + A method to decrypt file. + """ + + @staticmethod + def generate_salt(size: int): + """ + A method to generate a salt. + + Parameters + ---------- + size : int + The size of the bytes strings to be generated. + + Returns + ------- + bytes + The method returns bytes strings containing the secret token. + """ + + return secrets.token_bytes(size) + + @staticmethod + def load_salt(filename: str): + """ + A method to read and return a save salt file. + + Parameters + ---------- + filename : str + The file name to read from. + + Returns + ------- + bytes + The method returns bytes containing the salt. + """ + + # load salt from salt file + return open(filename.replace(".envs", ".salt"), "rb").read() + + @staticmethod + def derive_key(salt: bytes, password: str): + """ + A method to derive a key using password and salt token. + + Parameters + ---------- + salt : bytes + The bytes strings containing the salt token. + password : str + The strings of characters containing the password. + + Returns + ------- + bytes + The method returns bytes string containing the derived key. + """ + + # derive key using salt and password + key = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1) + return key.derive(password.encode()) + + @staticmethod + def generate_key(password: str, filename: str, load_existing_salt=False, save_salt=False): + """ + A method to generate key. + + Parameters + ---------- + password : str + The strings of characters containing the password. + filename : str + The strings of characters containing file name. + load_existing_salt : bool, optional + A boolean value determining existing salt exists. + save_salt : bool, optional + A boolean value determining save salt exists. + + Returns + ------- + bytes + The method returns bytes string containing the generated key. + """ + + # check existing salt file + if load_existing_salt: + try: + with open(filename.replace(".envs", ".salt"), "rb") as salt_file: + salt_file.readline() + except FileNotFoundError: + return base64.urlsafe_b64encode(os.urandom(32)) + # load existing salt + salt = EncryptionHelper.load_salt(filename) + if save_salt: + # generate new salt/token and save it to file + salt = EncryptionHelper.generate_salt(16) + with open(f"{filename}.salt", "wb") as salt_file: + salt_file.write(salt) + + # generate the key from the salt and the password + derived_key = EncryptionHelper.derive_key(salt, password) + # encode it using Base 64 and return it + return base64.urlsafe_b64encode(derived_key) + + @staticmethod + def encrypt(filename: str, key: bytes): + """ + A method to encrypt file. + + Parameters + ---------- + filename : str + The strings of characters containing file name. + key : bytes + A bytes of stings containing the encryption key. + + Returns + ------- + None + The method returns a none value. + """ + + fernet = Fernet(key) + + try: + with open(filename, "rb") as file: + file_data = file.read() + except FileNotFoundError: + print("File not found") + return + + # encrypting file_data + encrypted_data = fernet.encrypt(file_data) + + # writing to a new file with the encrypted data + with open(f"{filename}.envs", "wb") as file: + file.write(encrypted_data) + + print("File encrypted successfully...") + return "File encrypted successfully..." + + @staticmethod + def decrypt(filename: str, key: bytes): + """ + A method to decrypt file. + + Parameters + ---------- + filename : str + The strings of characters containing file name. + key : bytes + A bytes of stings containing the encryption key. + + Returns + ------- + None + The method returns a none value. + """ + + fernet = Fernet(key) + try: + with open(filename, "rb") as file: + encrypted_data = file.read() + except FileNotFoundError: + print("File not found.") + return + # decrypt data using the Fernet object + try: + decrypted_data = fernet.decrypt(encrypted_data) + except cryptography.fernet.InvalidToken: + print("Invalid token, likely the password is incorrect.") + return + # write the original file with decrypted content + with open(filename.replace(".envs", ""), "wb") as file: + file.write(decrypted_data) + # delete salt file after decrypting file + f = open(filename.replace(".envs", ".salt"), 'w') + f.close() + os.remove(f.name) + # delete decrypted file + os.remove(filename) + print("File decrypted successfully...") + + return "File decrypted successfully..." diff --git a/ENCRYPT-ENV-SCRIPT/envars_script.py b/ENCRYPT-ENV-SCRIPT/envars_script.py new file mode 100644 index 00000000..a30f8e54 --- /dev/null +++ b/ENCRYPT-ENV-SCRIPT/envars_script.py @@ -0,0 +1,54 @@ +import sys +import getpass + +from envars_helper import EncryptionHelper + +if __name__ == "__main__": + import argparse + + encryption_helper = EncryptionHelper() + + parser = argparse.ArgumentParser(description="File Encryption Script with a Password.", + allow_abbrev=False) + parser.add_argument("file", help="File to encrypt/decrypt.") + group_args = parser.add_mutually_exclusive_group(required=True) + group_args.add_argument("-e", "--encrypt", action="store_true", + help="To encrypt the file, only -e or --encrypt can be specified.") + group_args.add_argument("-d", "--decrypt", action="store_true", + help="To decrypt the file, only -d or --decrypt can be specified.") + + args = parser.parse_args() + filename = args.file + encrypt_arg = args.encrypt + decrypt_arg = args.decrypt + + try: + with open(filename, "r") as f: + file_data = f.readline() + except FileNotFoundError: + print("File not found.") + sys.exit(1) + + password = None + if encrypt_arg: + ext = filename.split(".").pop() + if ext == "envs": + print("File already encrypted.") + sys.exit(1) + password = getpass.getpass("Enter the password for encryption: ") + while len(password) < 8: + print("Password must be 8 characters or above. \n") + password = getpass.getpass("Enter the password for encryption: ") + elif decrypt_arg: + ext = filename.split(".").pop() + if ext != "envs": + print("File was not encrypted. Encrypted file has a .envs extension") + sys.exit(1) + password = getpass.getpass("Enter the password used for encryption: ") + + if encrypt_arg: + key = encryption_helper.generate_key(password, filename, save_salt=True) + encryption_helper.encrypt(filename, key) + else: + key = encryption_helper.generate_key(password, filename, load_existing_salt=True) + encryption_helper.decrypt(filename, key) diff --git a/ENCRYPT-ENV-SCRIPT/requirements.txt b/ENCRYPT-ENV-SCRIPT/requirements.txt new file mode 100644 index 00000000..84927ab5 --- /dev/null +++ b/ENCRYPT-ENV-SCRIPT/requirements.txt @@ -0,0 +1,3 @@ +cffi==1.15.1 +cryptography==37.0.4 +pycparser==2.21 diff --git a/ENCRYPT-ENV-SCRIPT/test_script.py b/ENCRYPT-ENV-SCRIPT/test_script.py new file mode 100644 index 00000000..1359a9ca --- /dev/null +++ b/ENCRYPT-ENV-SCRIPT/test_script.py @@ -0,0 +1,81 @@ +from fileinput import filename +import os +import unittest + +from envars_helper import EncryptionHelper + + +class TestEnvvarsEncryption(unittest.TestCase): + + def setUp(self): + self.password = "mypassword.com" + self.filename = ".env" + self.filename_to_be_decrypted = ".env.envs" + self.envvars_encryption = EncryptionHelper() + + def tearDown(self): + #delete test salt file from file + file_data = open(f"{self.filename}.salt", 'w') + file_data.close() + os.remove(file_data.name) + + #delete test encrypted file from file + file_data = open(f"{self.filename}.envs", 'w') + file_data.close() + os.remove(file_data.name) + + + + def test_is_instance(self): + """Test class instance. """ + + self.assertTrue(isinstance(self.envvars_encryption, EncryptionHelper)) + + def test_generate_key_method(self): + """Test generate key is instance method. """ + self.assertTrue(self.envvars_encryption.generate_key) + + def test_encrypt_method(self): + """Test encrypt is instance method. """ + + self.assertTrue(self.envvars_encryption.encrypt) + + def test_decrypt_method(self): + """Test decrypt is instance method. """ + + self.assertTrue(self.envvars_encryption.decrypt) + + def test_generate_key(self): + """Test generate key method. """ + + gen_key = self.envvars_encryption.generate_key(self.password, self.filename, save_salt=True) + + self.assertEqual(type(gen_key), bytes) + + + def test_encrypt(self): + """Test encrypt method. """ + + key = self.envvars_encryption.generate_key(self.password, self.filename, save_salt=True) + encrypted = self.envvars_encryption.encrypt(self.filename, key) + self.assertEqual(encrypted, "File encrypted successfully...") + + def test_decrypt_file_doesnot_exist(self): + """Test decryp file does not exist """ + + key = self.envvars_encryption.generate_key(self.password, self.filename, save_salt=True) + self.envvars_encryption.encrypt(self.filename, key) + self.envvars_encryption.decrypt("wrong.notenvs", key) + self.assertRaises(SystemExit) + + def test_decrypt(self): + """Test decrypt method. """ + + key = self.envvars_encryption.generate_key(self.password, self.filename, save_salt=True) + self.envvars_encryption.encrypt(self.filename, key) + decrypted = self.envvars_encryption.decrypt(self.filename_to_be_decrypted, key) + self.assertEqual(decrypted, "File decrypted successfully...") + + +if __name__ == '__main__': + unittest.main()