Dwolla permits an app to solicit and store a user's PIN as a form of pre-authorization, but requires that it be encrypted. From the TOS:
PIN(s) must be encrypted in transit and at rest (this includes any and all backup mediums) using FIPS 140-2 standards (at a minimum)
Normally, I'd use Bcrypt to encrypt (actually, make a secure hash. Neil Slater, thanks for the correction) something (using bcrypt-ruby gem), such as a password. But if I encrypt with Bcrypt, then I'd have to transmit the hash, and of course that won't match what Dwolla is expecting and the PIN will be rejected.
How do you encrypt the PIN and unencrypt it for secure transmittal?
One of the answers in the question that Andrew links to below referenced OpenSSL:Cipher, and using that I can encrypt the PIN with the below code. But remaining questions then are:
pin = "1111" # this is what needs to be encrypted
#encryption:
cipher = OpenSSL::Cipher.new('AES-128-CBC') #=> #<OpenSSL::Cipher:0x00000100ef09d8>
cipher.encrypt
key = cipher.random_key #=> odd characters...
iv = cipher.random_iv #=> odd characters...
encrypted = cipher.update(pin) + cipher.final #=> odd characters...
#dcryption:
decipher = OpenSSL::Cipher::AES.new(128, :CBC)
decipher.decrypt
decipher.key = key
decipher.iv = iv
plain = decipher.update(encrypted) + decipher.final
puts plain == pin #=> true
So this is what I've found out. In Rails, generate the key just once and store as an environment variable (and when you deploy encrypt it). Generate a new iv (initialization vector) for each pin. Store the iv and the encrypted pin in the database.
You may want to convert the encrypted PIN and the IV to UTF8 in order to successfully save without changing how you set up your database. (Be default, they'll be generated as ASCII 8-bit).
Here is one way to do it inside your User model, but you may want to refactor since these are large methods:
def dwolla_pin # => this is to decrypt the PIN in order to use it
unless encrypted_dwolla_pin.nil?
decipher = OpenSSL::Cipher::AES.new(128, :CBC)
decipher.decrypt
decipher.key = ENV["ENCRYPT_KEY"]
# Convert IV from UTF8 (as stored) back to ASCII-8bit (for OpenSSL)
utf8_iv = self.iv_for_pin
decipher.iv = Base64.decode64(utf8_iv.encode('ascii-8bit'))
# Convert PIN from UTF8 (as stored) back to ASCII-8bit (for OpenSSL)
utf8_pin = self.encrypted_dwolla_pin
ascii_pin = Base64.decode64(utf8_pin.encode('ascii-8bit'))
dwolla_pin ||= decipher.update(ascii_pin) + decipher.final
end
end
def dwolla_pin=(new_pin) # => this is to encrypt the PIN in order to store it
return false unless valid_pin?(new_pin)
cipher = OpenSSL::Cipher.new('AES-128-CBC')
cipher.encrypt
cipher.key = ENV["ENCRYPT_KEY"]
# Create IV and convert to UTF-8 for storage in database
iv = cipher.random_iv
utf8_iv = Base64.encode64(iv).encode('utf-8')
self.update_attribute(:iv_for_pin, utf8_iv)
# Encrypt PIN and convert to UTF-8 for storage in database
encrypted_pin = cipher.update(new_pin) + cipher.final
utf8_pin = Base64.encode64(encrypted_pin).encode('utf-8')
self.update_attribute(:encrypted_dwolla_pin, utf8_pin)
end
def valid_pin?(pin) # => Here I'm just checking to make sure the PIN is basically in the right format
pin.match(/^\d{4}/) && pin.length == 4
end
"Secure transit" means SSL for usage and SSH for deployment. If deploying to Heroku then already using SSH, but for SSL you will need to buy from your DNS host wildcard cert and the ssl endpoint on Heroku.
Does anyone have anything to add to this?