如何用另一种语言解密由 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 加密的数据?的主要内容,如果未能解决你的问题,请参考以下文章

常用加密解密——非对称加密

10.Java 加解密技术系列之 DH

JAVA和.NET使用DES对称加密的区别

每周精选能不能给我解释一下非对称加密?

目前常用的加密方式主要有哪两种

对称加密和非对称加密