Mimic AES_ENCRYPT and AES_DECRYPT functions in Ruby

Hello there!

I need to mimic what MySQL does when encrypting and decrypting strings
using built-in functions AES_ENCRYPT() and AES_DECRYPT().

Even though the application I am writing is a Rails application, I think
this question suited here better because, the encryption will take place
in Ruby and not necessarily depends on Rails.

I have read a couple of blog posts and apparently MySQL uses AES 128-bit
encryption for those functions. On top of that, since this encryption
requires a 16-bit key, MySQL pads the string with x0 chars (\0s) until
it’s 16-bit in size.

The algorithm in C from MySQL source code is spotted here:

I have even tried to examine MySQL’s C source code, but that didn’t help
me much, since I can’t really program in C. Maybe someone with a little
more experience can have some insights.

The source code that implements the encryption (rijndaelKeySetupEnc) and
decryption (rijndaelKeySetupDec) functions is here:

http://pastie.org/425070

And the actual AES_ENCRYPT (function my_aes_encrypt) and AES_DECRYPT
(my_aes_decrypt) source code is here:

http://pastie.org/425073

Please note that the necessity of using MySQL’s compliancy was not my
call and is not a choice. I need that in order to communicate properly
with a legacy application, and I don’t “own” this database. Please take
into consideration that security is definitely not the goal, talking to
that system properly is. The key length was not chosen by me and I know
it’s a little peculiar, as you’ll see below, on my replication “script”.

Now I need to replicate what MySQL does in a Rails application, but
every single thing I tried, doesn’t work.

Here’s a way to replicate the behavior I am getting (in this case, using
Rails):

  1. Create a new Rails app

rails encryption-test
cd encryption-test

  1. Create a new scaffolding

script/generate scaffold user name:string password:binary

  1. Edit your config/database.yml and add a test MySQL database

development:
adapter: mysql
host: localhost
database: test
user: <>
password: <>

  1. Run the migration

rake db:migrate

  1. Enter console, create an user and update its password from MySQL
    query

script/console
Loading development environment (Rails 2.2.2)

User.create(:name => “John D.”)
key = “82pjd12398JKBSDIGUSisahdoahOUASDHsdapdjqwjeASIduAsdh078asdASD087asdADSsdjhA7809asdajhADSs”
ActiveRecord::Base.connection.execute(“UPDATE users SET password = AES_ENCRYPT(‘password’, ‘#{key}’) WHERE name=‘John D.’”)

That’s where I got stuck. If I attempt to decrypt it, using MySQL it
works:

loaded_user = User.find_by_sql(“SELECT AES_DECRYPT(password, ‘#{key}’) AS password FROM users WHERE id=1”).first
loaded_user[‘password’]
=> “password”

However if I attempt to use OpenSSL library, there’s no way I can make
it work:

cipher = OpenSSL::Cipher::Cipher.new(“AES-128-ECB”)
cipher.padding = 0
cipher.key = key
cipher.decrypt

user = User.find(1)
cipher.update(user.password) << cipher.final #=>
“########gf####\027\227”

I have tried padding the key:

desired_length = 16 * ((key.length / 16) + 1)
padded_key = key + “\0” * (desired_length - key.length)

cipher = OpenSSL::Cipher::Cipher.new(“AES-128-ECB”)
cipher.key = key
cipher.decrypt

user = User.find(1)
cipher.update(user.password) << cipher.final #=>
“”|\e\261\205:\032s\273\242\030\261\272P##"

But it really doesn’t work.

Does anyone have a clue on how can I properly mimic whatever MySQL is
doing in Ruby?

Thanks a lot for your help.

Cheers,

– Felipe.

One additional information:

You may find weird that I tried padding the key, and even using \000
char to pad it. The reasoning behind it is the way MySQL documents those
two functions:

"AES_ENCRYPT() and AES_DECRYPT() allow encryption and decryption of data
using the official AES (Advanced Encryption Standard) algorithm,
previously known as “Rijndael.” Encoding with a 128-bit key length is
used, but you can extend it up to 256 bits by modifying the source. We
chose 128 bits because it is much faster and it is secure enough for
most purposes.

AES_ENCRYPT() encrypts a string and returns a binary string.
AES_DECRYPT() decrypts the encrypted string and returns the original
string. The input arguments may be any length. If either argument is
NULL, the result of this function is also NULL.

Because AES is a block-level algorithm, padding is used to encode uneven
length strings and so the result string length may be calculated using
this formula:

16 × (trunc(string_length / 16) + 1)

If AES_DECRYPT() detects invalid data or incorrect padding, it returns
NULL. However, it is possible for AES_DECRYPT() to return a non-NULL
value (possibly garbage) if the input data or the key is invalid."

Hope that also helps.

Thanks again.

– Felipe

On Mar 23, 2009, at 11:25 PM, Felipe C. wrote:

Because AES is a block-level algorithm, padding is used to encode
uneven
length strings and so the result string length may be calculated using
this formula:

16 × (trunc(string_length / 16) + 1)

Do you mean to have:

16 * (string_length + 15)/16

If the string length is 32, what do you expect the result to be? Your
formula gives 48, (16*(trunc(32/16)+1))==(16*(2+1)), while mine gives
32, (16*(32+15)/16)==(16*(47/16))==(16*2) [integer division].

Here’s a bit of code that I’ve lifted out of another project:

 # Encrypt the content of the document, block by block, in a manner
 # compatible with the original Python (so we can decrypt it and
 # remain backwardly compatible)
 rijndael = Crypt::Rijndael.new(self.key, 256, 256)
 encryptedData = ""
 data << 'X'            # a marker added to cope with partial block
 blocks, bytes = data.length.divmod(32)
 unless bytes.zero?
   data << "\0" * (32 - bytes)
   blocks += 1
 end
 (0...blocks).each do |block|
   encryptedData << rijndael.encrypt_block(data[block * 32, 32])
 end

The decrypting side was Java and I don’t know why the ‘X’ was chosen
(seems that I recall something about there being a byte with the
number of extra bytes of padding… or your formula might hold a clue).

Anyway, you’d have to adjust it for 128-bit/16-byte keys (and blocks).

-Rob

Rob B. http://agileconsultingllc.com
[email protected]

On 24 Mar, 04:21, Felipe C. [email protected] wrote:

cipher.key = key
cipher.decrypt

user = User.find(1)
cipher.update(user.password) << cipher.final #=>
“########gf####\027\227”

I use the following code for encrypt/decrypt:

@cipherAES256=OpenSSL::Cipher::AES256.new(“CBC”) if @cipherAES256.nil?
@cipherAES256.encrypt
@cipherAES256.key=key
ct = @cipherAES256.update(plainPassword) + @cipherAES256.final
password=ct.unpack(“H*”)[0]

@cipherAES256=OpenSSL::Cipher::AES256.new(“CBC”) if @cipherAES256.nil?
@cipherAES256.decrypt
@cipherAES256.key=key
ct = @cipherAES256.update([password].pack(“H*”)) + @cipherAES256.final

I don’t know if it can helps you (it uses 256 and CBC), try changing
your code from

cipher.update(user.password) << cipher.final
to
cipher.update([user.password].pack(“H*”)) << cipher.final

Giovanni

Giovanni / Rob,

Thanks a lot for your responses, really.

Unfortunately, it it still doesn’t work. I tried what you both
suggested, take a look at this console transcript:

def aes(m,k,t)
(aes = OpenSSL::Cipher::AES128.new(“ECB”).send(m)).key = k
aes.update(t) << aes.final
end
=> nil

?> def encrypt(key, text)

aes(:encrypt, key, text)
end
=> nil

?> def decrypt(key, text)

aes(:decrypt, key, text)
end
=> nil

key = “82pjd12398JKBSDIGUSisahdoahOUASDHsdapdjqwjeASIduAsdh078asdASD087asdADSsdjhA7809asdajhADSs”
=>
“82pjd12398JKBSDIGUSisahdoahOUASDHsdapdjqwjeASIduAsdh078asdASD087asdADSsdjhA7809asdajhADSs”

u = User.find(1)
=> #<User id: 1, name: “John D.”, password:
“###\270##\206ή5\202?\003\021###”, created_at: “2009-03-23 20:31:43”,
updated_at: “2009-03-23 20:31:43”>

u.password.length
=> 16

decrypt(key, u.password)
OpenSSL::CipherError: bad decrypt
from (irb):8:in final' from (irb):8:inaes’
from (irb):16:in `decrypt’
from (irb):19

decrypt(key, [u.password].pack(“H*”))
OpenSSL::CipherError: wrong final block length
from (irb):8:in final' from (irb):8:inaes’
from (irb):16:in `decrypt’
from (irb):32

[u.password].pack(“H*”).length
=> 8

card = ([u.password].pack(“H*”) + ("\0" * 8))
=> “9##n###\005\000\000\000\000\000\000\000\000”

decrypt(key, card)
OpenSSL::CipherError: bad decrypt
from (irb):8:in final' from (irb):8:inaes’
from (irb):16:in `decrypt’
from (irb):43

Any other ideas?

Thanks,

– Felipe

Some more discoveries…

According to the blog post I sent before, here’s how MySQL works with
the key you provide AES_ENCRYPT / DECRYPT:

“The algorithm just creates a 16 byte buffer set to all zero, then loops
through all the characters of the string you provide and does an
assignment with bitwise OR between the two values. If we iterate until
we hit the end of the 16 byte buffer, we just start over from the
beginning doing ^=. For strings shorter than 16 characters, we stop at
the end of the string.”

I don’t know if you can read C, but here’s the mentioned snippet:

http://pastie.org/425161

Specially this part:

bzero((char*) rkey,AES_KEY_LENGTH/8); /* Set initial key */

for (ptr= rkey, sptr= key; sptr < key_end; ptr++,sptr++)
{
if (ptr == rkey_end)
ptr= rkey; /* Just loop over tmp_key until we used all key */
*ptr^= (uint8) *sptr;
}

So I came up with this method:

def mysql_key(key)

The algorithm just creates a 16 byte buffer set to all zero,

final_key = “\0” * 16

Number of string “blocks”

t = key.length / 16

t.times do |i|
# For each block
key_block = key[i*16, 16]

# Runs bitwise XOR for each char on string
# and the same char on the block
16.times do |j|
  final_key[j] ^= key_block[j]
end

end

final_key
end

But it still fails:

key = “82pjd12398JKBSDIGUSisahdoahOUASDHsdapdjqwjeASIduAsdh078asdASD087asdADSsdjhA7809asdajhADSs”
=>
“82pjd12398JKBSDIGUSisahdoahOUASDHsdapdjqwjeASIduAsdh078asdASD087asdADSsdjhA7809asdajhADSs”
mkey = mysql_key(key)
=> “\027\024GK\023P{#8?G!8[r.”
mkey.length
=> 16

decrypt(mkey, User.find(1).password)
User Load (11.3ms) SELECT * FROM users WHERE (users.id = 1)
OpenSSL::CipherError: bad decrypt
from (irb):4:in final' from (irb):4:in aes’
from (irb):12:in `decrypt’
from (irb):42

decrypt(mkey, [User.find(1).password].pack(“H*”))
User Load (2.8ms) SELECT * FROM users WHERE (users.id = 1)
OpenSSL::CipherError: wrong final block length
from (irb):4:in final' from (irb):4:in aes’
from (irb):12:in `decrypt’
from (irb):43

Question is: did I miss something :slight_smile: ?

I have a feeling I am almost there…

Thanks again!

– Felipe

On Mar 24, 2009, at 2:35 AM, Felipe C. wrote:

Posted via http://www.ruby-forum.com/.

I’m glad you got it. Your key-building function doesn’t need to be
quite so complex:

def mysql_key2(key)
final_key = “\0” * 16
key.length.times do |i|
final_key[i%16] ^= key[i]
end
final_key
end

Hardly needs any comments now :wink: Just a pointer to the MySQL doc
perhaps.

irb> mkey2 = mysql_key2(key)
=> “dp&!{\021?pK?G!8[r.”
irb> mkey == mkey2
=> true

-Rob

Rob B. http://agileconsultingllc.com
[email protected]

Just as a FYI, it works!!!

I forgot about the remains… Take a look at the final incarnation:

def mysql_key(key)

The algorithm just creates a 16 byte buffer set to all zero,

final_key = “\0” * 16

Number of string “blocks”

blocks, remain = key.length.divmod(16)

blocks.times do |i|
# For each block
key_block = key[i*16, 16]

# Runs bitwise XOR for each char on string
# and the same char on the block
16.times do |j|
  final_key[j] ^= key_block[j]
end

end

if remain
remain.times do |i|
final_key[i] ^= key[(blocks * 16) + i]
end
end

final_key
end

And:

mkey = mysql_key(key)
=> “dp&!{\021?pK?G!8[r.”

decrypt(mkey, User.find(1).password)
User Load (2.9ms) SELECT * FROM users WHERE (users.id = 1)
=> “password”

Just BEATIFUL!

Thanks a lot everyone!

Rob B. wrote:

I’m glad you got it. Your key-building function doesn’t need to be
quite so complex:

Rob, you nailed it. Thanks a lot!

Best regards,

– Felipe

Having no luck with this. Getting the following error when generating
they key in 1.8.7 and 1.9.1. Any help would be much appreciated.

ruby-1.9.1-p378 > key = “test_key”
=> “test_key”
ruby-1.9.1-p378 > final_key = “\0” * 16
=> “\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00”
ruby-1.9.1-p378 > key.length.times do |i|
ruby-1.9.1-p378 > final_key[i%16] ^= key[i]
ruby-1.9.1-p378 ?> end
NoMethodError: undefined method ^' for "\x00":String from (irb):89:in block in irb_binding’
from (irb):88:in `times’
from (irb):88

ruby-1.9.1-p378 > final_key
=> “\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00”

Rob B. wrote:

On Mar 24, 2009, at 2:35 AM, Felipe C. wrote:

Posted via http://www.ruby-forum.com/.

I’m glad you got it. Your key-building function doesn’t need to be
quite so complex:

def mysql_key2(key)
final_key = “\0” * 16
key.length.times do |i|
final_key[i%16] ^= key[i]
end
final_key
end

Hardly needs any comments now :wink: Just a pointer to the MySQL doc
perhaps.

irb> mkey2 = mysql_key2(key)
=> “dp&!{\021?pK?G!8[r.”
irb> mkey == mkey2
=> true

-Rob

Rob B. http://agileconsultingllc.com
[email protected]

On Jul 1, 2010, at 11:34 AM, Joshua Mckinney wrote:

final_key[i%16] ^= key[i]

=> true
ruby-1.9.1-p378 > key = “test_key”

ruby-1.9.1-p378 > final_key
=> “\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00”

Because in Ruby 1.8.6, “hello”[0] is 104, but in 1.8.7 and 1.9.x,
“hello”[0] is “h”

Change that line to:

final_key[i%16] = (final_key[i%16].ord ^ key[i].ord).chr

And you should get the right answer:

irb> x=“hello”
=> “hello”
irb> x[0] = (x[0].ord ^ 0x20).chr
=> “H”
irb> x
=> “Hello”

String#ord gives the Fixnum value of a single-character string.
Fixnum#chr gives the single-character String whose #ord is the Fixnum

(I’m sure the actual docs say that better :wink:

-Rob

Rob B.
http://agileconsultingllc.com
[email protected]

[email protected]

Correction: in 1.8.7 no error is produced but the final_key is not
correct:

ruby-1.8.7-p174 > key = “test key”
=> “test key”
ruby-1.8.7-p174 > final_key = “\0” * 16
=> “\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000”
ruby-1.8.7-p174 > key.length.times do |i|
ruby-1.8.7-p174 > final_key[i%16] ^= key[i]
ruby-1.8.7-p174 ?> end
=> 8
ruby-1.8.7-p174 > final_key
=> “test key\000\000\000\000\000\000\000\000”

Joshua Mckinney wrote:

Having no luck with this. Getting the following error when generating
they key in 1.8.7 and 1.9.1. Any help would be much appreciated.

ruby-1.9.1-p378 > key = “test_key”
=> “test_key”
ruby-1.9.1-p378 > final_key = “\0” * 16
=> “\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00”
ruby-1.9.1-p378 > key.length.times do |i|
ruby-1.9.1-p378 > final_key[i%16] ^= key[i]
ruby-1.9.1-p378 ?> end
NoMethodError: undefined method ^' for "\x00":String from (irb):89:inblock in irb_binding’
from (irb):88:in `times’
from (irb):88

ruby-1.9.1-p378 > final_key
=> “\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00”

Thanks for the reply, we got rid of the error in 1.9.1 but both 1.9.1
and 1.8.7 produce the wrong final_key

ruby-1.9.1-p378 > final_key
=> “test key\x00\x00\x00\x00\x00\x00\x00\x00”

ruby-1.8.7-p174 > final_key
=> “test key\000\000\000\000\000\000\000\000”

Rob B. wrote:

On Jul 1, 2010, at 11:34 AM, Joshua Mckinney wrote:

final_key[i%16] ^= key[i]

=> true
ruby-1.9.1-p378 > key = “test_key”

ruby-1.9.1-p378 > final_key
=> “\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00”

Because in Ruby 1.8.6, “hello”[0] is 104, but in 1.8.7 and 1.9.x,
“hello”[0] is “h”

Change that line to:

final_key[i%16] = (final_key[i%16].ord ^ key[i].ord).chr

And you should get the right answer:

irb> x=“hello”
=> “hello”
irb> x[0] = (x[0].ord ^ 0x20).chr
=> “H”
irb> x
=> “Hello”

String#ord gives the Fixnum value of a single-character string.
Fixnum#chr gives the single-character String whose #ord is the Fixnum

(I’m sure the actual docs say that better :wink:

-Rob

Rob B.
http://agileconsultingllc.com
[email protected]
http://gaslightsoftware.com
[email protected]

I’m here are all the methods for encryption

def mysql_encrypt(s, key)
encrypt(s, mysql_key(key))
end

def mysql_decrypt(s, key)
puts s
decrypt(s, mysql_key(key))
end

protected
def aes(m,k,t)
(aes = OpenSSL::Cipher::AES128.new(“ECB”).send(m)).key = k
aes.update(t) << aes.final
end

def encrypt(text, key)
aes(:encrypt, key, text)
end

def decrypt(text, key)
aes(:decrypt, key, text)
end

def mysql_key(key)
key = key.encode(“UTF-8”)
final_key = “\0” * 16
key.length.times do |i|
final_key[i%16] = (final_key[i%16].ord ^ key[i].ord).chr
end
final_key

end

When is use the key set to “test key” and string to be encrypted set to
“Some text for encryption”. The returned encrypted string is:
“\x9E\xB6g\xB7\xF0\xF8\x9F
M\xC1\x82\xA0\xFC\xEF[hY\xEC]=\xE6U\xE8o\xFBN\xCD\x929\x9A\xF4\xB5”

But the encrypted string should be:
“9EB667B7F0F89F204DC182A0FCEF5B6859EC5D3DE655E86FFB4ECD92399AF4B5”
(generated from C# mysql_mimic and Mysql itself)

My encrypted string appears some hexadecimal values interlaced and I’m
not sure why or how “decode” it properly to match the desire encryption
results.

The above results were produced in ruby 1.9.1 (which is what we are
using for this project)

Thanks

Rob B. wrote:

What final_key do you expect? You need to post the full code and
input along with the expected value (or what 1.8.6 gives?) in order to
help.

-Rob

Looks like was I given some skewed information from our C# fella.

“\x9E\xB6g\xB7\xF0\xF8\x9F
M\xC1\x82\xA0\xFC\xEF[hY\xEC]=\xE6U\xE8o\xFBN\xCD\x929\x9A\xF4\xB5”.unpack(“H*”)
= [“9eb667b7f0f89f204dc182a0fcef5b6859ec5d3de655e86ffb4ecd92399af4b5”]

which means everything it is working :slight_smile:

My only issue now is taking
“9eb667b7f0f89f204dc182a0fcef5b6859ec5d3de655e86ffb4ecd92399af4b5” and
“packing” it back to “\x9E\xB6g\xB7\xF0\xF8\x9F
M\xC1\x82\xA0\xFC\xEF[hY\xEC]=\xE6U\xE8o\xFBN\xCD\x929\x9A\xF4\xB5”

Anyone know how to do that?

Thanks,

On Jul 1, 2010, at 5:27 PM, Joshua Mckinney wrote:

“9eb667b7f0f89f204dc182a0fcef5b6859ec5d3de655e86ffb4ecd92399af4b5” and
“packing” it back to “\x9E\xB6g\xB7\xF0\xF8\x9F
M\xC1\x82\xA0\xFC\xEF[hY\xEC]=\xE6U\xE8o\xFBN\xCD\x929\x9A\xF4\xB5”

Anyone know how to do that?

Thanks,

Posted via http://www.ruby-forum.com/.

irb>
["9eb667b7f0f89f204dc182a0fcef5b6859ec5d3de655e86ffb4ecd92399af4b5
"].pack(“H*”)
=> “\x9E\xB6g\xB7\xF0\xF8\x9F M\xC1\x82\xA0\xFC\xEF[hY\xEC]=\xE6U\xE8o
\xFBN\xCD\x929\x9A\xF4\xB5”

Rob B.
http://agileconsultingllc.com
[email protected]

[email protected]

Awesome, could not find .pack in the 1.9.1 documentation. Everything is
now working.

Here is all the code for 1.9.1:

def mysql_encrypt(s, key=@key)
encrypt(s, mysql_key(key))
end

def mysql_decrypt(s, key=@key)
puts s
decrypt(s, mysql_key(key))
end

protected
def aes(m,k,t)
c = OpenSSL::Cipher::Cipher.new(‘aes-128-ecb’).send(m)
c.key = k
c.update(t) + c.final

end

def encrypt(text, key)
aes(:encrypt, key, text).unpack(“H*”)
end

def decrypt(text, key)
aes(:decrypt, key, [text].pack(“H*”))
end

def mysql_key(key)

final_key = "\0" * 16
key.length.times do |i|
  final_key[i%16] = (final_key[i%16].ord ^ key[i].ord).chr
end

final_key
end

Thanks for you help Rob

Rob B. wrote:

["9eb667b7f0f89f204dc182a0fcef5b6859ec5d3de655e86ffb4ecd92399af4b5
"].pack(“H*”)
=> “\x9E\xB6g\xB7\xF0\xF8\x9F M\xC1\x82\xA0\xFC\xEF[hY\xEC]=\xE6U\xE8o
\xFBN\xCD\x929\x9A\xF4\xB5”

Rob B.
http://agileconsultingllc.com
[email protected]
http://gaslightsoftware.com
[email protected]

Joshua Mckinney wrote:

Awesome, could not find .pack in the 1.9.1 documentation

It’s Array#pack, as opposed to String#unpack.

On Jul 1, 2010, at 1:01 PM, Joshua Mckinney wrote:

Because in Ruby 1.8.6, “hello”[0] is 104, but in 1.8.7 and 1.9.x,
irb> x[0] = (x[0].ord ^ 0x20).chr

ruby-1.8.7-p174 > final_key
=> “test key\000\000\000\000\000\000\000\000”


Posted via http://www.ruby-forum.com/.

What final_key do you expect? You need to post the full code and
input along with the expected value (or what 1.8.6 gives?) in order to
help.

-Rob

Ha…well that would make sense given unpack returns an array. Deductive
reasoning was on the back-burner yesterday.

Made a small change so decrypt returns the string only

The following worked in 1.8.7 and 1.9.1

def mysql_encrypt(s, key)
encrypt(s, mysql_key(key))
end

def mysql_decrypt(s, key)
puts s
decrypt(s, mysql_key(key))
end

protected
def aes(m,k,t)
c = OpenSSL::Cipher::Cipher.new(‘aes-128-ecb’).send(m)
c.key = k
c.update(t) + c.final

end

def encrypt(text, key)
aes(:encrypt, key, text).unpack(“H*”)[0]
end

def decrypt(text, key)
aes(:decrypt, key, [text].pack(“H*”))
end

def mysql_key(key)
final_key = “\0” * 16
key.length.times do |i|
final_key[i%16] = (final_key[i%16].ord ^ key[i].ord).chr
end
final_key
end

Thanks,

Josh

Brian C. wrote:

Joshua Mckinney wrote:

Awesome, could not find .pack in the 1.9.1 documentation

It’s Array#pack, as opposed to String#unpack.