3

我需要模仿 MySQL 在使用内置函数 AES_ENCRYPT() 和 AES_DECRYPT() 加密和解密字符串时所做的事情。

我已经阅读了几篇博客文章,显然 MySQL 对这些功能使用了 AES 128 位加密。最重要的是,由于这种加密需要 16 位密钥,MySQL 用 x0 字符 (\0s) 填充字符串,直到它的大小为 16 位。

来自 MySQL 源代码的 C 语言算法在此处发现。

现在我需要复制 MySQL 在 Rails 应用程序中所做的事情,但是我尝试的每一件事都不起作用。

这是一种复制我得到的行为的方法:

1) 创建一个新的 Rails 应用程序

rails encryption-test
cd encryption-test

2) 创建一个新的脚手架

script/generate scaffold user name:string password:binary

3) 编辑你的 config/database.yml 并添加一个测试 MySQL 数据库

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

4) 运行迁移

rake db:migrate

5) 进入控制台,创建用户并从 MySQL 查询更新其密码

script/console
Loading development environment (Rails 2.2.2)
>> User.create(:name => "John Doe")
>> key = "82pjd12398JKBSDIGUSisahdoahOUASDHsdapdjqwjeASIduAsdh078asdASD087asdADSsdjhA7809asdajhADSs"
>> ActiveRecord::Base.connection.execute("UPDATE users SET password = AES_ENCRYPT('password', '#{key}') WHERE name='John Doe'")

这就是我卡住的地方。如果我尝试解密它,使用 MySQL 它可以工作:

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

但是,如果我尝试使用 OpenSSL 库,我将无法使其工作:

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"

我试过填充键:

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##"

但这真的行不通。

有没有人知道如何在 Ruby 中模仿 MySQL AES_ENCRYPT() 和 AES_DECRYPT() 函数的行为?

谢谢!

4

3 回答 3

6

备查:

根据我之前发送的博客文章,以下是 MySQL 如何使用您提供的密钥 AES_ENCRYPT / DECRYPT:

“该算法只是创建一个 16 字节缓冲区设置为全零,然后遍历您提供的字符串的所有字符,并在两个值之间进行按位或赋值。如果我们迭代直到我们到达 16 字节缓冲区的末尾,我们只是从头开始做 ^=。对于短于 16 个字符的字符串,我们在字符串的末尾停止。

我不知道您是否可以阅读 C,但这里是提到的片段:

http://pastie.org/425161

特别是这部分:

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;
}

所以我想出了这个方法(在 Rob Biedenharn 的帮助下,来自 ruby​​ 论坛):

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

也就是说,给定一个字符串,返回 MySQL 在加密和解密时使用的密钥。所以你现在需要的是:

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

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

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

要使用 ruby​​ 内置的 openssl lib,然后您可以制作两个“最终”方法:

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

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

你准备好了!此外,完整的代码可以在这个 Gist 中找到:

http://gist.github.com/84093

:-)

于 2009-03-24T13:41:47.487 回答
1

通常,您不想填充密钥,而是填充/取消填充要加密/解密的数据。这可能是另一个问题的根源。我建议使用完整数量的块的测试数据来消除这种可能性。

另外,我怀疑 OpenSSL API 的密钥需要一个“文字”密钥,而不是代码中的密钥的 ASCII 表示。

鉴于 OpenSSL ruby​​ 文档的缺乏,并且如果您会一点 Java,您可能希望使用 BouncyCastle 提供程序在 JRuby 中进行原型设计——这是我在使用 TwoFish 时所做的事情(OpenSSL API 中不存在) .

编辑:我重新阅读了您关于填充密钥的评论。您的问题有一些位/字节混淆,我不确定这在任何情况下如何应用,因为您发布的密钥长度为 89 个字符(712 位)。也许您应该尝试使用 128 位密钥/密码来消除这种填充现象?

顺便说一句,MySQL 开发人员应该为弱加密而打屁股,有更好的方法来拉伸密码,而不是简单地用零字节填充:(

于 2009-03-23T22:07:15.487 回答
0

如果您不介意使用 openssl 实现attr_encrypted是一个 gem,它将允许对大多数类进行直接加密,无论是否为 ActiveRecord。不幸的是,它与 MySQL 的 AES_EN/DECRYPT 函数不兼容。

于 2010-10-27T21:59:58.703 回答