class EenyMeeny::Encryptor
Encrypts messages with authentication
The use of authentication is essential to avoid Chosen Ciphertext Attacks. By using this in an encrypt then MAC form, we avoid some attacks such as e.g. being used as a CBC padding oracle to decrypt the ciphertext.
Public Class Methods
Create the encryptor
Pass in the secret, which should be at least 32-bytes worth of entropy, e.g. a string generated by `SecureRandom.hex(32)`. This also allows specification of the algorithm for the cipher and MAC. But don't change that unless you're very sure.
# File lib/eeny-meeny/models/encryptor.rb, line 18 def initialize(secret, cipher = 'aes-256-cbc', hmac = 'SHA256') @cipher = cipher @hmac = hmac # use the HMAC to derive two independent keys for the encryption and # authentication of ciphertexts It is bad practice to use the same key # for encryption and authentication. This also allows us to use all # of the entropy in a long key (e.g. 64 hex bytes) when straight # assignement would could result in assigning a key with a much # reduced key space. Also, the personalisation strings further help # reduce the possibility of key reuse by ensuring it should be unique # to this gem, even with shared secrets. @encryption_key = hmac("EncryptedCookie Encryption", secret) @authentication_key = hmac("EncryptedCookie Authentication", secret) end
Public Instance Methods
decrypts base64 encoded ciphertext
First, it checks the message tag and returns nil if that fails to verify. Otherwise, the data is passed on to the AES function for decryption.
# File lib/eeny-meeny/models/encryptor.rb, line 50 def decrypt(ciphertext) ciphertext = ciphertext.unpack('m').first tag = ciphertext[0, hmac_length] ciphertext = ciphertext[hmac_length..-1] # make sure we actually had enough data for the tag too. if tag && ciphertext && verify_message(tag, ciphertext) decrypt_ciphertext(ciphertext) else nil end end
Encrypts message
Returns the base64 encoded ciphertext plus IV. In addtion, the message is prepended with a MAC code to prevent chosen ciphertext attacks.
# File lib/eeny-meeny/models/encryptor.rb, line 39 def encrypt(message) # encrypt the message encrypted = encrypt_message(message) [authenticate_message(encrypted) + encrypted].pack('m0') end
Private Instance Methods
returns the message authentication tag
This is computed as HMAC(authentication_key, message)
# File lib/eeny-meeny/models/encryptor.rb, line 77 def authenticate_message(message) hmac(@authentication_key, message) end
Decrypt
Pulls the IV off the front of the message and decrypts. Catches OpenSSL errors and returns nil. But this should never happen, as the verify method should catch all corrupted ciphertexts.
# File lib/eeny-meeny/models/encryptor.rb, line 107 def decrypt_ciphertext(ciphertext) aes = OpenSSL::Cipher.new(@cipher).decrypt aes.key = @encryption_key iv = ciphertext[0, aes.iv_len] aes.iv = iv crypted_text = ciphertext[aes.iv_len..-1] return nil if crypted_text.nil? || iv.nil? aes.update(crypted_text) << aes.final rescue nil end
Encrypt
Encrypts the given message with a random IV, then returns the ciphertext with the IV prepended.
# File lib/eeny-meeny/models/encryptor.rb, line 94 def encrypt_message(message) aes = OpenSSL::Cipher.new(@cipher).encrypt aes.key = @encryption_key iv = aes.random_iv aes.iv = iv iv + (aes.update(message) << aes.final) end
HMAC digest of the message using the given secret
# File lib/eeny-meeny/models/encryptor.rb, line 66 def hmac(secret, message) OpenSSL::HMAC.digest(@hmac, secret, message) end
# File lib/eeny-meeny/models/encryptor.rb, line 70 def hmac_length OpenSSL::Digest.new(@hmac).size end
verifies the message
This does its best to be constant time, by use of the rack secure compare function.
# File lib/eeny-meeny/models/encryptor.rb, line 85 def verify_message(tag, message) own_tag = authenticate_message(message) Rack::Utils.secure_compare(tag, own_tag) end