Upgrade Ruby, find a bug

16 Feb 2017

There are lots of good reasons to upgrade Ruby versions, but recently I came across a new one - occasionally an upgrade will uncover a bug in my code.

I was doing a small-ish upgrade of a fairly new app from Ruby 2.3.3 to 2.4.0 and had budgeted about 10 minutes for it, when post-upgrade I noticed I could no longer create instances of a particular model. The model was a little interesting; it encrypted a field using the attr_encrypted gem. The error seemed related to that process; it was:

irb(main):001:0> [code to create a new instance]
ArgumentError: key must be 32 bytes
	from [blah]/.bundle/gems/encryptor-3.0.0/lib/encryptor.rb:72:in `key='
	from [blah]/.bundle/gems/encryptor-3.0.0/lib/encryptor.rb:72:in `crypt'
	from [blah]/.bundle/gems/encryptor-3.0.0/lib/encryptor.rb:36:in `encrypt'

This seemed odd because the key hadn't changed during the upgrade. But after getting Rails out of the equation - always a good idea to limit scope in these situations - it did appear to be a problem with the key:

$ ruby -ropenssl -e 'OpenSSL::Cipher.new("aes-256-gcm").key = File.read(".env").match(/=(.*)/)[1]'
-e:1:in `key=': key must be 32 bytes (ArgumentError)
	from -e:1:in `<main>'

A bit of searching uncovered that this was indeed a behavior change between 2.3 and 2.4 - certain openssl classes no longer silently truncate data. Which is good! So that part of the mystery was solved.

The next question, though, was 'why is the value too long?' It was supposed to be 32 bytes in length, and poking around at it, yup, it was. However, you probably noticed earlier I was loading it from a file named .env. That's because, in development and test modes, I'm using the dotenv gem to load that value. And that's where the problem was. The contents of the .env file was the standard format for specifying environment variables for that gem, so it looked something like:

FOOBAR_KEY=a\xAB # etc etc

But when dotenv read in the file, the sequence of characters on the right hand side of the = wasn't interpreted as hex values, because the file is being read, not eval'd. So those hex escapes were being escaped. Once loaded into memory, the RHS there would be something like a\\xAB followed by the rest of the value which had the same issue with other hex encoded characters. Instead of 32 bytes, the key value was 80+ bytes. And since Ruby 2.3 was truncating on assignment, I never noticed.

This isn't a bug in dotenv; it's a bug in the way I was thinking about how dotenv worked. The fix was (for development and test modes) to just generate a key limited to base-16 values, i.e., hexadecimal characters. So SecureRandom.hex(16) worked nicely.