I have a few suggestions regarding performance:
Choosing between 2 things n times is slow, especially when factoring in the fact that you are appending to an array n times.
I would take advantage of the
getrandbits method like so:
"""Generates a random key of bits (with 0s or 1s) of length n"""
A full explanation:
bin produces a binary string from an integer
getrandbits(n) produces an integer from n random bits
[2:] cuts out the '0b' that is put at the start of the string by
.zfill(n) pads the resulting string with 0's to force a length of n
With shorter strings you won't notice the performance difference but with a larger string the speed difference is immense.
timeit to get the following timings on my machine:
10000 bits, called 100 times:
1.12 seconds vs 0.003 seconds
100000 bits, called 100 times:
11.5 seconds vs 0.003 seconds
As you can see, my method is orders of magnitudes faster.
xor, you can use a similar approach:
def xor(m, k):
"""Given strings m and k of characters 0 or 1,
it returns the string representing the XOR
between each character in the same position.
This means that m and k should be of the same length.
Use this function both for encrypting and decrypting!"""
a = int(m, base=2)
b = int(k, base=2)
return bin(a ^ b)[2:].zfill(len(m))
Now ignoring that, let's discuss your general approach.
You convert a string of characters into a string of 0's and 1's. This is actually wasteful, as you are multiplying your memory usage immensely. The reason is that most characters take many bits to store. Thus, you are replacing one character with many 0 or 1 characters. Assuming pure ASCII, you might be using up to 7x the memory, and it will be quite a bit worse with arbitrary Unicode characters.
There's also another issue. Once you have your ciphertext, there isn't really a good way to recover the original string. Sure you can get the original bits back, but how do you know which how many or which bits correspond to which letter? You could solve this with padding, but that is an ugly solution that wastes even more memory.
Instead I propose the following solution:
Generate a string of characters (ASCII is fine, doesn't really matter too much) as a key. The key should have the same number of characters as the string. Then xor the
ord of the characters in the same spot and make a new character with the result (by using
chr). Join all these characters to form the cipher text.
My version of this (without comments/tests) is this:
return "".join(choice(string.printable) for _ in s)
def encrypt(string, key):
return "".join(chr(ord(i) ^ ord(j)) for (i, j) in zip(string, key))
def decrypt(ciphertext, key):
return encrypt(ciphertext, key)