Home > Mobile >  Reading Ruby Active Record Encryption from a Node Script
Reading Ruby Active Record Encryption from a Node Script

Time:06-15

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
  • Related