如何解密Ruby的`symmetric-encryption` gem加密的数据?

时间:2017-12-05 19:33:16

标签: ruby cryptography

我想访问由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=="

2 个答案:

答案 0 :(得分:5)

查看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

答案 1 :(得分: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"