encryptor: ArgumentError: key must be 32 bytes

I’m using encryptor via attr-encrypted in a Rails 5 application. While evaluating an upgrade to Ruby 2.4.0, I came across the following error coming out of this gem. Not sure if this is a bug, or something I’m doing wrong, or some change related to Ruby 2.4.0 that requires some effort on my part.

From Ruby 2.4.0

[7] pry(main)> RUBY_VERSION
=> "2.4.0"
[8] pry(main)> secret_key = SecureRandom.random_bytes(64)
=> "\xF3\xA6\xE9\x91\xFD\x94\xCB\xBDH\xA9|\xDF\x04\xBF\xAC\x13+0\xB5\xAF`[\b\xE6\xEDw\xCDD\x97\x19\"\xD1\xB1\xFB\x8A\x8Cn\x84N\x05\xDCp\x1C\xA0o3\x9D\t\xFA\x1F\xC1\x1C&F\xFC\xB0,\xDB\xBE\xE1\x8E9\xD4\xA6"
[9] pry(main)> iv = SecureRandom.random_bytes(12)
=> "\xD5\x00\xB1Q.>)\xAE\xF0x\xBB\xA1"
[10] pry(main)> encrypted_value = Encryptor.encrypt(value: 'some string to encrypt', key: secret_key, iv: iv)
ArgumentError: key must be 32 bytes
from /home/darren/.gem/ruby/2.4.0/gems/encryptor-3.0.0/lib/encryptor.rb:72:in `key='

Previous behavior from Ruby 2.3.3

[1] pry(main)> RUBY_VERSION
=> "2.3.3"
[2] pry(main)> secret_key = SecureRandom.random_bytes(64)
=> "A\xFD\xBC\xA5\x1A\x8E\xD7\x17W\x00r5\x8CHv|\xA7\xFB6\xB8N\x9Fb\x93\xA4\x9Aw\x8E\bq\xBA\xFC\xEF\xA3\x9E\xE2\xED\xB1\b\xBC\xE3\xDA\xEA\xDB\xF2\xAC0\xDAh\xCE\x88/(\x16\xC9\xDDs9\xD11\xE5\xE9\t\\"
[3] pry(main)> iv = SecureRandom.random_bytes(12)
=> "\xC8\xE3\xBE\x91y\x95sF\xE6\x89\x8C\""
[4] pry(main)> encrypted_value = Encryptor.encrypt(value: 'some string to encrypt', key: secret_key, iv: iv)
=> "\x12\x9A\x81*U\xCFT\x91\xB7;\xAF\xF2I]\x9C@L\xD5\xB8;\x00\x87\xF3\x82yS(r\x90\xC8\x86\xBB\x13\x92\xA83$O"

These are both using encryptor 3.0.0.

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Comments: 25 (3 by maintainers)

Commits related to this issue

Most upvoted comments

@anaumov We were able to simply truncate our keys from their longer length to 32 bytes. It seems that while the Ruby SSL implementation allowed for keys longer than 32 bytes, it truncated them. The recent change is to raise an error for keys that aren’t exactly 32 bytes.

Obviously, you should test this to make sure it’s works for you 😃.

I don’t think that the issue really comes from Encryptor; afaik this is the change in the OpenSSL library that causes the error: https://github.com/ruby/ruby/commit/ce635262f53b760284d56bb1027baebaaec175d1

We’ve solved it by generating keys that are the correct size length; if OpenSSL requires 32 bytes, make sure you generate it with SecureRandom.random_bytes(32).

If you plan to store the keys in a YML file (or similar), you will probably have to resort to using Base64.

We did this:

namespace :openssl do
  desc "Generate Base64 encoded bytes for application.yml"
  task generate: :environment do
    {
      ENCRYPT_SECRET_KEY: 32,
      ENCRYPT_IV: 12,
      ENCRYPT_SALT: 128
    }.each do |k, length|
      value = Base64.encode64(SecureRandom.random_bytes(length)).delete("\n")
      puts "#{k}: '#{value}'"
    end
  end
end

Then in initializers/encryptor.rb:

# frozen_string_literal: true
Encryptor.default_options.merge!(
  algorithm: 'aes-256-gcm',
  key: Base64.decode64(ENV['ENCRYPT_SECRET_KEY']),
  iv: Base64.decode64(ENV['ENCRYPT_IV']),
  salt: Base64.decode64(ENV['ENCRYPT_SALT'])
)

I was able to upgrade to Rails 2.5.5, without changing my old keys, using @pduey approach

attr_encrypted :my_attribute, key: ENV['MY_KEY'].bytes[0..31].pack( "c" * 32 )

I think it’d be a big help to improve this line to warn people about this issue.

raise ArgumentError.new("key must be #{cipher.key_len} bytes or longer") if options[:key].bytesize < cipher.key_len

The “or longer” is not true in this case.

My key was 32 chars, but 41 bytes because the key was stored in UTF-8. I was able to convert it, truncate to 32 bytes, then store in yml using base64 as suggested by @consti . I converted it something like: Base64.encode64 Rails.application.secrets[my_key].bytes[0..31].pack( "c" * 32 )

@danielricecodes correct, there isn’t official support for Ruby 2.4 yet. People are using it at their own risk. I’ve started work to support it but I’ve hit a few issues.

@NullVoxPopuli

  1. add new columns for the field (encypted value and IV)
  2. generate a new proper length key, but don’t use the same variable (environment or otherwise)
  3. write code to write the value into both places, encrypted with old key and with new key
  4. write backfill/migration script to run in production that updates the new columns
  5. deploy to production
  6. run the backfill
  7. double-check your work to make sure it worked
  8. triple-check your work to make sure it worked
  9. remove the old columns and old key
  10. deploy your now fully migrated app

Or, at least that’s what I would do. This is a useful procedure to try and do, since it’s also how you deal with a possibly exposed secret.

Here’s a way I figured out can generate a 32 character key without using SecureRandom…

Just run this in rails console

  `rake secret`[0..31]

It will spit out a 32 byte string - just without all the random garbage SecureRandom will throw in there. You don’t have to Base64 encode that value either.

I downgraded my app to Ruby 2.3 as a workaround. IMHO, it appears the attr_encrypted gem does not support Ruby 2.4 right now.

So perhaps an Issue needs to be opened for Ruby 2.4 support for attr_encrypted? The gem should behave the same with Ruby 2.4 as it does with 2.3. I’m not a super expert at Ruby or encryption - otherwise I’d take a crack at it myself.

PS, I was testing with Ruby 2.4, Rails 5.0.2, and attr_encrypted 3.0.3. Downgraded to Ruby 2.3 because downgrading Ruby is better than releasing an app with plaintext social security numbers 😄

UPDATE (Sep 26th, 2017):

This gem seems to work now with Rails 5.0.6 and Ruby 2.4.2.

I think it’d be a big help to improve this line to warn people about this issue.

raise ArgumentError.new("key must be #{cipher.key_len} bytes or longer") if options[:key].bytesize < cipher.key_len

The “or longer” is not true in this case.

Are we planning to change key_len & iv_len checking logic? Code needs to check exact lengths. Happy to raise a PR if its correct approach.

a simple bundle update did it for me

@mvaragnat

Array#pack converts an integer array to a string. The parameter is a format string. In our case, it’s interpreting the first 32 elements of the array as a signed integer and printing it in string form. “c” * 32 is shorthand for “cccccccccccccccccccccccccccccccc”. Array#pack also accepts “c*” which says interpret the entire array, which would be safe in our case since we also used bytes[0…31].

Also, String#unpack, the converse of Array#pack, could be used instead of String#bytes.

@danielricecodes I don’t recommend doing that, but you’re welcome to do whatever you like.