尝试使用 openssl/golang 解密已在 rails 中加密的字符串
Trying to decrypt a string using openssl/golang which has been encrypted in rails
我正在尝试解密已在我的 rails 项目中加密的字符串。这就是我加密数据的方式:
def encrypt_text(text_To_encrypt)
# 0. generate the key using command openssl rand -hex 16 on linux machines
# 1. Read the secret from config
# 2. Read the salt from config
# 3. Encrypt the data
# 4. return the encypted data
# Ref: http://www.monkeyandcrow.com/blog/reading_rails_how_does_message_encryptor_work/
secret = Rails.configuration.miscconfig['encryption_key']
salt = Rails.configuration.miscconfig['encryption_salt']
key = ActiveSupport::KeyGenerator.new(secret).generate_key(salt, 32)
crypt = ActiveSupport::MessageEncryptor.new(key)
encrypted_data = crypt.encrypt_and_sign(text_To_encrypt)
encrypted_data
end
现在的问题是我无法使用 openssl 解密它。它只是显示坏的幻数。一旦我在 open ssl 中这样做,我的计划是在 golang 中解密它。
以下是我尝试使用 openssl 解密它的方法:
openssl enc -d -aes-256-cbc -salt -in encrypted.txt -out decrypted.txt -d -pass pass:<the key given in rails> -a
这只是显示了错误的幻数
尝试解密在不同系统中加密的数据是行不通的,除非您了解并处理两个系统如何进行加密的许多复杂细节。尽管 Rails 和 openssl
命令行工具都在后台使用 OpenSSL 库进行加密操作,但它们都以自己独特的方式使用它,无法直接互操作。
如果仔细观察这两个系统,您会看到例如:
- Rails 消息加密器不仅加密消息而且签名它
- Rails 加密器使用
Marshal
序列化输入数据
openssl enc
工具期望加密数据采用具有 Salted__<salt>
header 的不同文件格式(这就是为什么您会得到 错误的幻数 来自 openssl
) 的消息
- 必须正确配置
openssl
工具以使用与 Rails 加密器和密钥生成器相同的密码,因为 openssl
默认值不同于 Rails 默认值
- 默认密码配置自 Rails 5.2 以来发生了显着变化。
有了这些一般信息,我们就可以看一个实际的例子了。它在 Rails 4.2 中进行了测试,但在 Rails 5.1.
之前应该同样有效
Rails-encrypted 消息剖析
让我从您提供的稍微修改过的代码开始。唯一的变化是将 password
和 salt
预设为静态值并打印大量调试信息:
def encrypt_text(text_to_encrypt)
password = "password" # the password to derive the key
salt = "saltsalt" # salt must be 8 bytes
key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, 32)
puts "salt (hexa) = #{salt.unpack('H*').first}" # print the saltin HEX
puts "key (hexa) = #{key.unpack('H*').first}" # print the generated key in HEX
crypt = ActiveSupport::MessageEncryptor.new(key)
output = crypt.encrypt_and_sign(text_to_encrypt)
puts "output (base64) = #{output}"
output
end
encrypt_text("secret text")
当你运行这个时,你会得到类似下面的输出:
salt (hexa) = 73616c7473616c74
key (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518
output (base64) = SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==--80d091e8799776113b2c0efd1bf75b344bf39994
最后一行(encrypt_and_sign
方法的输出)是由 --
分隔的两部分组合而成(参见 source):
- 加密消息(Base64 编码)和
- 消息签名(Base64 编码)。
签名对于加密并不重要,所以让我们看一下第一部分 - 让我们在 Rails 控制台中对其进行解码:
> Base64.strict_decode64("SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==")
=> "HdSQv1G+57XZUciUlqY7B0yr2MyJt4uaA8+wgGcYWAg=--9+wXA5bLVoGrzmgyh8mf4w=="
您可以看到解码后的消息再次由两个 Base64 编码的部分组成,由 --
分隔(参见 source):
- 加密信息本身
- 加密中使用的初始化向量
Rails 消息加密器默认使用 aes-256-cbc
密码 (请注意,自 Rails 5.2 以来已更改)。此密码需要一个初始化向量,它由 Rails 随机生成,并且必须存在于加密输出中,以便我们可以将其与密钥一起使用来解密消息。
此外,Rails 不会将输入数据加密为简单的纯文本,而是 数据的序列化版本 ,使用 Marshal
默认情况下的序列化程序 (source)。如果我们用 openssl 解密这样的序列化值,我们仍然会得到初始纯文本数据的轻微乱码(序列化)版本。这就是为什么在 Rails 中加密数据时禁用序列化会更合适。这可以通过将参数传递给加密方法来完成:
# crypt = ActiveSupport::MessageEncryptor.new(key)
crypt = ActiveSupport::MessageEncryptor.new(key, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
A re-run 的代码产生的输出比以前的版本稍短,因为加密数据现在还没有序列化:
salt (hexa) = 73616c7473616c74
key (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518
output (base64) = SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=--58bbaf983fd20459062df8b6c59eb470311cbca9
最后,我们必须找到一些关于加密密钥推导过程的信息。 source 告诉我们 KeyGenerator 使用 pbkdf2_hmac_sha1
算法和 2**16 = 65536
次迭代从密码/秘密中导出密钥。
openssl
加密消息剖析
现在,openssl
方需要进行类似的调查,以了解其解密过程的细节。首先,如果您使用 openssl enc
工具加密任何内容,您会发现输出具有 不同的格式 :
Salted__<salt><encrypted_message>
它以 Salted__
魔术字符串 开头,然后是 salt(十六进制形式),最后是通过加密数据。为了能够使用此工具解密任何数据,我们必须将加密数据转换为相同的格式。
openssl
工具默认使用 EVP_BytesToKey
(see source) 派生密钥,但可以配置为使用 pbkdf2_hmac_sha1
算法,使用 -pbkdf2
和 -md sha1
选项。可以使用 -iter
选项设置迭代次数。
如何解密 openssl
中的 Rails-encrypted 消息
所以,我们终于有了足够的信息来实际尝试解密 openssl
中的 Rails-encrypted 消息。
首先我们必须再次解码Rails-encrypted输出的第一部分以获得加密数据和初始化向量:
> Base64.strict_decode64("SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=")
=> "IIHXPcItTsBhtC3/8WrBsQ==--hdkOWVQsb9Z/38m5tSNuWA=="
现在让我们使用 IV(第二部分)并将其转换为六角字符串形式,因为这是 openssl
需要的形式:
> Base64.strict_decode64("hdkOWVQsb9Z/38m5tSNuWA==").unpack("H*").first
=> "85d90e59542c6fd67fdfc9b9b5236e58" # the initialization vector in hex form
现在我们需要获取 Rails-encrypted 数据并将其转换为 openssl
能够识别的格式,即在其前面添加魔术字符串和盐,并再次对其进行 Base64 编码:
> Base64.strict_encode64("Salted__" + "saltsalt" + Base64.strict_decode64("IIHXPcItTsBhtC3/8WrBsQ=="))
=> "U2FsdGVkX19zYWx0c2FsdCCB1z3CLU7AYbQt//FqwbE=" # encrypted data suitable for openssl
最后,我们可以构造ct openssl
命令解密数据:
$ echo "U2FsdGVkX19zYWx0c2FsdCCB1z3CLU7AYbQt//FqwbE=" |
> openssl enc -aes-256-cbc -d -iv 85d90e59542c6fd67fdfc9b9b5236e58 \
> -pass pass:password -pbkdf2 -iter 65536 -md sha1 -a
secret text
瞧,我们成功解密了初始消息!
openssl
参数如下:
-aes-256-cbc
设置与 Rails 相同的密码用于加密
-d
代表解密
-iv
以十六进制字符串形式传递初始化向量
-pass pass:password
将用于导出加密密钥的密码设置为 "password"
-pbkdf2
和 -md sha1
设置与 Rails (pbkdf2_hmac_sha1
) 使用的相同的密钥派生算法
-iter 65536
设置与 Rails 相同的密钥派生迭代次数
-a
允许使用 Base64 编码的加密数据 - 无需处理文件中的原始字节
默认情况下 openssl
从 STDIN 读取,因此我们只需使用 echo 将加密数据(以正确的格式)传递给 openssl
。
调试
如果您在使用 openssl
解密时遇到任何问题,将 -P
参数添加到命令行会很有用,它会输出有关密码/密钥参数的调试信息:
$ echo ... | openssl ... -P
salt=73616C7473616C74
key=196827B250431E911310F5DBC82D395782837B7AE56230DCE24E497CF07B6518
iv =85D90E59542C6FD67FDFC9B9B5236E58
salt
、key
和iv
值必须对应上面打印的encrypt_text
方法中原始代码打印的调试值。如果它们不同,你就知道你做错了什么......
现在,我想您在尝试解密消息时可能会遇到类似的问题,但我认为您现在有一些很好的建议可以开始。
我正在尝试解密已在我的 rails 项目中加密的字符串。这就是我加密数据的方式:
def encrypt_text(text_To_encrypt)
# 0. generate the key using command openssl rand -hex 16 on linux machines
# 1. Read the secret from config
# 2. Read the salt from config
# 3. Encrypt the data
# 4. return the encypted data
# Ref: http://www.monkeyandcrow.com/blog/reading_rails_how_does_message_encryptor_work/
secret = Rails.configuration.miscconfig['encryption_key']
salt = Rails.configuration.miscconfig['encryption_salt']
key = ActiveSupport::KeyGenerator.new(secret).generate_key(salt, 32)
crypt = ActiveSupport::MessageEncryptor.new(key)
encrypted_data = crypt.encrypt_and_sign(text_To_encrypt)
encrypted_data
end
现在的问题是我无法使用 openssl 解密它。它只是显示坏的幻数。一旦我在 open ssl 中这样做,我的计划是在 golang 中解密它。
以下是我尝试使用 openssl 解密它的方法:
openssl enc -d -aes-256-cbc -salt -in encrypted.txt -out decrypted.txt -d -pass pass:<the key given in rails> -a
这只是显示了错误的幻数
尝试解密在不同系统中加密的数据是行不通的,除非您了解并处理两个系统如何进行加密的许多复杂细节。尽管 Rails 和 openssl
命令行工具都在后台使用 OpenSSL 库进行加密操作,但它们都以自己独特的方式使用它,无法直接互操作。
如果仔细观察这两个系统,您会看到例如:
- Rails 消息加密器不仅加密消息而且签名它
- Rails 加密器使用
Marshal
序列化输入数据 openssl enc
工具期望加密数据采用具有Salted__<salt>
header 的不同文件格式(这就是为什么您会得到 错误的幻数 来自openssl
) 的消息
- 必须正确配置
openssl
工具以使用与 Rails 加密器和密钥生成器相同的密码,因为openssl
默认值不同于 Rails 默认值 - 默认密码配置自 Rails 5.2 以来发生了显着变化。
有了这些一般信息,我们就可以看一个实际的例子了。它在 Rails 4.2 中进行了测试,但在 Rails 5.1.
之前应该同样有效Rails-encrypted 消息剖析
让我从您提供的稍微修改过的代码开始。唯一的变化是将 password
和 salt
预设为静态值并打印大量调试信息:
def encrypt_text(text_to_encrypt)
password = "password" # the password to derive the key
salt = "saltsalt" # salt must be 8 bytes
key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, 32)
puts "salt (hexa) = #{salt.unpack('H*').first}" # print the saltin HEX
puts "key (hexa) = #{key.unpack('H*').first}" # print the generated key in HEX
crypt = ActiveSupport::MessageEncryptor.new(key)
output = crypt.encrypt_and_sign(text_to_encrypt)
puts "output (base64) = #{output}"
output
end
encrypt_text("secret text")
当你运行这个时,你会得到类似下面的输出:
salt (hexa) = 73616c7473616c74
key (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518
output (base64) = SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==--80d091e8799776113b2c0efd1bf75b344bf39994
最后一行(encrypt_and_sign
方法的输出)是由 --
分隔的两部分组合而成(参见 source):
- 加密消息(Base64 编码)和
- 消息签名(Base64 编码)。
签名对于加密并不重要,所以让我们看一下第一部分 - 让我们在 Rails 控制台中对其进行解码:
> Base64.strict_decode64("SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==")
=> "HdSQv1G+57XZUciUlqY7B0yr2MyJt4uaA8+wgGcYWAg=--9+wXA5bLVoGrzmgyh8mf4w=="
您可以看到解码后的消息再次由两个 Base64 编码的部分组成,由 --
分隔(参见 source):
- 加密信息本身
- 加密中使用的初始化向量
Rails 消息加密器默认使用 aes-256-cbc
密码 (请注意,自 Rails 5.2 以来已更改)。此密码需要一个初始化向量,它由 Rails 随机生成,并且必须存在于加密输出中,以便我们可以将其与密钥一起使用来解密消息。
此外,Rails 不会将输入数据加密为简单的纯文本,而是 数据的序列化版本 ,使用 Marshal
默认情况下的序列化程序 (source)。如果我们用 openssl 解密这样的序列化值,我们仍然会得到初始纯文本数据的轻微乱码(序列化)版本。这就是为什么在 Rails 中加密数据时禁用序列化会更合适。这可以通过将参数传递给加密方法来完成:
# crypt = ActiveSupport::MessageEncryptor.new(key)
crypt = ActiveSupport::MessageEncryptor.new(key, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
A re-run 的代码产生的输出比以前的版本稍短,因为加密数据现在还没有序列化:
salt (hexa) = 73616c7473616c74
key (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518
output (base64) = SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=--58bbaf983fd20459062df8b6c59eb470311cbca9
最后,我们必须找到一些关于加密密钥推导过程的信息。 source 告诉我们 KeyGenerator 使用 pbkdf2_hmac_sha1
算法和 2**16 = 65536
次迭代从密码/秘密中导出密钥。
openssl
加密消息剖析
现在,openssl
方需要进行类似的调查,以了解其解密过程的细节。首先,如果您使用 openssl enc
工具加密任何内容,您会发现输出具有 不同的格式 :
Salted__<salt><encrypted_message>
它以 Salted__
魔术字符串 开头,然后是 salt(十六进制形式),最后是通过加密数据。为了能够使用此工具解密任何数据,我们必须将加密数据转换为相同的格式。
openssl
工具默认使用 EVP_BytesToKey
(see source) 派生密钥,但可以配置为使用 pbkdf2_hmac_sha1
算法,使用 -pbkdf2
和 -md sha1
选项。可以使用 -iter
选项设置迭代次数。
如何解密 openssl
中的 Rails-encrypted 消息
所以,我们终于有了足够的信息来实际尝试解密 openssl
中的 Rails-encrypted 消息。
首先我们必须再次解码Rails-encrypted输出的第一部分以获得加密数据和初始化向量:
> Base64.strict_decode64("SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=")
=> "IIHXPcItTsBhtC3/8WrBsQ==--hdkOWVQsb9Z/38m5tSNuWA=="
现在让我们使用 IV(第二部分)并将其转换为六角字符串形式,因为这是 openssl
需要的形式:
> Base64.strict_decode64("hdkOWVQsb9Z/38m5tSNuWA==").unpack("H*").first
=> "85d90e59542c6fd67fdfc9b9b5236e58" # the initialization vector in hex form
现在我们需要获取 Rails-encrypted 数据并将其转换为 openssl
能够识别的格式,即在其前面添加魔术字符串和盐,并再次对其进行 Base64 编码:
> Base64.strict_encode64("Salted__" + "saltsalt" + Base64.strict_decode64("IIHXPcItTsBhtC3/8WrBsQ=="))
=> "U2FsdGVkX19zYWx0c2FsdCCB1z3CLU7AYbQt//FqwbE=" # encrypted data suitable for openssl
最后,我们可以构造ct openssl
命令解密数据:
$ echo "U2FsdGVkX19zYWx0c2FsdCCB1z3CLU7AYbQt//FqwbE=" |
> openssl enc -aes-256-cbc -d -iv 85d90e59542c6fd67fdfc9b9b5236e58 \
> -pass pass:password -pbkdf2 -iter 65536 -md sha1 -a
secret text
瞧,我们成功解密了初始消息!
openssl
参数如下:
-aes-256-cbc
设置与 Rails 相同的密码用于加密-d
代表解密-iv
以十六进制字符串形式传递初始化向量-pass pass:password
将用于导出加密密钥的密码设置为 "password"-pbkdf2
和-md sha1
设置与 Rails (pbkdf2_hmac_sha1
) 使用的相同的密钥派生算法
-iter 65536
设置与 Rails 相同的密钥派生迭代次数
-a
允许使用 Base64 编码的加密数据 - 无需处理文件中的原始字节
默认情况下 openssl
从 STDIN 读取,因此我们只需使用 echo 将加密数据(以正确的格式)传递给 openssl
。
调试
如果您在使用 openssl
解密时遇到任何问题,将 -P
参数添加到命令行会很有用,它会输出有关密码/密钥参数的调试信息:
$ echo ... | openssl ... -P
salt=73616C7473616C74
key=196827B250431E911310F5DBC82D395782837B7AE56230DCE24E497CF07B6518
iv =85D90E59542C6FD67FDFC9B9B5236E58
salt
、key
和iv
值必须对应上面打印的encrypt_text
方法中原始代码打印的调试值。如果它们不同,你就知道你做错了什么......
现在,我想您在尝试解密消息时可能会遇到类似的问题,但我认为您现在有一些很好的建议可以开始。