Last active
December 19, 2024 14:49
-
-
Save nerun/1cf486dbd550f2c65846f64f46124e04 to your computer and use it in GitHub Desktop.
bfisher, short for 'blowfisher', is a tool written in Python 3 using Cryptography package, that encrypts and decrypts using blowfish cipher and PBKDF2 with HMAC.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# -*- coding: utf-8 -*- | |
""" | |
bfisher - version 1.0b - December 19, 2024 | |
Copyright (c) 2024 Daniel Dias Rodrigues <[email protected]> | |
This program is free software; you can redistribute it and/or modify it under | |
the terms of the Creative Commons Zero 1.0 Universal (CC0 1.0) Public Domain | |
Dedication (https://creativecommons.org/publicdomain/zero/1.0/). | |
DEPENDENCY | |
In Debian you must install package "python3-cryptography". | |
ABOUT KEY SIZE | |
https://cryptography.io/en/latest/hazmat/decrepit/ciphers/ | |
#cryptography.hazmat.decrepit.ciphers.Blowfish | |
Blowfish supports a variable-length key, from 4 to 56 bytes. | |
ABOUT INITIALIZATION VECTOR (IV) | |
https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/ | |
#cryptography.hazmat.primitives.ciphers.modes.CBC | |
IV, or "Initialization Vector", is the same as nonce (a number used only | |
once). They do not need to be kept secret and they can be included in a | |
transmitted message. IV must be equal to cipher block size in bytes (not | |
bits). Do not reuse an IV with a given key, and particularly do not use a | |
constant IV. Blowfish has a block size of 8 bytes. | |
ABOUT PKCS7 PADDING | |
https://cryptography.io/en/latest/hazmat/primitives/padding/ | |
#cryptography.hazmat.primitives.padding.PKCS7 | |
PKCS7 padding works by adding bytes to the data to make the final block of | |
data the same size as the cipher block. The number provided to padding | |
function is the size of the block in bits that the data is being padded to | |
(1 byte = 8 bits). Blowfish has a padding of 64 bits (8 × 8 = 64). | |
ABOUT encryptor(), decryptor(), update(), finalize() | |
https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/ | |
#cryptography.hazmat.primitives.ciphers.CipherContext | |
""" | |
import argparse, os, re, secrets, sys, string, textwrap | |
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | |
from cryptography.hazmat.primitives import padding | |
from cryptography.hazmat.primitives import hashes | |
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC | |
from getpass import getpass | |
# FILTER DEPRECATION WARNING | |
# CryptographyDeprecationWarning: Blowfish has been moved to | |
# cryptography.hazmat.decrepit.ciphers.algorithms.Blowfish and will be | |
# removed from this module in 45.0.0. | |
import warnings | |
warnings.filterwarnings("ignore", message='Blowfish has been moved to') | |
class BlowfishHelper(): | |
def __init__(self, key, salt): | |
self.key = key | |
self.salt = salt | |
def encrypt(self, data): | |
data.seek(0) | |
file_original = data.read() | |
padder = padding.PKCS7(algorithms.Blowfish.block_size).padder() | |
padded_data = padder.update(file_original) + padder.finalize() | |
iv = secrets.token_bytes(8) | |
cipher = Cipher(algorithms.Blowfish(self.key), modes.CBC(iv)) | |
encryptor = cipher.encryptor() | |
enc = encryptor.update(padded_data) + encryptor.finalize() | |
# append IV to the end of data | |
enc += b'$iv:' + iv + b':iv$' | |
# append SALT to the end of data | |
enc += b'$salt:' + self.salt + b':salt$' | |
# create new file name to encrypted data | |
filename, file_extension = os.path.splitext(data.name) | |
# append file_extension to the end of data | |
enc += b'$ext:' + file_extension.encode('utf-8') + b':ext$' | |
new_filename = filename + '.fish' | |
with open(new_filename, 'wb') as file: | |
file.write(enc) | |
if os.path.isfile(new_filename): | |
print('File \"{}\" was successfully encrypted as \"{}\".' | |
.format(data.name, new_filename)) | |
def decrypt(self, data): | |
data.seek(0) | |
file_encrypted = data.read() | |
# retrieve IV from data | |
pattern_iv = re.compile(b'\\$iv:.*:iv\\$') | |
for match in re.finditer(pattern_iv, file_encrypted): | |
iv_full = match.group() | |
# [4:-4] excludes '$iv:' and ':iv$' | |
iv = iv_full[4:-4] | |
# retrieve original file extension | |
pattern_ext = re.compile(b'\\$ext:.*:ext\\$') | |
for match in re.finditer(pattern_ext, file_encrypted): | |
ext_full = match.group() | |
# [5:-5] excludes '$ext:' and ':ext$' | |
# decode('utf-8') turns bytes to string | |
ext = ext_full[5:-5].decode('utf-8') | |
# remove IV, SALT and EXT from data | |
file_encrypted = file_encrypted.replace(iv_full,b'') | |
file_encrypted = file_encrypted.replace(self.salt,b'') | |
file_encrypted = file_encrypted.replace(ext_full,b'') | |
cipher = Cipher(algorithms.Blowfish(self.key), modes.CBC(iv)) | |
decryptor = cipher.decryptor() | |
dec = decryptor.update(file_encrypted) + decryptor.finalize() | |
unpadder = padding.PKCS7(algorithms.Blowfish.block_size).unpadder() | |
try: | |
unpadded_data = unpadder.update(dec) + unpadder.finalize() | |
except: | |
sys.exit("\n Wrong password!") | |
new_filename = data.name.replace('.fish',ext) | |
with open(new_filename, 'wb') as file: | |
file.write(unpadded_data) | |
if os.path.isfile(new_filename): | |
print('File \"{}\" was successfully decrypted as \"{}\".' | |
.format(data.name, new_filename)) | |
parser = argparse.ArgumentParser( | |
prog = 'bfisher', | |
formatter_class = argparse.RawTextHelpFormatter, | |
description = '''Encrypts and decrypts files with Blowfish symmetric encryption algorithm through | |
cryptography package available at https://pypi.org/project/cryptography.''', | |
epilog = ''' | |
KDF: | |
By default, the key is derived from any password provided through the PBKDF2 | |
function with HMAC, using SHA-256 hash, 128-bit salt, and 600k iterations as | |
recommended by Open Worldwide Application Security Project (OWASP) in 2023. | |
WARNING: | |
Blowfish is now considered a decrepit cipher, and will be removed from | |
Cryptography package. This warning shows up when Blowfish cipher is called: | |
\033[91mCryptographyDeprecationWarning: Blowfish has been moved to | |
cryptography.hazmat.decrepit.ciphers.algorithms.Blowfish and will be | |
removed from this module in 45.0.0.\033[0m | |
The above notice has been \"filtered\" so as not to interfere with the use of | |
this tool. | |
CURIOSITY: | |
The name \"bfisher\" is an abbreviation of blowfisher. Blowfish is a fish and | |
fisher is one who fishes, so blowfisher is a software that handles the | |
blowfish cipher. | |
''', | |
add_help = False | |
) | |
parser._optionals.title = 'OPTIONS' | |
group = parser.add_mutually_exclusive_group(required=True) | |
group.add_argument( | |
'-e', '--encrypt', | |
metavar = 'FILE', | |
type = argparse.FileType('rb'), | |
help = '''Encrypts a file. Will prompt for a password if a keyfile | |
is not provided. See '-k'. | |
''' | |
) | |
group.add_argument( | |
'-d', '--decrypt', | |
metavar = 'FILE', | |
type = argparse.FileType('rb'), | |
help = '''Decrypts a file. Will prompt for a password if a keyfile | |
is not provided. See '-k'. | |
''' | |
) | |
group.add_argument( | |
'-g', '--genkey', | |
metavar = 'KEYFILE', | |
help = '''Generates a plain text keyfile named KEYFILE in the | |
current working directory (alternatively you can provide | |
a full path). But see '-k' first. | |
''' | |
) | |
parser.add_argument( | |
'-k', '--keyfile', | |
metavar = 'KEYFILE', | |
help = '''A keyfile to be used in addition to the -d/-e commands, | |
containing a very complex and high-entropy password. | |
(optional) | |
Absolutely ANY file can be used as a keyfile: images, | |
plain texts, spreadsheets, mp3 etc. But you can create | |
one with '-g'. | |
''' | |
) | |
parser.add_argument( | |
'-h', '--help', | |
help = 'Show this help message and exit.\n\n', | |
action = 'help' | |
) | |
parser.add_argument( | |
'-v', '--version', | |
help = 'Shows version and copyright information.\n\n', | |
action = 'version', | |
version = '''bfisher - Version 1.0b - December 19, 2024 | |
Copyright (c) 2024 Daniel Dias Rodrigues <[email protected]> | |
This program is free software; you can redistribute it and/or modify it | |
under the terms of the Creative Commons Zero 1.0 Universal (CC0 1.0) Public | |
Domain Dedication (https://creativecommons.org/publicdomain/zero/1.0/).''' | |
) | |
def get_keyfile(k): | |
"""Attempts to open the file in text mode. If it fails, the file is | |
binary (non-text). It will then return the first 56 bytes of the | |
file, encoding it to UTF-8 if it is a text file.""" | |
try: | |
with open(k, 'rt') as file: | |
keyfile = file.read() | |
return keyfile[:56].encode('utf-8') | |
except: | |
with open(k, 'rb') as file: | |
keyfile = file.read() | |
return keyfile[:56] | |
def get_password(mode): | |
"""Prompts for a password and checks if it is the right length and | |
encodes it to UTF-8.""" | |
password = None | |
while password == None: | |
pwd1 = '' | |
while pwd1 == '' or len(pwd1) < 4 or len(pwd1) > 56: | |
if mode == 'En': | |
print('Password must be between 4 and 56 characters long.') | |
pwd1 = getpass() | |
if mode == 'En': | |
pwd2 = getpass('Retype password: ') | |
else: # mode == 'De' | |
pwd2 = pwd1 | |
if pwd1 == pwd2: | |
return pwd1.encode('utf-8') | |
else: | |
print('\033[93mPasswords does not match!\033[0m') | |
def derive_key(algorithm=hashes.SHA256(), length=56, | |
salt=secrets.token_bytes(16), iterations=600000): | |
"""Key Derivation Fucntion PBKDF2 with HMAC | |
Returns a tuple with derived key and the salt used.""" | |
return PBKDF2HMAC(algorithm, length, salt, iterations), salt | |
def cipher(mode, file): | |
"""mode (string) = En / De; | |
file (object) = args.encrypt / args.decrypt""" | |
print('{}crypting file {}...'.format(mode, file.name)) | |
# Get a password | |
if args.keyfile != None: | |
pwd = get_keyfile(args.keyfile) | |
else: | |
pwd = get_password(mode) | |
""" | |
If Encrypting: | |
derive_key() generates a salt, which is saved as "spice" and spice is | |
passed to BlowfishHelper. BlowfishHelper append salt to the encrypted | |
data. | |
Else If Decrypting: | |
We need to use the same salt used for encryption, so we extract it from | |
the data file, the salt_retrieved is cleaned as salt_pure and then | |
passed to derive_key() which uses it instead of generating a new one, | |
then derive_key() returns the same salt_pure as "spice", but | |
BlowfishHelper needs the salt_retrieved for decrypting, so we equals | |
spice to salt_retrieved. BlowfishHelper just use salt to remove it from | |
encrypted data before decrypting it, otherwise it fails to decrypt. | |
""" | |
# Derive a key from password | |
if mode == 'En': | |
kdf, spice = derive_key() | |
else: # if mode == 'De': | |
# retrieve SALT from data if decrypting | |
pattern_salt = re.compile(b'\\$salt:.*:salt\\$') | |
for match in re.finditer(pattern_salt, file.read()): | |
salt_retrieved = match.group() | |
# [6:-6] excludes '$salt:' and ':salt$' | |
salt_pure = salt_retrieved[6:-6] | |
kdf, spice = derive_key(salt=salt_pure) | |
spice = salt_retrieved # decrypting needs patterned salt | |
password = kdf.derive(pwd) | |
return BlowfishHelper(key=password, salt=spice) | |
def genkey_gpg_like(length=1009): | |
"""Generates a plain text keyfile of length 1024 bytes (1009 + 15) or (8192 | |
bits), which resembles GNU PG keys, but it's not the same, it's just an | |
imitation.""" | |
# 0-9, A-Z, a-z, +, /, = | |
alphabet = string.ascii_letters + string.digits + '/+=' | |
key = ''.join(secrets.choice(alphabet) for i in range(length)) | |
# GPG keys wraps every 64 characters | |
key = textwrap.wrap(key, width=64) | |
key = '\n'.join(key) | |
# GPG keys ends with '==....' | |
key = key[:-7] + '\n==' + key[-4:] | |
return key | |
args = parser.parse_args() | |
if args.encrypt != None: | |
b = cipher('En', args.encrypt) | |
b.encrypt(args.encrypt) | |
if args.decrypt != None: | |
b = cipher('De', args.decrypt) | |
b.decrypt(args.decrypt) | |
if args.genkey != None: | |
keyfile = genkey_gpg_like() | |
with open(args.genkey, 'wt') as file: | |
file.write(keyfile) | |
if os.path.isfile(args.genkey): | |
print('Keyfile was successfully generated and saved in \"{}\".' | |
.format(args.genkey)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Install to yours
/home/$USER/.local/bin
, then open terminal and usebfisher -h
.