I wrote this just out of curiosity, and as something to distract myself from the routine tasks…
One of the simplest and most widely known encryption techniques is a Caesar cipher (Caesar cipher - Wikipedia)
and it’s descendant, ROT13 (ROT13 - Wikipedia).
But for extra security, instead of a static shift, we can use a password/passphrase, which will determine the shift size.
A simplest example would then be as follows:
message = "The quick brown fox jumps over the lazy dog"
cipher = "You can use Rails.application.secret_key_base here,
or any other super-secret string.
It can be as long or as short as you like, since when we
get to the end of the phrase, we can always
rotate to the beginning of it."
To iterate over the charaters in the message
, we need to convert it to array of bytes:
message_bytes = message.bytes
=> [84, 104, 101, 32, 113, 117, 105, 99, 107, 32...
Now we have to iterate through each byte, adding to it’s integer value the integer value of corresponding byte from passphrase:
# T h e q u i c k
[84, 104, 101, 32, 113, 117, 105, 99, 107, 32...
+
# Y o u c a n u s
[89, 111, 117, 32, 99, 97, 110, 32, 117, 115...
encrypted_bytes = message.bytes.map.with_index do |byte, position|
byte + cipher.bytes.at(position % cipher.bytes.length)
end
=> [173, 215, 218, 64, 212, 214, 215, 131, 224, 147...
# ` % cipher.bytes.length` is there to ensure we're looping
# the cipher over from the beginning when the message is
# longer than used cipher
OK, it’s a sequence of integers, but how can we get it to be a string again?
Simplest option would be to just get the char value of a byte, and be done with it:
encrypted_string = encrypted_bytes.map(&:chr).join
=> "\xAD\xD7\xDA@\xD4\xD6\xD7\x83\xE0\x93...
OK, well, this is not very readable, nor printable, nor can be used in the URL…
So, let’s use Base64.urlsafe_encode64
then:
url_safe_string = Base64.urlsafe_encode64(encrypted_string)
=> "rdfaQNTW14Pgk8eSwdjXjNmd2ZDa4dbT1JTY5dOgk9nL14XgwOXemcPRyA"
Alright, we got the url-safe string! Now let’s decipher it back to message:
encrypted_string = Base64.urlsafe_decode64(url_safe_string)
=> "\xAD\xD7\xDA@\xD4\xD6\xD7\x83\xE0\x93..."
encrypted_bytes = encrypted_string.bytes
=> [173, 215, 218, 64, 212, 214, 215, 131, 224, 147...
decrypted_bytes = encrypted_bytes.map.with_index do |byte, position|
byte - cipher.bytes.at(position % cipher.bytes.length)
end
=> [84, 104, 101, 32, 113, 117, 105, 99, 107, 32...
decrypted_bytes.map(&:chr).join
=> "The quick brown fox jumps over the lazy dog"
And that’s it!
To put it nicely:
def encrypt(message, passphrase)
pass_bytes = passphrase.bytes
pass_length = pass_bytes.length
Base64.urlsafe_encode64(
message.bytes.map.with_index { |byte, position|
(byte + pass_bytes.at(position % pass_length)).chr
}.join,
padding: false
)
end
def decrypt(message, passphrase)
pass_bytes = passphrase.bytes
pass_length = pass_bytes.length
Base64.urlsafe_decode64(message).bytes.map.with_index{ |byte, position|
(byte - pass_bytes.at(position % pass_length)).chr
}.join
end
encrypted_message = encrypt(message, passphrase)
decrypt(encrypted_message, passphrase) == message
=> true
This approach can then be used to encrypt any data structure that can be Marshaled:
data = {id: 123456789, purpose: "'tis a secret!"}
encrypted_data = encrypt(Marshal.dump(data), passphrase)
# "XXfwJ51o14Ted3rtrWijeOOj0-Df386sg4eQ49ehk8aD5crX0dDZmmWcZ7i5"
decrypted_data = Marshal.load(decrypt(encrypted_data, passphrase))
decrypted_data == data
=> true
And Bob’s your uncle!
