如何用另一种语言解密由 Ruby 的“对称加密”gem 加密的数据?
Posted
技术标签:
【中文标题】如何用另一种语言解密由 Ruby 的“对称加密”gem 加密的数据?【英文标题】:How do I decrypt data encrypted by Ruby's `symmetric-encryption` gem in another language? 【发布时间】:2017-12-05 19:33:16 【问题描述】:我想访问由 Rails 创建的数据库中的数据,以供非 Ruby 代码使用。一些字段使用attr_encrypted
访问器,使用的库是symmetric-encryption
gem。如果我尝试使用例如 NodeJS crypto
库解密数据,我总是会收到“错误的最终块长度”错误。
我怀疑这与字符编码或填充有关,但根据文档我无法弄清楚。
作为一个实验,我尝试在 Ruby 自己的 OpenSSL 库中解密来自 symmetric-encryption
的数据,但我得到了“错误解密”错误或同样的问题:
SymmetricEncryption.cipher = SymmetricEncryption::Cipher.new(
key: "1234567890ABCDEF",
iv: "1234567890ABCDEF",
cipher_name: "aes-128-cbc"
)
ciphertext = SymmetricEncryption.encrypt("Hello world")
c = OpenSSL::Cipher.new("aes-128-cbc")
c.iv = c.key = "1234567890ABCDEF"
c.update(ciphertext) + c.final
这给了我一个“错误的解密”错误。
有趣的是,数据库中的加密数据可以通过symmetric-encryption
gem 解密,但与SymmetricEncryption.encrypt
的输出不同(OpenSSL 也无法成功解密)。
编辑:
psql=# SELECT "encrypted_firstName" FROM people LIMIT 1;
encrypted_firstName
----------------------------------------------------------
QEVuQwBAEAAuR5vRj/iFbaEsXKtpjubrWgyEhK5Pji2EWPDPoT4CyQ==
(1 row)
然后
irb> SymmetricEncryption.decrypt "QEVuQwBAEAAuR5vRj/iFbaEsXKtpjubrWgyEhK5Pji2EWPDPoT4CyQ=="
=> "Lurline"
irb> SymmetricEncryption.encrypt "Lurline"
=> "QEVuQwAAlRBeYptjK0Fg76jFQkjLtA=="
【问题讨论】:
1.将加密的数据和数据库中的十六进制数据添加到问题中。 2. 这可能是填充问题。 3. IV和key是否真的一样,一般是安全错误。 4.c.update(ciphertext) + c.final
:好可爱。
1.会做。 2. 这似乎是合理的。 3. 不,只在开发数据库中。
那么,QEVuQwBAEAAuR5vRj/iFbaEsXKtpjubrWgyEhK5Pji2EWPDPoT4CyQ==
是 Base64 不是十六进制,所以我必须进行转换>? Base64 是计算机的二进制编码,十六进制是开发人员的二进制编码。
好的,知道了。如果我从***.com/questions/18923515/… 中正确理解,则数据库中数据的十六进制为“40456e43004010002e479bd18ff8856da12c5cab698ee6eb5a0c8484ae4f8e2d8458f0cfa13e02c9”。
更新:有些东西不匹配,有些东西匹配。 1. "QEVuQwBAEAAuR5vRj/iFbaEsXKtpjubrWgyEhK5Pji2EWPDPoT4CyQ=="
解码为 40 个字节,但 "Lurline"
是 7 个字符,填充 16 个字节。即使允许 UTF-16 也是 14 字节,仍然填充到 16 字节。 2. ASCII/UTF-8 中的 AES 加密“Lurline”(7 个字符)将被填充到 16 个字节,加密数据将是 16 个字节,matt 发现有 6 个字节的前导标头,"QEVuQwAAlRBeYptjK0Fg76jFQkjLtA=="
解码为 22字节所以这行得通。
【参考方案1】:
查看source for the symmetric-encryption gem,默认情况下adds a header 输出和base64 encodes it,尽管这两个都是可配置的。
要直接使用 Ruby 的 OpenSSL 解密,您需要对其进行解码并去掉此标头 which is 6 bytes long in this simple case:
ciphertext = Base64.decode64(ciphertext)
ciphertext = ciphertext[6..-1]
c = OpenSSL::Cipher.new("aes-128-cbc")
c.decrypt
c.iv = "1234567890ABCDEF"
c.key = "1234567890ABCDEF"
result = c.update(ciphertext) + c.final
当然,您可能需要根据您在对称加密中使用的设置来更改此设置,例如标头长度可能会有所不同。为了从数据库中解密结果,您需要解析标头。看看source。
【讨论】:
是的,就是这样,请参阅 Cryptomathic AES CALCULATOR。但是该死的,应该没必要看源代码就知道它在做什么!【参考方案2】:基于@Shepmaster 在我的other question 中完成的Rust 实现(以及symmetric-encryption
gem 的源代码),我在TypeScript 中有一个工作版本。 @matt 与他的回答很接近,但标头实际上可以包含额外的字节,其中包含有关加密数据的元数据。请注意,这不处理 (1) 压缩的加密数据,或 (2) 从标头本身设置加密算法;这两种情况都与我的用例无关。
import createDecipher, createDecipheriv, Decipher from "crypto";
// We use two types of encoding with SymmetricEncryption: Base64 and UTF-8. We
// define them in an `enum` for type safety.
const enum Encoding
Base64 = "base64",
Utf8 = "utf8",
// Symmetric encryption's header contains the following data:
interface IHeader
version: number, // The version of the encryption algo
isCompressed: boolean, // Whether the data is compressed (TODO: Implement)
hasIv: boolean, // Whether the header itself has the IV
hasKey: boolean, // Whether the header itself has the Key
hasCipherName: boolean, // Whether the header contains the cipher name
hasAuthTag: boolean, // Whether the header has an authorization tag
offset: number, // How many bytes into the encoded ciphertext the actual encrypted data starts
iv?: Buffer, // The IV, present only if `hasIv` is true
key?: Buffer, // The key, present only if `hasKey` is true
// The cipher name, present only if `hasCipherName` is true. Currently ignored.
cipherName?: string,
authTag?: string, // The authorization tag, present only if // `hasAuthTag` is true
// Byte 6 of the header contain bit flags
interface IFlags
isCompressed: boolean,
hasIv: boolean,
hasKey: boolean,
hasCipherName: boolean,
hasAuthTag: boolean
// The 7th byte until the end of the header have the actual values. If all
// of the flags are false, the header ends at the 6th byte.
interface IValues
iv?: Buffer,
key?: Buffer,
cipherName?: string,
authTag?: string,
size: number,
/**
* Represent the encoded ciphertext, complete with the SymmetricEncryption header.
*/
class Ciphertext
// Bit flags corresponding to the data encoded in byte 6 of the
// header.
readonly FLAG_COMPRESSED = 0b1000_0000;
readonly FLAG_IV = 0b0100_0000;
readonly FLAG_KEY = 0b0010_0000;
readonly FLAG_CIPHER_NAME = 0b0001_0000;
readonly FLAG_AUTH_TAG = 0b0000_1000;
// The literal data encoded in bytes 1 - 4 of the header
readonly MAGIC_HEADER = "@EnC";
// If any of the values represented by the bit flags is present, the first 2
// bytes of the data tells us how long the actual value is. In other words,
// the first 2 bytes aren't the value itself, but rather give the info about
// the length of the rest of the value.
readonly LENGTH_INFO_SIZE = 2;
public header: IHeader | null;
public data: Buffer;
private cipherBuffer: Buffer;
constructor(private input: string)
this.cipherBuffer = new Buffer(input, Encoding.Base64);
this.header = this.getHeader();
const offset = this.header ? this.header.offset : 0; // If no header, then no offset
this.data = this.cipherBuffer.slice(offset);
/**
* Extract the header from the data
*/
private getHeader(): IHeader | null
let offset = 0;
// Bytes 1 - 4 are the literal `@EnC`. If that's absent, there's no
// SymmetricEncryption header.
if (this.cipherBuffer.toString(Encoding.Utf8, offset, offset += 4) != this.MAGIC_HEADER)
return null;
// Byte 5 is the version
const version = this.cipherBuffer.readInt8(offset++); // Post increment
// Byte 6 is the flags
const rawFlags = this.cipherBuffer.readInt8(offset++);
const flags = this.readFlags(rawFlags);
// Bytes 7 - end are the values.
const values = this.getValues(offset, flags);
offset += values.size;
return Object.assign( version, offset , flags, values);
/**
* Get the values for `iv`, `key`, `cipherName`, and `authTag`, if any are
* set, based on the bitflags. Return that data, plus how many bytes in the
* header those values represent.
*
* @param offset - What byte we're on when we get to the values. Should be 7
* @param flags - The flags we've extracted, showing us which values to expect
*/
private getValues(offset: number, flags: IFlags): IValues
let iv: Buffer | undefined = undefined;
let key: Buffer | undefined = undefined;
let cipherName: string | undefined = undefined;
let authTag: string | undefined = undefined;
let size = 0; // If all of the bit flags are false, there is no additional data.
// For each value, see if the flag is set to true. If it is, we need to
// read the value. Keys and IVs need to be `Buffer` types; other values
// should be strings.
[iv, size] = flags.hasIv ? this.readBuffer(offset) : [undefined, size];
[key, size] = flags.hasKey ? this.readBuffer(offset + size) : [undefined, size];
[cipherName, size] = flags.hasCipherName ? this.readString(offset + size) : [undefined, size];
[authTag, size] = flags.hasAuthTag ? this.readString(offset + size) : [undefined, size];
return iv, key, cipherName, authTag, size ;
/**
* Parse the 16-bit integer representing the bit flags into an object for
* easier handling
*
* @param flags - The 16-bit integer that contains the bit flags
*/
private readFlags(flags: number): IFlags
return
isCompressed: (flags & this.FLAG_COMPRESSED) != 0,
hasIv: (flags & this.FLAG_IV) != 0,
hasKey: (flags & this.FLAG_KEY) != 0,
hasCipherName: (flags & this.FLAG_CIPHER_NAME) != 0,
hasAuthTag: (flags & this.FLAG_AUTH_TAG) != 0
/**
* Read a string out of the value at the specified offset. Return the value
* itself, plus the number of bytes consumed by the value (including the
* 2-byte encoding of the length of the actual value).
*
* @param offset - The offset (bytes from the beginning of the encoded,
* encrypted Buffer) at which the value in question begins
*/
private readString(offset: number): [string, number]
// The length is the first 2 bytes, encoded as a little-endian 16-bit integer
const length = this.cipherBuffer.readInt16LE(offset);
// The total size occupied in the header is the 2 bytes encoding length plus the length itself
const size = this.LENGTH_INFO_SIZE + length;
const value = this.cipherBuffer.toString(Encoding.Base64, offset + this.LENGTH_INFO_SIZE, offset + size);
return [value, size];
/**
* Read a Buffer out of the value at the specified offset. Return the value
* itself, plus the number of bytes consumed by the value (including the
* 2-byte encoding of the length of the actual value).
*
* @param offset - The offset (bytes from the beginning of the encoded,
* encrypted Buffer) at which the value in question begins
*/
private readBuffer(offset: number): [Buffer, number]
// The length is the first 2 bytes, encoded as a little-endian 16-bit integer
const length = this.cipherBuffer.readInt16LE(offset);
// The total size occupied in the header is the 2 bytes encoding length plus the length itself
const size = this.LENGTH_INFO_SIZE + length;
const value = this.cipherBuffer.slice(offset + this.LENGTH_INFO_SIZE, offset + size);
return [value, size];
/**
* Allow decryption of data encrypted by Ruby's `symmetric-encryption` gem
*/
class SymmetricEncryption
private key: Buffer;
private iv?: Buffer;
constructor(key: string, private algo: string, iv?: string)
this.key = new Buffer(key);
this.iv = iv ? new Buffer(iv) : undefined;
public decrypt(input: string): string
const ciphertext = new Ciphertext(input);
// IV can be specified by the user. But if it's encoded in the header
// itself, go with that instead.
const iv = (ciphertext.header && ciphertext.header.iv) ? ciphertext.header.iv : this.iv;
// Key can be specified by the user. but if it's encoded in the header,
// go with that instead.
const key = (ciphertext.header && ciphertext.header.key) ? ciphertext.header.key : this.key;
const decipher: Decipher = iv ?
createDecipheriv(this.algo, key, iv) :
createDecipher(this.algo, key);
// Terse version of `update()` + `final()` that passes type checking
return Buffer.concat([decipher.update(ciphertext.data), decipher.final()]).toString();
const s = new SymmetricEncryption("1234567890ABCDEF", "aes-128-cbc", "1234567890ABCDEF");
console.log(s.decrypt("QEVuQwAADWK0cKzgFIovdIThq9Scrg==")); // => "Hello world"
【讨论】:
以上是关于如何用另一种语言解密由 Ruby 的“对称加密”gem 加密的数据?的主要内容,如果未能解决你的问题,请参考以下文章