I'm decrypting legacy data created by a Ruby on Rails application using the symmetric-encryption gem in Rust. See my earlier question How do I decrypt data encrypted by Ruby's `symmetric-encryption` gem in another language?.
I've already implemented this in Node where the crypto library seems to know how to strip out gibberish in an encrypted string that Rust's openssl
crate isn't stripping out (at least, the way I'm using it). I have already stripped out the PKCS7 padding and a header, yet it's still there. What is that gobbledygook, and how do I get Rust to remove it?
The encrypted data began as plaintext with a fixed-size header, was encrypted using AES-128-CBC with PKCS7 padding, and was then Base64 encoded. Using Node, I can decrypt it correctly using the following code:
const crypto = require("crypto");
const KEY = Buffer.from("1234567890ABCDEF");
const IV = Buffer.from("1234567890ABCDEF");
const CIPHERTEXT = Buffer.from("QEVuQwBAEACuPUPByDkk5jyNzQ3Wd3xTy2Isihz62XTLe1M5qKQrvw==", "base64");
const HEADER_SIZE = 8;
const ALGO = "aes-128-cbc";
const decipher = crypto.createDecipheriv(ALGO, KEY, IV);
decipher.update(CIPHERTEXT.slice(HEADER_SIZE));
const result = decipher.final();
console.log([...result]);
console.log(result.toString());
The result is
[ 72, 97, 108, 102 ]
Half
I'd prefer to use Rust for the application I'm writing. Using the openssl
crate, I can decode the encrypted data, but there's a bunch of junk that Node's library knows how to strip but Rust isn't stripping automatically the way I'm using it:
extern crate base64;
extern crate openssl;
use openssl::symm::*;
const KEY: &'static [u8] = b"1234567890ABCDEF";
const IV: &'static [u8] = b"1234567890ABCDEF";
const CIPHERTEXT: &'static str = "QEVuQwBAEACuPUPByDkk5jyNzQ3Wd3xTy2Isihz62XTLe1M5qKQrvw==";
const HEADER_SIZE: usize = 8;
fn main() {
let decoded = base64::decode(&CIPHERTEXT).unwrap();
let ciphertext = &decoded[HEADER_SIZE..];
let result = decrypt(Cipher::aes_128_cbc(), KEY, Some(IV), ciphertext).unwrap();
println!("{:?}", result);
println!("{:?}", String::from_utf8_lossy(&result));
}
Here the result is
[221, 75, 14, 215, 54, 120, 246, 222, 194, 208, 53, 68, 127, 190, 124, 8, 72, 97, 108, 102]
"�K\u{e}�6x����5D\u{7f}�|\u{8}Half"
You can see that the last four bytes are correct, but that Node stripped out the preceding 16 bytes, as it appears it should have. I don't know what those bytes are.
At first I thought the 16 bytes of gibberish were from attempting to decrypt the PKCS7 padding. But I can verify that the padding was already stripped out: if I create a Crypter
following the example in the openssl
crate's documentation, but don't include the truncate()
step, the resulting Vec
has 12 consecutive 12
s—the PKCS7 padding—that were automatically removed.
So the gibberish is not PKCS7 padding, but I don't know what it is or how to get rid of it using Rust.
And to anticipate the Miranda warning about not futzing with crypto if I'm not an expert: this is not going to be used in production or to create data to put into production.
Your Node decryption is incorrect and it only works because you are ignoring exactly the correct number of bytes before the data begins, as sfackler explains.
symmetric-encryption pads the data with a header. The header has flexible length based on the options you are using. This code properly parses the entire header and then uses the IV embedded in the header:
decipher.update
returns aBuffer
which is ignored in the Node implementation. If you change the Node implementation to print the buffer returned from the update call, you can see that it's the same garbage data:Outputs:
If you change the Rust version to ignore that portion of the output as well, you see the expected output:
Outputs: