Skip to content

Commit 034a59c

Browse files
committedJul 24, 2023
env encryption script
1 parent bed45c8 commit 034a59c

File tree

6 files changed

+406
-0
lines changed

6 files changed

+406
-0
lines changed
 

‎ENCRYPT-ENV-SCRIPT/README.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# ENCRYPTING ENVIRONMENT VARIABLE FILE SCRIPT
2+
3+
## A commandline software script to encrypt or decrypt an env file, specifically to share secret encrypted environment variables using a password via git
4+
The encryption uses the cryptography library using symmetric encryption, which means the same key used to encrypt data, is also usable for decryption.
5+
6+
7+
**To run the software:**
8+
```
9+
pip3 install -r requirements.txt
10+
python3 envvars_script.py --help
11+
```
12+
13+
**Output:**
14+
```
15+
usage: ennvars_script.py [-h] (-e | -d) file
16+
17+
File Encryption Script with a Password.
18+
19+
positional arguments:
20+
file File to encrypt/decrypt.
21+
22+
optional arguments:
23+
-h, --help show this help message and exit
24+
-e, --encrypt To encrypt the file, only -e or --encrypt can be specified.
25+
-d, --decrypt To decrypt the file, only -d or --decrypt can be specified.
26+
```
27+
28+
### Encrypt a file
29+
* 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:
30+
```
31+
python3 envvars_script.py <.env> --encrypt
32+
OR
33+
python3 envvars_script.py <.env> -e
34+
```
35+
* replace <.env> with the right filename.
36+
* Enter password to encrypt file. Note: password must be atleast 8 characters
37+
38+
* Check file content has been encrypted with the following files created:
39+
```
40+
.env.envs
41+
.env.salt
42+
43+
```
44+
45+
### Decrypt file
46+
* To decrypt file, (use same password used in encryting the file otherwise decrypting won't work). Run the following example command:
47+
```
48+
python3 envvars_script.py <.env.envs> --decrypt
49+
OR
50+
python3 envvars_script.py <.env.envs> -d
51+
```
52+
* replace <.env.envs> with the right filename.
53+
* Enter password to decrypt file. This must be the same password used in encrypting the file.
54+
55+
* Check file has been decrypted and updated with the original content(before encryption) if .env existed otherwise new .env file created with content.
56+
57+
**To run unit test:**
58+
```
59+
python3 test_script.py
60+
```

‎ENCRYPT-ENV-SCRIPT/__init__.py

Whitespace-only changes.

‎ENCRYPT-ENV-SCRIPT/envars_helper.py

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import os
2+
import base64
3+
import secrets
4+
5+
from cryptography.fernet import Fernet
6+
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
7+
import cryptography
8+
9+
10+
class EncryptionHelper:
11+
"""
12+
A class to represent Encryption.
13+
14+
Methods
15+
-------
16+
load_salt(self, filename):
17+
A method to read and return a generated salt saved in file.
18+
derive_key(self, salt, password):
19+
A method to derive key.
20+
generate_key(self, password, filename, load_existing_salt=False, save_salt=False):
21+
A method to generate key.
22+
encrypt(self, filename, key):
23+
A method to encrypt file.
24+
decrypt(self, filename, key):
25+
A method to decrypt file.
26+
"""
27+
28+
@staticmethod
29+
def generate_salt(size: int):
30+
"""
31+
A method to generate a salt.
32+
33+
Parameters
34+
----------
35+
size : int
36+
The size of the bytes strings to be generated.
37+
38+
Returns
39+
-------
40+
bytes
41+
The method returns bytes strings containing the secret token.
42+
"""
43+
44+
return secrets.token_bytes(size)
45+
46+
@staticmethod
47+
def load_salt(filename: str):
48+
"""
49+
A method to read and return a save salt file.
50+
51+
Parameters
52+
----------
53+
filename : str
54+
The file name to read from.
55+
56+
Returns
57+
-------
58+
bytes
59+
The method returns bytes containing the salt.
60+
"""
61+
62+
# load salt from salt file
63+
return open(filename.replace(".envs", ".salt"), "rb").read()
64+
65+
@staticmethod
66+
def derive_key(salt: bytes, password: str):
67+
"""
68+
A method to derive a key using password and salt token.
69+
70+
Parameters
71+
----------
72+
salt : bytes
73+
The bytes strings containing the salt token.
74+
password : str
75+
The strings of characters containing the password.
76+
77+
Returns
78+
-------
79+
bytes
80+
The method returns bytes string containing the derived key.
81+
"""
82+
83+
# derive key using salt and password
84+
key = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1)
85+
return key.derive(password.encode())
86+
87+
@staticmethod
88+
def generate_key(password: str, filename: str, load_existing_salt=False, save_salt=False):
89+
"""
90+
A method to generate key.
91+
92+
Parameters
93+
----------
94+
password : str
95+
The strings of characters containing the password.
96+
filename : str
97+
The strings of characters containing file name.
98+
load_existing_salt : bool, optional
99+
A boolean value determining existing salt exists.
100+
save_salt : bool, optional
101+
A boolean value determining save salt exists.
102+
103+
Returns
104+
-------
105+
bytes
106+
The method returns bytes string containing the generated key.
107+
"""
108+
109+
# check existing salt file
110+
if load_existing_salt:
111+
try:
112+
with open(filename.replace(".envs", ".salt"), "rb") as salt_file:
113+
salt_file.readline()
114+
except FileNotFoundError:
115+
return base64.urlsafe_b64encode(os.urandom(32))
116+
# load existing salt
117+
salt = EncryptionHelper.load_salt(filename)
118+
if save_salt:
119+
# generate new salt/token and save it to file
120+
salt = EncryptionHelper.generate_salt(16)
121+
with open(f"{filename}.salt", "wb") as salt_file:
122+
salt_file.write(salt)
123+
124+
# generate the key from the salt and the password
125+
derived_key = EncryptionHelper.derive_key(salt, password)
126+
# encode it using Base 64 and return it
127+
return base64.urlsafe_b64encode(derived_key)
128+
129+
@staticmethod
130+
def encrypt(filename: str, key: bytes):
131+
"""
132+
A method to encrypt file.
133+
134+
Parameters
135+
----------
136+
filename : str
137+
The strings of characters containing file name.
138+
key : bytes
139+
A bytes of stings containing the encryption key.
140+
141+
Returns
142+
-------
143+
None
144+
The method returns a none value.
145+
"""
146+
147+
fernet = Fernet(key)
148+
149+
try:
150+
with open(filename, "rb") as file:
151+
file_data = file.read()
152+
except FileNotFoundError:
153+
print("File not found")
154+
return
155+
156+
# encrypting file_data
157+
encrypted_data = fernet.encrypt(file_data)
158+
159+
# writing to a new file with the encrypted data
160+
with open(f"{filename}.envs", "wb") as file:
161+
file.write(encrypted_data)
162+
163+
print("File encrypted successfully...")
164+
return "File encrypted successfully..."
165+
166+
@staticmethod
167+
def decrypt(filename: str, key: bytes):
168+
"""
169+
A method to decrypt file.
170+
171+
Parameters
172+
----------
173+
filename : str
174+
The strings of characters containing file name.
175+
key : bytes
176+
A bytes of stings containing the encryption key.
177+
178+
Returns
179+
-------
180+
None
181+
The method returns a none value.
182+
"""
183+
184+
fernet = Fernet(key)
185+
try:
186+
with open(filename, "rb") as file:
187+
encrypted_data = file.read()
188+
except FileNotFoundError:
189+
print("File not found.")
190+
return
191+
# decrypt data using the Fernet object
192+
try:
193+
decrypted_data = fernet.decrypt(encrypted_data)
194+
except cryptography.fernet.InvalidToken:
195+
print("Invalid token, likely the password is incorrect.")
196+
return
197+
# write the original file with decrypted content
198+
with open(filename.replace(".envs", ""), "wb") as file:
199+
file.write(decrypted_data)
200+
# delete salt file after decrypting file
201+
f = open(filename.replace(".envs", ".salt"), 'w')
202+
f.close()
203+
os.remove(f.name)
204+
# delete decrypted file
205+
os.remove(filename)
206+
print("File decrypted successfully...")
207+
208+
return "File decrypted successfully..."

‎ENCRYPT-ENV-SCRIPT/envars_script.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import sys
2+
import getpass
3+
4+
from envars_helper import EncryptionHelper
5+
6+
if __name__ == "__main__":
7+
import argparse
8+
9+
encryption_helper = EncryptionHelper()
10+
11+
parser = argparse.ArgumentParser(description="File Encryption Script with a Password.",
12+
allow_abbrev=False)
13+
parser.add_argument("file", help="File to encrypt/decrypt.")
14+
group_args = parser.add_mutually_exclusive_group(required=True)
15+
group_args.add_argument("-e", "--encrypt", action="store_true",
16+
help="To encrypt the file, only -e or --encrypt can be specified.")
17+
group_args.add_argument("-d", "--decrypt", action="store_true",
18+
help="To decrypt the file, only -d or --decrypt can be specified.")
19+
20+
args = parser.parse_args()
21+
filename = args.file
22+
encrypt_arg = args.encrypt
23+
decrypt_arg = args.decrypt
24+
25+
try:
26+
with open(filename, "r") as f:
27+
file_data = f.readline()
28+
except FileNotFoundError:
29+
print("File not found.")
30+
sys.exit(1)
31+
32+
password = None
33+
if encrypt_arg:
34+
ext = filename.split(".").pop()
35+
if ext == "envs":
36+
print("File already encrypted.")
37+
sys.exit(1)
38+
password = getpass.getpass("Enter the password for encryption: ")
39+
while len(password) < 8:
40+
print("Password must be 8 characters or above. \n")
41+
password = getpass.getpass("Enter the password for encryption: ")
42+
elif decrypt_arg:
43+
ext = filename.split(".").pop()
44+
if ext != "envs":
45+
print("File was not encrypted. Encrypted file has a .envs extension")
46+
sys.exit(1)
47+
password = getpass.getpass("Enter the password used for encryption: ")
48+
49+
if encrypt_arg:
50+
key = encryption_helper.generate_key(password, filename, save_salt=True)
51+
encryption_helper.encrypt(filename, key)
52+
else:
53+
key = encryption_helper.generate_key(password, filename, load_existing_salt=True)
54+
encryption_helper.decrypt(filename, key)

‎ENCRYPT-ENV-SCRIPT/requirements.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
cffi==1.15.1
2+
cryptography==37.0.4
3+
pycparser==2.21

‎ENCRYPT-ENV-SCRIPT/test_script.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from fileinput import filename
2+
import os
3+
import unittest
4+
5+
from envars_helper import EncryptionHelper
6+
7+
8+
class TestEnvvarsEncryption(unittest.TestCase):
9+
10+
def setUp(self):
11+
self.password = "mypassword.com"
12+
self.filename = ".env"
13+
self.filename_to_be_decrypted = ".env.envs"
14+
self.envvars_encryption = EncryptionHelper()
15+
16+
def tearDown(self):
17+
#delete test salt file from file
18+
file_data = open(f"{self.filename}.salt", 'w')
19+
file_data.close()
20+
os.remove(file_data.name)
21+
22+
#delete test encrypted file from file
23+
file_data = open(f"{self.filename}.envs", 'w')
24+
file_data.close()
25+
os.remove(file_data.name)
26+
27+
28+
29+
def test_is_instance(self):
30+
"""Test class instance. """
31+
32+
self.assertTrue(isinstance(self.envvars_encryption, EncryptionHelper))
33+
34+
def test_generate_key_method(self):
35+
"""Test generate key is instance method. """
36+
self.assertTrue(self.envvars_encryption.generate_key)
37+
38+
def test_encrypt_method(self):
39+
"""Test encrypt is instance method. """
40+
41+
self.assertTrue(self.envvars_encryption.encrypt)
42+
43+
def test_decrypt_method(self):
44+
"""Test decrypt is instance method. """
45+
46+
self.assertTrue(self.envvars_encryption.decrypt)
47+
48+
def test_generate_key(self):
49+
"""Test generate key method. """
50+
51+
gen_key = self.envvars_encryption.generate_key(self.password, self.filename, save_salt=True)
52+
53+
self.assertEqual(type(gen_key), bytes)
54+
55+
56+
def test_encrypt(self):
57+
"""Test encrypt method. """
58+
59+
key = self.envvars_encryption.generate_key(self.password, self.filename, save_salt=True)
60+
encrypted = self.envvars_encryption.encrypt(self.filename, key)
61+
self.assertEqual(encrypted, "File encrypted successfully...")
62+
63+
def test_decrypt_file_doesnot_exist(self):
64+
"""Test decryp file does not exist """
65+
66+
key = self.envvars_encryption.generate_key(self.password, self.filename, save_salt=True)
67+
self.envvars_encryption.encrypt(self.filename, key)
68+
self.envvars_encryption.decrypt("wrong.notenvs", key)
69+
self.assertRaises(SystemExit)
70+
71+
def test_decrypt(self):
72+
"""Test decrypt method. """
73+
74+
key = self.envvars_encryption.generate_key(self.password, self.filename, save_salt=True)
75+
self.envvars_encryption.encrypt(self.filename, key)
76+
decrypted = self.envvars_encryption.decrypt(self.filename_to_be_decrypted, key)
77+
self.assertEqual(decrypted, "File decrypted successfully...")
78+
79+
80+
if __name__ == '__main__':
81+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.