Twofish encryption in Python

8.6k Views Asked by At

I have a test tool which uses Twofish as the encryption algorithm to encrypt data before sending it to the server. The code is written in C++ and uses Bruce Schneier's optimized C implementation (https://www.schneier.com/code/twofish-optimized-c.zip). I need to port this tool to Python and I am using twofish module (https://pypi.python.org/pypi/twofish/0.3.0). I can encrypt and decrypt strings of 16 char length but for other string lengths it gives an error 'ValueError: invalid block length'.

How can I encrypt & decrypt large data using Python's Twofish module?

>>> from twofish import Twofish
>>> key = binascii.unhexlify('8CACBE276491F6FF4B1EC0E9CFD52E76')
>>> t = Twofish(key)
>>> cipher_text = T.encrypt('deadbeaf12345678')
>>> plain_text = t.decrypt(cipher_text)
>>> plain_text
'deadbeaf12345678'
>>> cipher_text = t.encrypt('deadbeaf12345678hello world 1234')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/site-packages/twofish.py", line 69, in encrypt
    raise ValueError('invalid block length')
ValueError: invalid block length

Update: I am trying out another solution for this problem. I have created a Windows DLL, twofish.dll from Bruce Schneier's optimized C implementation (https://www.schneier.com/code/twofish-optimized-c.zip). Also, I have exported wrapper functions for encode & decode member functions using __declspec(dllexport).

I am loading this DLL in Python using ctype.CDLL function. The prototype for encode function is:

__declspec(dllexport) int encode(unsigned char *key, unsigned char *in, unsigned int inlen, unsigned char *out, unsigned int outbuflen, unsigned int& outlen)

How should I define the argument types in Python script?

import ctypes
import binascii
import requests

twofish_dll = ctypes.CDLL("twofish.dll")

encode = twofish_dll.encode

f = open('test.xml', 'r')
plain_text = f.read()
f.close()

cipher_text = ctypes.create_string_buffer(8192)
cipher_text_lenght = (ctypes.c_uint)()

KCS = '8CACBE276491F6FF4B1EC0E9CFD52E76'
key = binascii.unhexlify(KCS)

encode.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint, ctypes.c_char_p, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint)]
encode(ctypes.c_char_p(key), ctypes.c_char_p(plain_text), len(plain_text), cipher_text, 8192, ctypes.byref(cipher_text_lenght))

When executed the above code throws below error:

Traceback (most recent call last):
  File "C:\Data\sepm_test.py", line 21, in <module>
    encode(ctypes.c_char_p(key), ctypes.c_char_p(plain_text), len(plain_text), cipher_text, 8192, ctypes.byref(cipher_text_lenght))
TypeError: bytes or integer address expected instead of str instance
3

There are 3 best solutions below

0
On BEST ANSWER

I finally solved this problem by compiling Bruce Schneier's optimized C implementation for Twofish (https://www.schneier.com/code/twofish-optimized-c.zip) into a DLL and loading that DLL using ctypes module.

import ctypes
import binascii
import requests

twofish_dll = ctypes.CDLL("twofish.dll")

encode = twofish_dll.encode

f = open('test.xml', 'r')
plain_text = f.read()
f.close()
plain_text_buffer = ctypes.create_string_buffer(str.encode(plain_text))
plain_text_buffer_length = (ctypes.c_uint)(len(plain_text_buffer))

cipher_text_buffer = ctypes.create_string_buffer(8192)
cipher_text_buffer_length = (ctypes.c_uint)(len(cipher_text_buffer))
cipher_text_length = ctypes.c_uint(0)

KCS = '8CACBE276491F6FF4B1EC0E9CFD52E76'
key = binascii.unhexlify(KCS)

encode.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint, ctypes.c_char_p, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint)]
encode.restype = ctypes.c_int

encode(ctypes.c_char_p(key), plain_text_buffer, plain_text_buffer_length, cipher_text_buffer, cipher_text_buffer_length, ctypes.pointer(cipher_text_length))
1
On

Twofish is a block cipher which only encrypts 16 octets at a time. Quoth the documentation:

Create a twofish.Twofish instance with a key of length ]0, 32] and then use the encrypt and decrypt methods on 16 bytes blocks.

All values must be binary strings (str on Python 2, bytes on Python 3)

[WARNING] this should be used in a sensible cipher mode, like CTR or CBC. If you don't know what this means, you should probably use a higher level library.

Do pay attention to the warning, CBC or CTR is not rocket science, but if you use Twofish naively, its security is dreadfully compromised.

3
On

The below answer is kind of full solution with modular code. I am using this as it is in production with little configuration change.

Function - "argumentParse": takes the arguments as parses it. The program does both encryption and decryption. Below are the example how the code could be called for both encryption and decryption.

python TwofishEncryptDecrypt.py --encrypt --filename --keypath python TwofishEncryptDecrypt.py --decrypt --filename --keypath

Now - how does the Twofish works? Twofish is an encryption where block size of 16 is encrypted and keep concatenating the result until there are no more blocks present. Now there are situation when you file can not be equally divided by 16, then add some filler at the end which could be space and make last block as 16 and encrypt.

paddingBytesLength = self.BLOCK_SIZE - (len(fileContent) % self.BLOCK_SIZE)

For decryption it is just reverse, take block size of 16 decrypt and keep concatenating until there are no more encrypted block left. Now you could trim space from the end, since it is possible during encryption some padding was used.

Happy coding.

'''
The encryption algo is "Twofish Encryption"
'''

import sys
from twofish import Twofish
import os

def usages():
    print('-------Usgaes of TwoFish encrypt/decrypt script------------')
    print('')
    print('    --help or -h to get help of the script')
    print('    --encrypt or -e to encrypt a file')
    print('    --decrypt or -d to decrypt a file')
    print('    --filename or -f filename to be encrypted or decrypted')
    print('    --keypath or -k filename to pass keyfile name')
    print('')
    print('python TwofishEncryptDecrypt.py --encrypt --filename <FileName> --keypath <KeyFilename>')
    print('python TwofishEncryptDecrypt.py --decrypt --filename <FileName> --keypath <KeyFilename>')
    print('')
    print('-----------------------------End---------------------------')

def argumentParse(argsList):
    if '--help' in argsList or '-h' in argsList:
        usages()
        sys.exit(0)
    elif ('-e' in argsList and '-d' in argsList) or ('--encrypt' in argsList and '--decrypt' in argsList):
        print('Both action not allowed together')
        usages()
        sys.exit(0)

    i = 0
    for item in argsList:
        if item == '-e' or item == '--encrypt':
            arguments.update({'action': 'encryption'})
        elif item == '-d' or item == '--decrypt':
            arguments.update({'action': 'decryption'})
        elif item == '-f' or item == '--filename':
            if os.path.exists(argsList[i+1]) and os.path.getsize(argsList[i+1])>0:
                arguments.update({'filename': argsList[i+1]})
            else:
                print("[No such file or Directory] or [File Might Be empty] :",argsList[i+1])
                pass
        elif item == '-k' or item == '--keypath':
            arguments.update({'keypath': argsList[i+1]})
        else:
            pass
        i+=1

    return arguments

class encryptDecrypt():

    BLOCK_SIZE = 16
    twofish_passphrase = ''

    def encryptTwofish(self, filename):
        fileContent = open(filename,'r').read()
        
        paddingBytesLength = self.BLOCK_SIZE - (len(fileContent) % self.BLOCK_SIZE)
        
        paddingBytes = ''
        for i in range(paddingBytesLength):
            #paddingBytes += '00'
            paddingBytes += ' '
        
        #fileContent = fileContent + bytearray.fromhex(paddingBytes).decode("utf-8")
        fileContent = fileContent.decode('utf-8') + paddingBytes
        
        iteration_count = int(len(fileContent) / self.BLOCK_SIZE)
        encryptedFileContent = ''.encode()
        for i in range(iteration_count):
            encryptedFileContent += self.T.encrypt(fileContent[self.BLOCK_SIZE * i : (i+1) * self.BLOCK_SIZE].encode())
        
        return encryptedFileContent
        
    def decryptTwofish(self, filename):
        decryptedFileContent = ''
        encryptedFileContent = open(filename,'rb').read()
        
        iteration_count = int(len(encryptedFileContent) / self.BLOCK_SIZE)
        
        for i in range(iteration_count):
            decryptedFileContent += self.T.decrypt(encryptedFileContent[self.BLOCK_SIZE * i : (i+1) * self.BLOCK_SIZE]).decode()
    
        return decryptedFileContent.strip()

    def readKey(self, path):
        keyValue = open(path,'r')
        for line in keyValue:
            self.twofish_passphrase=line.rstrip('\n')
            self.T = Twofish(self.twofish_passphrase)

# entry point
    def writeFile(self, filename, fileContent):
        resultFile = open(filename,'wb')
        resultFile.write(fileContent)
        resultFile.close()
    
if __name__ == "__main__":
    arguments = {}
    argsList = sys.argv
    arguments = argumentParse(argsList)

    if not 'keypath' in arguments or not 'action' in arguments or not 'filename' in arguments:
        usages()
        sys.exit()

    twofish_encrypt = encryptDecrypt()
    twofish_encrypt.readKey(arguments['keypath'])
    if (arguments['action'] == 'encryption'):
        twofish_encrypt.writeFile(arguments['filename'], twofish_encrypt.encryptTwofish(arguments['filename']))
    elif (arguments['action'] == 'decryption'):
        twofish_encrypt.writeFile(arguments['filename'], twofish_encrypt.decryptTwofish(arguments['filename']))

# Remove ending NULL character from decrypted string