Creating a manual cipher

Any thoughts on creating a home-made cipher rather than relying on imported libraries?

At the moment I have a combination of Base64 and Vigenère cipher but this uses only one key. Would be good to create a cipher using two keys together if possible. However, been scratching my head and can’t seem to come up with a way to implement that.

My thoughts are: coming up with a good cipher is really really hard. If this is for an actual security application, I strongly recommend using a modern and standardized secure cipher.

However! If you’re just in it for the love of the game, I think it’s a great exercise in problem solving.

When you say “two keys”, what do you mean exactly? Something like a public and private key?

No I was thinking of two keys to encrypt and the same two keys to decrypt.

Maybe do Vigenère twice with different keys? encrypt(encrypt(data, key1), key2)

As Dunc said, this is a terrible idea if you’re actually trying to secure things, but an awesome idea if you want to learn about how encryption works :slight_smile: I’m still a bit unsure as to why you specifically want to use two keys, but let’s say you’re trying to make a dual-custody lock and you want better security than Soviet era locks.

If you want two different people to have to enter their passwords in order to encrypt/decrypt something, the first thing you want to do is to hash those keys, because otherwise there’ll be a lot of detectable properties in the passwords themselves. So you’ll start with something like this:

>>> hashlib.sha256(b"password").hexdigest()
'5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'
>>> hashlib.sha256(b"secret").hexdigest()
'2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b'

Okay. So now, regardless of the passwords used, you have two encryption keys of predictable length. (Note that, in order to actually have 256 bits of real entropy, you would need an incredibly long passphrase; but using SHA256 is convenient.) The next question is, what do we do with those? The nearest equivalent to a Vignere cipher would be to XOR your message with these bytes - which is a pretty good idea, so I think you’re onto something there. So long as your message is less than 32 bytes long, we can just do that; we’ll use the binary version of the keys here rather than hex:

>>> key1 = hashlib.sha256(b"password").digest()
>>> key2 = hashlib.sha256(b"secret").digest()
>>> plaintext = b"Hello, world!"
>>> encrypted = bytes(p ^ k1 ^ k2 for p, k1, k2 in zip(plaintext, key1, key2))
>>> encrypted
b'=U)\xa7\xce\x19\x87\xe5\xb5q\x8aj\x06'
>>> encrypted.hex()
'3d5529a7ce1987e5b5718a6a06'

What have I done here? I’ve taken the first byte of the original text (“H”, 0x48) and done a bitwise XOR with the first byte of each key (0x5E and 0x2B). This results in the first byte of our encrypted text (“=”, 0x3D).

This is quite secure so long as (a) your passwords really do have good entropy, and (b) your plaintext is as long as, or shorter than, your hashed passwords. If you want to send longer messages, you need some way to turn a 32-byte encryption key into a much longer one. You could simply duplicate it, but then you’d risk someone figuring out the pattern and exploiting that to detect the keys. A better way might be to take the SHA256 of the key to form the next key, then take the hash of that for the third key, the hash of that for the fourth key, and so on.

There’s another problem here, though: you really only have one key, formed by combining the two. Have a think about how you might disrupt that; what could you do that would ensure that the two keys aren’t just forming two halves of the same key?

There’s a lot in here that’s fun to think about. But just remember, if you ACTUALLY want to secure something, use an encryption algorithm that’s had more people think about it than just you. The standard algorithms available in your libraries are tested, mathematically evaluated, and battle hardened. What you’re doing here is strictly for educational purposes - and on that basis, can be extremely worthwhile.

Two interesting ideas, thanks.

@mfile_bay
Entering Vigenère encryption twice (or more) with different keys would certainly enhance the level of security.

@Chris Angelico
The reason for having two keys is just to increase security. As this is just for user passwords the length is not an issue. When you say "you really only have one key"surely the answer then is to call the encryption routine twice, once with each key.

The other idea I have thought about would be to use str.translate() to combine with Vigenère encryption. My thought here is to create an input translate key from all 256 characters (or thereabouts), then randomise that string to form the output translate key, and then copy those two strings into the program.

You have certainly given me some suggestions to try out, many thanks.

Imagine this in terms of someone trying to break your encryption. I’ll use a very naive Caesar cipher as an example here.

  1. Plain text: “helloworld”
  2. Encrypt with key 3: “khoorzruog”
  3. Encrypt again with key 7: “rovvygybvn”

Okay, so I now give you that cipher text: “rovvygybvn”. You attempt to figure out the key.

  1. Cipher text: “rovvygybvn”
  2. Attempt various keys…
  3. Decrypt with key 10: “helloworld”

For this simple type of cipher, using two different keys is actually exactly the same as using a single key that is a combination of both. As an extreme example, rot13 might not be very good security, but if you use it TWICE… :slight_smile:

The XOR cipher that I used in my example is also vulnerable to this. There’s nothing in it that makes the two keys different, so there is actually only one key, and it’s a merge of the two keys.

So this is something to think about. How can you use these two keys differently so that there’s a real difference between them? There are a number of options here, but rather than list any of them, I want you to have a think and see if you can come up with one :slight_smile:

This is definitely a good place to start thinking. However, if there’s only one translation table for the entire message, then no matter how good that table is, the resulting cipher is vulnerable to simple dictionary attacks. In fact, it’s so vulnerable that this makes for an enjoyable game - I found https://api.razzlepuzzles.com/cryptogram which has some very useful tips. These tips work very nicely for alphabetic text, but they work for binary data too (though you’d need more of it to get enough information).

In the Caesar cipher example above, you’ll see that the double L in “HELLO” remains a double letter through all the changes. That’s incredibly significant and a great starting clue. Similarly, in any English text, far and away the most common letter is “e”, and if you have all byte values, a space comes between every pair of words, so if you start by picking out the most common byte value and assuming that that came from either " " or “e”, you will likely be correct.

(As I type this, I took all the paragraphs prior to this one and stuck them into a collections.Counter, and the result was 469 spaces, 258 ‘e’, and 226 ‘t’. That’s surprisingly close for ‘t’, usually there’s a much bigger gap.)

So, how do you make sure that two of the same letter end up as different letters in the result? Important thing to think about.

I think it means bytes(p ^ k1 ^ k2 for p, k1, k2 in zip(plaintext, key1, key2)) is equivalent to bytes(p ^ k3 for p, k3 in zip(plaintext, key3)) if key3 is a sequence of key1 and key2’s bytes xor-ed with each other (xor is associative, (a xor b) xor c = a xor (b xor c))

(note: I didn’t figure if this applies to Vigenère)

I think it does with the caveat that the combined key will have the length of lcm(len(k1) ,len(k2))

@Rosuav

In the Caesar cipher example above, you’ll see that the double L in “HELLO” remains a double letter through all the changes.

By the way the Vigenère cipher takes care of the repeated character issue..

Been playing about and a few more thoughts:

The hashlib & XOR solution is very clever but…

  • truncates extra characters after 32 if a long string is used
  • not sure that it provides any benefits over str.translate()

Now looking at ways to change the length of the string and, if I can find it, a method of changing the position of the characters. Possibly some form of string-elongating encoding; however the output from that is always going to be in hex, although that might not be important if that output if then passed through str.translate() or Vigenère afterwards.

Yes, that’s one of the limitations I mentioned; there are multiple ways to do it, but you will definitely need to do something.

The main benefit is that your cipher can’t be decrypted by simple frequency analysis. When you have a single translation table, you can look at the most common letters to figure out what the encryption key is.