Skip to content

Instantly share code, notes, and snippets.

@nerun
Last active December 19, 2024 14:49
Show Gist options
  • Save nerun/1cf486dbd550f2c65846f64f46124e04 to your computer and use it in GitHub Desktop.
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.
#!/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))
@nerun
Copy link
Author

nerun commented Dec 18, 2024

Install to yours /home/$USER/.local/bin, then open terminal and use bfisher -h.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment