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