I'm porting a ruby on rails application to node.js, and need to be able to read the data encrypted via active record encryption.
It seems that the algorithm used is aes-256-gcm, and I've got a node.js script here to encrypt and decrypt data:
import crypto from 'crypto';
import config from './config';
const algorithm = 'aes-256-gcm';
const encrypt = (secret: string, originalData: string) => {
console.log('encrypting with ', {
secret,
originalData
});
const iv = crypto.randomBytes(16);
var crypt = crypto.createCipheriv(algorithm, Buffer.from(secret, 'base64'), iv);
var encoded = crypt.update(originalData, 'utf8', 'hex');
encoded = crypt.final('hex');
const at = crypt.getAuthTag();
return { encrypted: encoded, iv, at };
};
const decrypt = (secret: string, encryptedData: string, iv: string, at: string) => {
console.log('decrypting with ', {
secret,
encryptedData,
iv,
at
});
var crypt = crypto.createDecipheriv(algorithm, Buffer.from(secret, 'base64'), Buffer.from(iv, 'base64'));
crypt.setAuthTag(Buffer.from(at, 'base64'));
var decoded = crypt.update(encryptedData, 'hex', 'utf8');
decoded = crypt.final('utf8');
return decoded;
};
const secret = crypto.createHash('sha256').update('ThisIsASecretKey', 'ascii').digest();
const originalData = 'This is just a test to see what would happen';
const encryptionResult = encrypt(secret.toString('base64'), originalData);
const decryptionResult = decrypt(
secret.toString('base64'),
encryptionResult.encrypted,
encryptionResult.iv.toString('base64'),
encryptionResult.at.toString('base64')
);
This works fine and I get the result:
encrypting with {
secret: 'eKAyTQoTH0apq8 fsQYHSYelUy3mL34gl2M rnvpXhs=',
originalData: 'This is just a test to see what would happen'
}
decrypting with {
secret: 'eKAyTQoTH0apq8 fsQYHSYelUy3mL34gl2M rnvpXhs=',
encryptedData: '63eaf4dde9b1cc0e76d9d95725c85234ef2a742dd1c76a9579a8f03d20494cd85b658dcc3518b1ee9a5301b0',
iv: 'eloq0RQOnaFejiJzkQ7ybw==',
at: 'ylfqtCSRdMZLgnGC0wf3EA=='
}
Encryption data matches {
originalData: 'This is just a test to see what would happen',
encryptedData: '63eaf4dde9b1cc0e76d9d95725c85234ef2a742dd1c76a9579a8f03d20494cd85b658dcc3518b1ee9a5301b0',
decryptionResult: 'This is just a test to see what would happen'
}
However when I try and use the values from ruby it throws the message "Unsupported state or unable to authenticate data"
The data from rails I'm using is exported from the database, with the key exported via running the following in ruby:
key_provider = ActiveRecord::Encryption.key_provider
key = key_provider.encryption_key
puts Base64.encode64(key.secret)
And from looking through the ruby source code this appears to be how it encrypts/decrypts the data:
require "openssl"
secret = Base64.strict_decode64("eKAyTQoTH0apq8 fsQYHSYelUy3mL34gl2M rnvpXhs=")
clear_text = "This is some debug text"
puts "--------------- encrypt -------------"
CIPHER_TYPE = "aes-256-gcm"
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
cipher.encrypt
cipher.key = secret
iv = cipher.random_iv
cipher.iv = iv
encrypted_data = clear_text.empty? ? clear_text.dup : cipher.update(clear_text)
encrypted_data << cipher.final
auth_tag = cipher.auth_tag
puts "clear_text=" clear_text
puts "secret=" Base64.strict_encode64(secret)
puts "iv=" Base64.strict_encode64(iv)
puts "auth_tag=" Base64.strict_encode64(auth_tag)
puts "encrypted_data=" Base64.strict_encode64(encrypted_data)
puts "--------------- /encrypt -------------"
puts "--------------- decrypt -------------"
begin
# secret = key.secret
# cipher = ActiveRecord::Encryption::Aes256Gcm.new(secret, deterministic: false)
# decrypted_data = cipher.decrypt(encrypted_message)
# puts decrypted_data
CIPHER_TYPE = "aes-256-gcm"
# encrypted_data = encrypted_message.payload
puts "encrypted_data=" Base64.strict_encode64(encrypted_data)
# iv = encrypted_message.headers.iv
puts "iv=" Base64.strict_encode64(iv)
# auth_tag = encrypted_message.headers.auth_tag
puts "auth_tag=" Base64.strict_encode64(auth_tag)
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
puts cipher
cipher.decrypt
cipher.key = secret
puts "secret=" Base64.strict_encode64(secret)
cipher.iv = iv
cipher.auth_tag = auth_tag
cipher.auth_data = ""
decrypted_data = encrypted_data.empty? ? encrypted_data : cipher.update(encrypted_data)
decrypted_data << cipher.final
puts "decrypted_data=" decrypted_data
end
puts "--------------- /decrypt -------------"
Which gives the values:
app_1 | --------------- encrypt -------------
app_1 | clear_text=This is some debug text
app_1 | secret=eKAyTQoTH0apq8 fsQYHSYelUy3mL34gl2M rnvpXhs=
app_1 | iv=377YrtFzBASSjvol
app_1 | auth_tag=QqQcyOhGKPP0sPFOANToFw==
app_1 | encrypted_data=LtbIG7tnBVtbMUlnvG qtmoK2NgL 18=
app_1 | --------------- /encrypt -------------
app_1 | --------------- decrypt -------------
app_1 | encrypted_data=LtbIG7tnBVtbMUlnvG qtmoK2NgL 18=
app_1 | iv=377YrtFzBASSjvol
app_1 | auth_tag=QqQcyOhGKPP0sPFOANToFw==
app_1 | #<OpenSSL::Cipher:0x0000558adf4ae830>
app_1 | secret=eKAyTQoTH0apq8 fsQYHSYelUy3mL34gl2M rnvpXhs=
app_1 | decrypted_data=This is some debug text
app_1 | --------------- /decrypt -------------
However running these values on node gives me the same error:
const debug = decrypt(
'eKAyTQoTH0apq8 fsQYHSYelUy3mL34gl2M rnvpXhs=',
'LtbIG7tnBVtbMUlnvG qtmoK2NgL 18=',
'377YrtFzBASSjvol',
'qQcyOhGKPP0sPFOANToFw=='
);
Error: Unsupported state or unable to authenticate data
at Decipheriv.final (node:internal/crypto/cipher:193:29)
CodePudding user response:
The NodeJS code returns the same results as the Ruby code if
- no key derivation (SHA256) is used
- for the ciphertext a Base64 encoding is applied instead of a hex encoding
Full NodeJS code:
var crypto = require('crypto');
const algorithm = 'aes-256-gcm';
function encrypt(secret, originalData) {
console.log('encrypting with ', {
secret,
originalData
});
const iv = Buffer.from('377YrtFzBASSjvol', 'base64'); // crypto.randomBytes(16); // disabled for testing (in the final solution a random nonce MUST be applied for security reasons)
var crypt = crypto.createCipheriv(algorithm, Buffer.from(secret, 'base64'), iv);
var encoded = crypt.update(originalData, 'utf8', 'base64'); // Fix: Base64 encoding
encoded = crypt.final('base64'); // Fix: Base64 encoding
const at = crypt.getAuthTag();
return { encrypted: encoded, iv: iv.toString('base64'), at: at.toString('base64') };
};
function decrypt(secret, encryptedData, iv, at) {
console.log('decrypting with ', {
secret,
encryptedData,
iv,
at
});
var crypt = crypto.createDecipheriv(algorithm, Buffer.from(secret, 'base64'), Buffer.from(iv, 'base64'));
crypt.setAuthTag(Buffer.from(at, 'base64'));
var decoded = crypt.update(encryptedData, 'base64', 'utf8'); // Fix: Base64 encoding
decoded = crypt.final('utf8');
return decoded;
};
var originalData = 'This is some debug text';
var secret = 'eKAyTQoTH0apq8 fsQYHSYelUy3mL34gl2M rnvpXhs='; // Fix: No key derivation
const encryptionResult = encrypt(
secret,
originalData,
);
console.log(encryptionResult);
var secret = 'eKAyTQoTH0apq8 fsQYHSYelUy3mL34gl2M rnvpXhs='; // Fix: No key derivation
var encryptedData = 'LtbIG7tnBVtbMUlnvG qtmoK2NgL 18=';
var iv = '377YrtFzBASSjvol';
var at = 'QqQcyOhGKPP0sPFOANToFw==';
const decryptionResult = decrypt(
secret,
encryptedData,
iv,
at
);
console.log(decryptionResult);
Output:
encrypting with {
secret: 'eKAyTQoTH0apq8 fsQYHSYelUy3mL34gl2M rnvpXhs=',
originalData: 'This is some debug text'
}
{
encrypted: 'LtbIG7tnBVtbMUlnvG qtmoK2NgL 18=',
iv: '377YrtFzBASSjvol',
at: 'QqQcyOhGKPP0sPFOANToFw=='
}
decrypting with {
secret: 'eKAyTQoTH0apq8 fsQYHSYelUy3mL34gl2M rnvpXhs=',
encryptedData: 'LtbIG7tnBVtbMUlnvG qtmoK2NgL 18=',
iv: '377YrtFzBASSjvol',
at: 'QqQcyOhGKPP0sPFOANToFw=='
}
This is some debug text