It will be a story about open source software, trust and responsibility.
Task and its solution
Once I needed to add symmetric encryption to my Ruby application. The AES algorithm seemed like a good choice and I decided to find an encryption library with support for this algorithm. As I wrote in Ruby, I did the same thing that almost every Ruby programmer would do in my place — go to Google and write the query “ruby gem aes”. Of course, Google offered me a gem in the first line, called (here is a surprise!) - “aes”. It was very easy to use:
require 'aes' message = "Super secret message" key = "password" encrypted = AES.encrypt(message, key)
If you used the wrong password when decrypting, gem threw the error:
')
decrypted = AES.decrypt(encrypted, "Some other password")
Well, great. What could possibly go wrong?
Bug
After connecting the gem, I enclosed its functionality in a new feature and, just in case, wrote a couple of tests for it - to decrypt with the correct password and to decrypt the error with the wrong password. In the second test, I simply replaced the first letter of the password when decrypting. I expected to get a decryption error, which in this case would be a correctly passed test. And ... my test failed! Not only did I not get the decoding error, I even received the correctly decoded data with the wrong password!
encrypted = AES.encrypt("Super secret message", "password") decrypted = AES.decrypt(encrypted, "gassword")
Oh wow! Perhaps I accidentally hit the very rare, one in billions, case when another password came up for me? Something like a hash function collision or something. With the following attempt I changed the two characters already in the password:
encrypted = AES.encrypt("Super secret message", "password") decrypted = AES.decrypt(encrypted, "ggssword")
And again, I received a successfully decrypted message! Well, there was only one thing left. I tried a completely different password:
encrypted = AES.encrypt("Super secret message", "password") decrypted = AES.decrypt(encrypted, "totally wrong password") decrypted
It already looked like a screaming security hole, so I decided to figure out what was going on here.
Debugging
The problem arose due to the following line in the gem code:
@cipher.key = @key.unpack('a2'*32).map{|x| x.hex}.pack('c'*32)
First let's explain what unpack does. In this case, it splits the input string into an array of 32 lines (see the
documentation ):
"password".unpack("a2"*32) => ["pa", "ss", "wo", "rd", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]
Further, for each of the received lines the method #hex is called.
String # hex in Ruby converts strings containing hex numbers to integers (and if the conversion fails, then to the number 0).
'9'.hex
Thus, any string that does not contain a valid hex number will be transformed into an array of 32 zeros.
"pa".hex
That is, we can almost always decrypt any encrypted message with any password. I think the author meant that the input parameter of the encryption function will always be a hex number (and in this case, gem would work reliably). However, the gem interface does not imply any errors when encrypting with a regular string, which leads to a false sense of encryption with its actual absence.
findings
aes - not very common gem. At the time of this writing, he has 45 stars and 13 forks on GitHub. But the problem is that Google gives it as the first result for “aes gem” or “ruby aes gem” requests, and we often believe that top search results lead to high-quality and popular libraries. Often, programmers do not think at all about checking and writing tests for external libraries connected to a project. As you can see from this example, this behavior carries a danger.
Technical details:
- Gem:
github.com/chicks/aes- Version with this error: 0.5.0 / 12c3648