Skip to content

Commit

Permalink
Push first stable release
Browse files Browse the repository at this point in the history
  • Loading branch information
Herkkomehtala committed May 21, 2023
1 parent 0485728 commit 21b9e55
Show file tree
Hide file tree
Showing 6 changed files with 308 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
testfile.*
launch.json
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,28 @@
# PythonEncrypt
AES256 file encryption/decryption solution with key derivation

Python3 solution to perform AES256 file encryption/decryption with key derivation.

## Installation

1. Install [Cryptography library](https://cryptography.io/en/latest/) for python.
- With pip: ```pip install cryptography```

2. Clone this repository or download the latest release of PythonEncrypt to your machine.
3. Run ```PythonEncrypt.py```

## Usage

PythonEnrypt supports AES256 file encryption/decryption with the following modes: ```CBC, CTR, OFB``` . You can select the mode with the ```--mode``` flag.
Print program usage with the flag ```-h```

To encrypt a file with the default CBC mode, run command:
```PythonEncrypt.py --file <File> --encrypt```
To decrypt a file, run command:
```PythonEncrypt.py --file <File> --decrypt```

## Security

<span style="color:red">This program is **ONLY** for low-security uses.</span> This tool is not NIST SP 800-57 or FIPS 140 compliant. There are a few reasons for this, most importantly the cryptographic keys are not held in tamper proof memory and there aren't adequate cryptographic boundaries. This tool is made to protect against your snoopy co-worker or that cousin you have.

There won't be updates to address this issue. This is due to Python being a poor choice for creating high security cryptograhic modules. To find out why, you can read up [here](https://stackoverflow.com/questions/728164/securely-erasing-password-in-memory-python).

159 changes: 159 additions & 0 deletions modules/AES256.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import modules.keyDerivation, modules.auxiliary
import os, hashlib

def AesCBCencrypt(file):
"""
Performs AES CBC Encryption with 256 bit key to a file and outputs the ciphertext into a new file.
Input: Filename (Absolute path recommended)
Output: 1 on success.
"""
Blocksize_bytes, Blocksize_bits = 16, 128
ofile = file + ".enc"
iv = os.urandom(Blocksize_bytes)
hash = hashlib.sha256(b'AesCBC').digest()

# Derive the key from password
KeyandSalt = modules.keyDerivation.DeriveKey()

# Read the plaintext
with open(file, "rb") as reader:
plaintext = reader.read()

# Do the padding and encrypt data
padder = padding.PKCS7(Blocksize_bits).padder()
plaintext = padder.update(plaintext) + padder.finalize()
aesCipher = Cipher(algorithms.AES256(KeyandSalt[0]), modes.CBC(iv), default_backend())
aesEncryptor = aesCipher.encryptor()
ciphertext = aesEncryptor.update(plaintext) + aesEncryptor.finalize()

# Create encrypted file
with open(ofile, "wb+") as writer:
writer.write(KeyandSalt[1] + iv + hash + ciphertext) # Variables salt + IV + hash are required for decryption

print('Success! Encrypted file at: %s' % ofile)
return 1

def AesCTRencrypt(file):
"""
Performs AES CTR Encryption with 256 bit key to a file and outputs the ciphertext into a new file.
Input: Filename (Absolute path recommended)
Output: 1 on success.
"""
Blocksize_bytes, Blocksize_bits = 16, 128
ofile = file + ".enc"
nonce = os.urandom(Blocksize_bytes)
hash = hashlib.sha256(b'AesCTR').digest()

# Derive the key from password
KeyandSalt = modules.keyDerivation.DeriveKey()

# Read the plaintext
with open(file, "rb") as reader:
plaintext = reader.read()

# Do the padding and encrypt data
padder = padding.PKCS7(Blocksize_bits).padder()
plaintext = padder.update(plaintext) + padder.finalize()
aesCipher = Cipher(algorithms.AES256(KeyandSalt[0]), modes.CTR(nonce), default_backend())
aesEncryptor = aesCipher.encryptor()
ciphertext = aesEncryptor.update(plaintext) + aesEncryptor.finalize()

# Create encrypted file
with open(ofile, "wb+") as writer:
writer.write(KeyandSalt[1] + nonce + hash + ciphertext) # Variables salt + nonce + hash are required for decryption

print('Success! Encrypted file at: %s' % ofile)
return 1

def AesOFBencrypt(file):
"""
Performs AES OFB Encryption with 256 bit key to a file and outputs the ciphertext into a new file.
Input: Filename (Absolute path recommended)
Output: 1 on success.
"""
Blocksize_bytes, Blocksize_bits = 16, 128
ofile = file + ".enc"
iv = os.urandom(Blocksize_bytes)
hash = hashlib.sha256(b'AesOFB').digest()

# Derive the key from password
KeyandSalt = modules.keyDerivation.DeriveKey()

# Read the plaintext
with open(file, "rb") as reader:
plaintext = reader.read()

# Encrypt the data, no padding needed
aesCipher = Cipher(algorithms.AES256(KeyandSalt[0]), modes.OFB(iv), default_backend())
aesEncryptor = aesCipher.encryptor()
ciphertext = aesEncryptor.update(plaintext) + aesEncryptor.finalize()

# Create encrypted file
with open(ofile, "wb+") as writer:
writer.write(KeyandSalt[1] + iv + hash + ciphertext) # Variables salt + iv + hash are required for decryption

print('Success! Encrypted file at: %s' % ofile)
return 1

def AesDecrypt(file):
"""
Decrypts AES256 encrypted file.
Input: Filename (Absolute path recommended)
Output: 1 on Success
"""
Blocksize_bits = 128
ofile = file[:-4]
HeaderInfo = modules.auxiliary.ExtractHeader(file)
KeyandSalt = modules.keyDerivation.DeriveKey(HeaderInfo[0])

# Get the ciphertext
with open(file, 'rb') as f:
f.seek(64)
ciphertext = f.read()

# CBC Mode
if HeaderInfo[2] == 1:
print("CBC Mode of operation detected...")
aesCipher = Cipher(algorithms.AES256(KeyandSalt[0]), modes.CBC(HeaderInfo[1]), default_backend())
aesDecryptor = aesCipher.decryptor()
paddedData = aesDecryptor.update(ciphertext) + aesDecryptor.finalize()

unpadder = padding.PKCS7(Blocksize_bits).unpadder()
plaintext = unpadder.update(paddedData) + unpadder.finalize()

with open(ofile, "wb+") as writer:
writer.write(plaintext)

print("File decrypted succesfully!")

# CTR Mode
if HeaderInfo[2] == 2:
print("CTR Mode of operation detected...")
aesCipher = Cipher(algorithms.AES256(KeyandSalt[0]), modes.CTR(HeaderInfo[1]), default_backend())
aesDecryptor = aesCipher.decryptor()
paddedData = aesDecryptor.update(ciphertext) + aesDecryptor.finalize()

unpadder = padding.PKCS7(Blocksize_bits).unpadder()
plaintext = unpadder.update(paddedData) + unpadder.finalize()

with open(ofile, "wb+") as writer:
writer.write(plaintext)

print("File decrypted succesfully!")

# OFB Mode
if HeaderInfo[2] == 3:
print("OFB Mode of operation detected...")
aesCipher = Cipher(algorithms.AES256(KeyandSalt[0]), modes.OFB(HeaderInfo[1]), default_backend())
aesDecryptor = aesCipher.decryptor()
plaintext = aesDecryptor.update(ciphertext) + aesDecryptor.finalize()

with open(ofile, "wb+") as writer:
writer.write(plaintext)

print("File decrypted succesfully!")

return 1
46 changes: 46 additions & 0 deletions modules/auxiliary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os
from sys import exit

def ExtractHeader(file):
"""
Extract the header information from an encrypted file. Parses the salt, IV and mode of operation. CBC = 1, CTR = 2, OFB = 3
Input: Filename (Absolute path recommended)
Output: Array of salt, IV, mode of operation code
"""
CBChash = "03872ff57deb958f37678ed016fb340709bbeb4deba111ca518ddc8f064c0ddd"
CTRhash = "2d676e12555bcd4e155538681b165af6b494cc9cbff16adb27ad4533592a51b4"
OFBhash = "2e4146a86c183223f533d4ad39eed6d6280795e354279a65555944648d899112"

with open(file, 'rb') as f:
header = f.read(64)

# Parse the header information
salt = header[:16]
iv = header[16:32]
hash = header[32:64]

if hash.hex() != CBChash and hash.hex() != CTRhash and hash.hex() != OFBhash:
print("Error: Incorrect mode of operation in header")
exit(2)
elif hash.hex() == CBChash:
return [salt, iv, 1]
elif hash.hex() == CTRhash:
return [salt, iv, 2]
else:
return [salt, iv, 3]

def SecureDelete(path, passes=1):
"""
Secure delete original plaintext file function.
Input: File path
Output: 1 on success
"""
with open(path, "ba+") as delfile:
length = delfile.tell()
with open(path, "br+") as delfile:
for i in range(passes):
delfile.seek(0)
delfile.write(os.urandom(length))
os.remove(path)
return 1

23 changes: 23 additions & 0 deletions modules/keyDerivation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from os import urandom
from getpass import getpass
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

def DeriveKey(salt=None):
"""
A Key derivation function using PBKDF2. Produces a cryptographically secure key from user inputted password. If salt is given, it will be used.
Input: Salt (Optional)
Output: An array of Encryption key, Salt
"""
if salt is None:
salt = urandom(16)

kdf = PBKDF2HMAC(
algorithm = hashes.SHA256(),
length = 32,
salt = salt,
iterations = 300000,
)

key = kdf.derive(getpass("Please enter your password: ").encode())
return [key, salt]
50 changes: 50 additions & 0 deletions pythonEncrypt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import os, sys, argparse
import modules.AES256, modules.auxiliary

def getArgs(argv=None):
"""
Argument parser function. Returns the namespace of the parsed arguments.
"""
parser = argparse.ArgumentParser(
prog = "pythonEncrypt.py",
description='Python utility to encrypt or decrypt files.'
)
parser.add_argument("-f", "--file", help="File to perform action on", required=True)
parser.add_argument("-m", "--mode", help="Select mode for encryption, default CBC (CBC, CTR, OFB)", nargs="?", type=str, default="CBC")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-e", "--encrypt", help="Encrypt the file", action="store_true")
group.add_argument("-d", "--decrypt", help="Decrypt the file", action="store_true")

if len(sys.argv) < 2:
parser.print_usage()
sys.exit(2)

args = parser.parse_args()
if args.mode != "CBC" and args.mode != "CTR" and args.mode != "OFB":
print("Error: Invalid mode of operation!")
sys.exit(2)

return parser.parse_args(argv)

if __name__ == "__main__":
args = getArgs()

if os.path.isfile(args.file):
# Valid file given
if args.encrypt and args.mode == "CBC":
print("Encrypting file: {}".format(args.file))
modules.AES256.AesCBCencrypt(os.path.abspath(args.file))
modules.auxiliary.SecureDelete(os.path.abspath(args.file))
elif args.encrypt and args.mode == "CTR":
print("Encrypting file: {}".format(args.file))
modules.AES256.AesCTRencrypt(os.path.abspath(args.file))
modules.auxiliary.SecureDelete(os.path.abspath(args.file))
elif args.encrypt and args.mode == "OFB":
modules.AES256.AesOFBencrypt(os.path.abspath(args.file))
modules.auxiliary.SecureDelete(os.path.abspath(args.file))
elif args.decrypt:
print("Decrypting file: {}".format(args.file))
modules.AES256.AesDecrypt(os.path.abspath(args.file))
else:
print("Error: File {} could not be found!".format(args.file))
sys.exit(2)

0 comments on commit 21b9e55

Please sign in to comment.