Simple Encryption in Ruby without external gems
Asked Answered
T

9

37

I need a simple encryption for some text strings. I want to create coupon codes and make them look cool so subsequently created code should look very different. (And besides looking cool, it shouldn't be easy to guess a code.) But I want to be able to decrypt them again. So the algorithm must be reversible.

I alread tried some stuff with moving bits around so they look kind of random already. But two subsequent codes (just one bit different) of course look very similar.

Any suggestions? I would like to do that without using external gems.

Philip

Tanhya answered 8/11, 2010 at 22:52 Comment(1)
Just wondering, what's wrong about external gems?Parlous
T
7

The solution is kind of from scratch but based on this: https://math.stackexchange.com/questions/9508/looking-for-a-bijective-discrete-function-that-behaves-as-chaotically-as-possib

The simplest way presented is using a * x + b (mod 2^n)

Obviously this is no real encryption and really only useful if you want to create sequential coupon codes without using much code.

So to implement this, you first need to pick a, b and n. (a must be odd) For example a=17, b=37 and n=27. Also we need to find "a^(-1)" on "mod 2^n". It's possible to do this on https://www.wolframalpha.com using the ExtendedGcd function:

enter image description here

So the inverse of a is therefore 15790321. Putting all this together:

A=17
B=37
A_INV=15790321

def encrypt(x)
  (A*x+B)%(2**27)
end

def decrypt(y)
  ((y-B)*A_INV)%(2**27)
end

And now you can do:

irb(main):038:0> encrypt(4)
=> 105
irb(main):039:0> decrypt(105)
=> 4

Obviously we want the coupon codes to look cool. So 2 extra things are needed: start the sequence at 4000 or so, so the codes are longer. Also convert them into something alpha-numeric, that's also an easy one with Ruby:

irb(main):050:0> decrypt("1ghx".to_i(36))
=> 4000
irb(main):051:0> encrypt(4000).to_s(36)
=> "1ghx"

One nice additional property is that consecutive numbers are different enough that guessing is practically impossible. Of course we assume that the users are not crypto analysts and if someone indeed guesses a valid number, it's not the end of the world: :-)

irb(main):053:0> encrypt(4001).to_s(36)
=> "1gie"
irb(main):054:0> decrypt("1gie".to_i(36))
=> 4001

Let's try to naively "hack" it by counting from 1gie to 1gif:

irb(main):059:0* decrypt("1gif".to_i(36))
=> 15794322

That's completely out of range, there are just 2000 or so coupons anyways - not a million. :-) Also if I remember correctly one can experiment a bit with the parameters, so subsequent numbers look more chaotic.

(Pick a larger n for longer codes and vice-versa. Base 36 means 6 bits are needed for each character ("Math.log(36, 2)"). So n=27 allows for up to 5 characters.)

Tanhya answered 13/11, 2010 at 16:58 Comment(3)
Sorry I don't understand it. Could anyone make a further explanation or give a piece of sample code for it?Animalist
@Parlous Although this is already more than 8 years ago, I'll try to explain the requirements: a)no external gems probably because there were problems with the deployment (bundler used to be painful at the time, esp. w/ JRuby) b)coupon codes must be like 10 (simple) chars max because they must be typed by non-tech people. c) the notion of seq. must be present, i.e. at some place I expect to put in int. Technically this is crypto, the use case is unusual and non of the other answers satisfy the requirement. FWIW I think I picked up this solution from an SO comment IIRC and I deleted the srcTanhya
.. or you specifically use a horrible platform (cough .. cough .. shopify) that allows you to write ruby scripts but not include any external libraries. Answering your own question is not to show how smart you are but to provide answers to the community. So if you figure out a solution dont be selfish and keep it to yourself.Supercolumnar
S
53

You could use OpenSSL::Cipher

# for more info, see http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/Cipher.html

require 'openssl'
require 'digest/sha1'

# create the cipher for encrypting
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
cipher.encrypt

# you will need to store these for later, in order to decrypt your data
key = Digest::SHA1.hexdigest("yourpass")
iv = cipher.random_iv

# load them into the cipher
cipher.key = key
cipher.iv = iv

# encrypt the message
encrypted = cipher.update('This is a secure message, meet at the clock-tower at dawn.')
encrypted << cipher.final
puts "encrypted: #{encrypted}\n"

# now we create a sipher for decrypting
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
cipher.decrypt
cipher.key = key
cipher.iv = iv

# and decrypt it
decrypted = cipher.update(encrypted)
decrypted << cipher.final
puts "decrypted: #{decrypted}\n"

But the intermediate form doesn't lend itself well to printing


Given your thought that it would be nice if the intermediate form was the same length, you might just use a simple map of one char to another.

PLEASE UNDERSTAND THAT THIS IS NOT SECURE

You can easily brute force the key, but it seems to be congruent with your requirements.

class Cipher
  
  def initialize(shuffled)
    normal = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + [' ']
    @map = normal.zip(shuffled).inject(:encrypt => {} , :decrypt => {}) do |hash,(a,b)|
      hash[:encrypt][a] = b
      hash[:decrypt][b] = a
      hash
    end
  end

  def encrypt(str)
    str.split(//).map { |char| @map[:encrypt][char] }.join
  end
  
  def decrypt(str)
    str.split(//).map { |char| @map[:decrypt][char] }.join
  end
  
end

# pass the shuffled version to the cipher
cipher = Cipher.new ["K", "D", "w", "X", "H", "3", "e", "1", "S", "B", "g", "a", "y", "v", "I", "6", "u", "W", "C", "0", "9", "b", "z", "T", "A", "q", "U", "4", "O", "o", "E", "N", "r", "n", "m", "d", "k", "x", "P", "t", "R", "s", "J", "L", "f", "h", "Z", "j", "Y", "5", "7", "l", "p", "c", "2", "8", "M", "V", "G", "i", " ", "Q", "F"]

msg = "howdy pardner"

crypted = cipher.encrypt msg
crypted # => "1IzXAF6KWXvHW"

decrypted = cipher.decrypt crypted
decrypted # => "howdy pardner"
Stuck answered 8/11, 2010 at 23:53 Comment(5)
Thanks for your solution, though two very similar codes yield very similar encrypted results using your last solution...Tanhya
@Joshua Is there anyway to make your first example print out "regular" characters rather than gibberish? I imagine it'll need some type of encoding and decoding. Sorry, I'm not a Ruby guy, but was asked to modify some old code that's still running on 1.8.7.Caber
I have no idea what you mean by "regular" characters and "giberish". Every character in here is ASCII, are you getting some sort of unicode or you just don't want that particular set of characters?Stuck
Note that you do not have to store iv - just send it with the encrypted message. You can also encrypt it for added security, but use a separate key!Exciter
Note that if you use CBC and don't provide the iv, it sets iv to all zeroes, which is pretty iffy. Also note that the secret should probably be a large randomly generated string. And to the asker's original question, it probably makes more sense to just hash them, or generate random strings, and then save the random string with the coupon, you don't need to encrypt/decrypt when you know the messages.Stuck
C
30

If you don't need real encryption, you can use a simple cipher. (This can be used when you don't need security, or to encrypt short random/one-off strings.)

ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

#generated with ALPHABET.split('').shuffle.join
ENCODING = "MOhqm0PnycUZeLdK8YvDCgNfb7FJtiHT52BrxoAkas9RWlXpEujSGI64VzQ31w"

def encode(text)
  text.tr(ALPHABET, ENCODING)
end

def decode(text)
  text.tr(ENCODING, ALPHABET)
end
Connotation answered 29/7, 2015 at 22:7 Comment(0)
J
20

Optional method for encryption and decryption

gem 'activesupport'

require 'active_support'

key = SecureRandom.random_bytes(32)
crypt = ActiveSupport::MessageEncryptor.new(key)
encrypted_data = crypt.encrypt_and_sign("your password")
password = crypt.decrypt_and_verify(encrypted_data)
Johppah answered 9/5, 2018 at 9:57 Comment(0)
O
11

For basic encoding/decode purpose I guess ruby's inbuilt Base64 library can be handy:

2.2.1 :001 > require 'base64'
 => true 
2.2.1 :002 > str = "[email protected]"
 => "[email protected]" 
2.2.1 :003 > Base64.encode64(str)
 => "YWJjQGV4YW1wbGUuY29t\n" 

It also has the urlsafe version methods in case the encoded strings are to be used in urls.

Reference: http://ruby-doc.org/stdlib-2.3.0/libdoc/base64/rdoc/Base64.html

Ophthalmologist answered 17/2, 2016 at 12:25 Comment(4)
I like this simple solution. However, if you need to encode a Ruby Hash, you will need to use JSON.generate before Base64.encode64.Gusty
I never really understodo base64 encoding. It looks like what's happening is that we're going from a string to another string but I thought base64 encoding went from a sequence of bytes to a text string.Bacillary
A string is a sequence of bytes. An encoding is used for the string, e.g. UTF-8 and it determines the bytes that will be used to represent the code points of the string.Marilyn
Please note that Base64 is an encoding of bytes to ASCII. It is not a way to encrypt stuff. It is crystal clear given a Base64 decoder. No encryption key involved.Marilyn
T
7

The solution is kind of from scratch but based on this: https://math.stackexchange.com/questions/9508/looking-for-a-bijective-discrete-function-that-behaves-as-chaotically-as-possib

The simplest way presented is using a * x + b (mod 2^n)

Obviously this is no real encryption and really only useful if you want to create sequential coupon codes without using much code.

So to implement this, you first need to pick a, b and n. (a must be odd) For example a=17, b=37 and n=27. Also we need to find "a^(-1)" on "mod 2^n". It's possible to do this on https://www.wolframalpha.com using the ExtendedGcd function:

enter image description here

So the inverse of a is therefore 15790321. Putting all this together:

A=17
B=37
A_INV=15790321

def encrypt(x)
  (A*x+B)%(2**27)
end

def decrypt(y)
  ((y-B)*A_INV)%(2**27)
end

And now you can do:

irb(main):038:0> encrypt(4)
=> 105
irb(main):039:0> decrypt(105)
=> 4

Obviously we want the coupon codes to look cool. So 2 extra things are needed: start the sequence at 4000 or so, so the codes are longer. Also convert them into something alpha-numeric, that's also an easy one with Ruby:

irb(main):050:0> decrypt("1ghx".to_i(36))
=> 4000
irb(main):051:0> encrypt(4000).to_s(36)
=> "1ghx"

One nice additional property is that consecutive numbers are different enough that guessing is practically impossible. Of course we assume that the users are not crypto analysts and if someone indeed guesses a valid number, it's not the end of the world: :-)

irb(main):053:0> encrypt(4001).to_s(36)
=> "1gie"
irb(main):054:0> decrypt("1gie".to_i(36))
=> 4001

Let's try to naively "hack" it by counting from 1gie to 1gif:

irb(main):059:0* decrypt("1gif".to_i(36))
=> 15794322

That's completely out of range, there are just 2000 or so coupons anyways - not a million. :-) Also if I remember correctly one can experiment a bit with the parameters, so subsequent numbers look more chaotic.

(Pick a larger n for longer codes and vice-versa. Base 36 means 6 bits are needed for each character ("Math.log(36, 2)"). So n=27 allows for up to 5 characters.)

Tanhya answered 13/11, 2010 at 16:58 Comment(3)
Sorry I don't understand it. Could anyone make a further explanation or give a piece of sample code for it?Animalist
@Parlous Although this is already more than 8 years ago, I'll try to explain the requirements: a)no external gems probably because there were problems with the deployment (bundler used to be painful at the time, esp. w/ JRuby) b)coupon codes must be like 10 (simple) chars max because they must be typed by non-tech people. c) the notion of seq. must be present, i.e. at some place I expect to put in int. Technically this is crypto, the use case is unusual and non of the other answers satisfy the requirement. FWIW I think I picked up this solution from an SO comment IIRC and I deleted the srcTanhya
.. or you specifically use a horrible platform (cough .. cough .. shopify) that allows you to write ruby scripts but not include any external libraries. Answering your own question is not to show how smart you are but to provide answers to the community. So if you figure out a solution dont be selfish and keep it to yourself.Supercolumnar
S
2

I can recommend you uuencode and uudecode utils you can use them wuth standart ruby function pack:

str = "\007\007\002\abcde"
new_string = [str].pack("u")
original = new_string.unpack("u")

(sample from Hal Fulton's Ruby Way)

Sverige answered 8/11, 2010 at 23:11 Comment(1)
Well the strings need to be shorter and more random ;) E.g. the results of ["abcde"].pack("u") and ["abcde"].pack("u") differ by just one byte. Actually it would be nice if the result strings had the same length as the original strings.Tanhya
G
2

Do you really want to trust the user to give you back the right value? If you trust what the client gives you back and the user figures out your encryption scheme you'll be using data they provide. That sounds like a very bad idea.

It's not clear to me why you don't want to give them a key into a database that maps a random numbers, perhaps with some error correction properties, to the coupon discounts. That way you have control of the final result. They provide you a key, you look up the associated coupon and apply the coupon. In this way you're only using your own data and if you want to remove a coupon it's all on the server side.

If you keep all the key-codes you can also check that new codes are different from previously released ones.

Grouper answered 9/11, 2010 at 17:58 Comment(2)
With random keys the user could also try to brute-force a valid key. The problem is that I'm using Google Appengine and to create > 300 can result in Time-Out problems, in particular when I need to check other 1500 coupon codes that are already in the database. Actually I also got a simple algorithm for that, taking x -> a * x + b (mod 2^n), with that I don't even need to check for uniqueness.Tanhya
The brute force problem is no different that if something were encrypted. You could guess an encrypted code too. If your coupon state space is big enough it's not going to be worthwhile and the server should rate limit/block users that have too many bad guesses. As for checking, isn't that just a query that your proposed code isn't there already?Grouper
D
1

You can check all different ways of encryption/decryption using ruby in this gist: https://gist.github.com/iufuenza/183a45c601a5c157a5372c5f1cfb9e3e

If you don't want to use a gem, I would totally recommend Openssl as the most secure which is also very easy to implement as it has very good Ruby support.

Devilmaycare answered 30/7, 2017 at 22:30 Comment(0)
P
-2

I know that you are looking for a no-gem encryption, but still want to offer to those who are here and don't worry about using external gems. Try glogin (I'm the author):

require 'glogin/codec'
codec = GLogin:Codec.new('the secret')
encrypted = codec.encrypt('Hello, world!')
decrypted = codec.decrypt(encrypted)

It's based on OpenSSL and Base58.

Parlous answered 4/7, 2019 at 10:7 Comment(1)
That'd be great if your gem actually worked. require 'glogin/codec' NameError: uninitialized constant GLogin from /usr/local/lib/ruby/gems/2.5.0/gems/glogin-0.6.0/lib/glogin/codec.rb:33:in `<top (required)>' Caused by LoadError: cannot load such file -- glogin/codec from /usr/local/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'Marte

© 2022 - 2024 — McMap. All rights reserved.