2
\$\begingroup\$

After doing the Cryptopals challanges, which I found scrolling through codereview, and finally being able to solve set1 challenge 6. I decided to have a little xor fun, and make an automated xor encryptor/decryptor.

Usage is pretty straightforward

  • You can encrypt from a file to another file
  • Or try to decrypt from a file.

import binascii
import base64
import string

from itertools import cycle
import os.path

import argparse

KEY_MIN = 2
KEY_MAX = 40
FREQ_ENG = {
             'a': 248362256,
             'e': 390395169,
             't': 282039486,
             'o': 235661502,
             'z': 2456495,
             ' ': 700000000
           }

# xor
xor = lambda b1, b2: bytearray(i ^ j for i, j in zip(b1, b2))

# calculate hamming distance for 2 strings
ham = lambda s1, s2: sum(bin(byte).count("1") for byte in xor(s1, s2))

# normalize by hamming distance
normal = lambda b1, b2, b3, b4, k: float(
                                    ham(b1, b2) +
                                    ham(b2, b3) +
                                    ham(b3, b4)
                                ) / (k * 3)

# Scoring text for english
score = lambda s, FREQ: sum(FREQ[c] for c in s.lower() if c in FREQ)

# Succes
succes = lambda plaintext: all(c in string.printable for c in plaintext)


def break_single_key_xor(b1):
    """Break signle key char by scoring the xor."""

    max_score = english_plaintext = key = None
    for i in range(256):
        plaintext = bytes(xor(b1, [i] * len(b1)))
        pscore = score(plaintext, FREQ_ENG)

        if pscore > max_score or not max_score:
            max_score = pscore
            english_plaintext = plaintext
            key = chr(i)

    return key


def normalize_keysizes(b):
    """Guess the possible keysize by normalizing the hamming distance."""

    normalize = []
    for KEYSIZE in range(KEY_MIN, KEY_MAX):
        b1 = b[: KEYSIZE]
        b2 = b[KEYSIZE: KEYSIZE*2]
        b3 = b[KEYSIZE*2: KEYSIZE*3]
        b4 = b[KEYSIZE*3: KEYSIZE*4]
        normalize.append((KEYSIZE, normal(b1, b2, b3, b4, KEYSIZE)))

    return sorted(normalize, key=lambda x: x[1])


def find_possible_key(KEYSIZE, norm, b):
    """Guess the possible key, by breaking single_key xor and scoring result."""

    print ('[*] Guessing the key')
    block_bytes = [[] for _ in range(KEYSIZE)]
    for i, byte in enumerate(b):
        block_bytes[i % KEYSIZE].append(byte)

    keys = ""
    for bbytes in block_bytes:
        keys += break_single_key_xor(bbytes)

    key = bytearray(keys * len(b))

    print ('[*] Possible key:"{}" with size {}'.format(keys, KEYSIZE))
    return key


def decrypt(fin, fout):
    """"Decrypt a file that has been xor encrypted."""

    print ('[*] Reading encrypted file')
    b = bytearray("".join(list(open(fin, "r"))).decode("base64"))

    print ('[*] Normalizing the text with hamming distance.')
    normalize = normalize_keysizes(b)
    print ('[*] Possible keys ordered by the norm.\n')

    for KEYSIZE, norm in normalize:
        print ('[*] KEYSIZE: {} \n[*] Norm: {}'.format(KEYSIZE, norm))
        key = find_possible_key(KEYSIZE, norm, b)
        plaintext = bytes(xor(b, key))

        # If the text is printable it is correct
        if succes(plaintext):
            with open(fout, "w") as f:
                f.write(plaintext)
            print ('[!] Succes')
            print ('[*] Wrote decrypted text to {}'.format(fout))
            break
        else:
            print ('[!] Failed key was incorrect\n')


def encrypt(fin, key, fout):
    """Encrypt a file by xor'ing it with the key."""

    print ('[*] Encrypting file:{} with key "{}"'.format(fin, key))
    if os.path.isfile(fin):
        fin = "".join(list(open(fin, "r")))

    cyphered = "".join(chr(ord(c) ^ ord(k)) for c, k in zip(fin, cycle(key))).encode("base64")
    print ('[*] Wrote encrypted text to {} \n'.format(fout))
    with open(fout, "w") as f:
        f.write(cyphered)


def parse_arguments():
    """Arguments parser."""

    parser = argparse.ArgumentParser(usage='%(prog)s [options] <filename_in> <filename_out>',
                                     description='Fun with xor by @Ludisposed',
                                     formatter_class=argparse.RawDescriptionHelpFormatter,
                                     epilog=
'''
Examples:
python xor_fun.py -e -p password plain.txt encrypted.txt
python xor_fun -d encrypt.txt decrypted.txt
''')
    parser.add_argument('-e', '--encrypt', action="store_true", help='encrypt filename')
    parser.add_argument('-d', '--decrypt', action="store_true", help='decrypt filename')
    parser.add_argument('-p', '--password', type=str, help='password to encrypt with')
    parser.add_argument('--keymin', type=int, help='Minimum keysize to look for')
    parser.add_argument('--keymax', type=int, help='Maximum keysize to look for')
    parser.add_argument('filename_in', type=str, help='file in')
    parser.add_argument('filename_out', type=str, help='file out')
    args = parser.parse_args()

    if args.keymin is not None:
        global KEY_MIN
        KEY_MIN = args.keymin
    if args.keymax is not None:
        global KEY_MAX
        KEY_MAX = args.keymax

    if not args.encrypt ^ args.decrypt:
        parser.error('Incorrect encrypt/decrypt mode')
    if args.encrypt and args.password is None:
        parser.error('Specify password parameter')
    return args


def main():
    """"Main function."""
    args = parse_arguments()
    if args.encrypt:
        encrypt(args.filename_in, args.password, args.filename_out)
    if args.decrypt:
        decrypt(args.filename_in, args.filename_out)

if __name__ == '__main__':
    main()

NOTE!

This will only work for english text.

And the text has to be long enough for my program to detect it as such.

Example of plain_english.txt that can be encrypted/decrypted

Hey do it now yeah hey

Yeah, there was a funky singer
Playin' in a rock and roll band
And never had no problems yeah
Burnin' down one night stands
And everything around me, yeah
Got to stop to feelin' so low
And I decided quickly, yes I did
To disco down and check out the show
Yeah they was

Dancin' and singin' and movin' to the groovin'
And just when it hit me somebody turned around and shouted
Play that funky music white boy
Play that funky music right
Play that funky music white boy
Lay down that boogie and play that funky music till you die
Till you die, oh till you die

I tried to understand this
I thought that they were out of their minds
How could I be so foolish (How could I)
To not see I was the one behind
So still I kept on fighting
Well, loosing every step of the way
I said, I must go back there (I got to go back)
And check to see if things still the same
Yeah they was dancin' and singin' and movin' to the groovin'
And just when it hit me somebody turned around and shouted
Play that funky music white boy
Play that funky music right
Play that funky music white boy
Lay down the boogie and play that funky music till you die
Till you die, ya
Till you die

Now first it wasn't easy
Changin' Rock and Roll and minds and things were getting shaky
I thought I'd have to leave it behind
But now its so much better (it's so much better)
I'm funking out in every way
But I'll never lose that feelin' (no I won't)
Of how I learned my lesson that day
When they were dancin' and singin' and movin' to the groovin'
And just when it hit me somebody turned around and shouted
Play that funky music white boy
Play that funky music right
Play that funky music white boy
Lay down the boogie and play that funky music till you die
Till you die
Oh' till you die

They shouted play that funky music
(Play that funky music)
Play that funky music
(You Gotta keep on playin' funky music)
Play that funky music
(Play that funky music)
Play that funky music
(Come and take you higher, ya)
Play that funky music white boy
Play that funky music right, ya
Play that funky music white boy
Play that funky music right
Play that funky music white boy
Play that funky music right

I am really happy with the way it works at the moment, but I have some questions about:

  1. My use of lambda's PEP8 discourages this, but why?
  2. The scoring of the language is a bit 'hacky' and incorrect, is there another way of tackling this problem
  3. Any general code/style review

Feel free to ask any questions if need be...

\$\endgroup\$
  • \$\begingroup\$ "1. My use of lambda's PEP8 discourages this, but why?" Aren't you asking the wrong people? \$\endgroup\$ – Mast Sep 25 '17 at 14:04
  • \$\begingroup\$ Maybe just wondering if it is bothersome to read for other people. I don't find it bothersome and I will say no thank you to PEP if I cannot find a valid reason... I like lambda's \$\endgroup\$ – Ludisposed Sep 25 '17 at 14:10
  • \$\begingroup\$ Which exact PEP are you referring to anyway? I wasn't familiar with the PEP being against lambda's. Generator expressions aren't outlawed either to the best of my knowledge and I've always considered them familiar to one another. \$\endgroup\$ – Mast Sep 25 '17 at 14:14
  • 1
    \$\begingroup\$ "Always use a def statement instead of an assignment statement that binds a lambda expression directly to an identifier." PEP8 \$\endgroup\$ – Ludisposed Sep 25 '17 at 14:16
  • 2
    \$\begingroup\$ You can use lambda, but it defeats the point of making an anonymous function, if your going to remove the anonymity. Also you can't easily add useful meta data, whilst having other quirks. Even in JavaScript it's nicer to read function fn() {}, rather than const fn = function() {}. \$\endgroup\$ – Peilonrayz Sep 25 '17 at 14:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.