Implementation of AES-GCM-256 with Rust

I need to make a client-server connection from node.js/javascript server to a client written in Rust. The message had to be encrypted with AES-GCM-256. While in Rust (version 1.67.0) I use aes-gcm crate.

The Rust code below throws an error: aead::Error. What's wrong with the cipher.decrypt() here ?

I'm sure that the node.js implementation is correct. I think that the key variable in Rust code should be the same key from the node.js (12341234123412341234123412341234)

Implementation in Node.js / sender side

const crypto = require('crypto')
const aes256gcm = (key) => {

    const encrypt = (str) => {
        const iv = new crypto.randomBytes(12);
        const ivString = iv.toString("base64")
        const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
        let enc1 = cipher.update(str, 'utf8');
        let enc2 = cipher.final();
        let result = Buffer.concat([enc1, enc2, iv, cipher.getAuthTag()]).toString("base64");
        return { result, iv: ivString }
    };

    const decrypt = (enc) => {
        enc = Buffer.from(enc, "base64");
        const iv = enc.slice(enc.length - 28, enc.length - 16);
        const tag = enc.slice(enc.length - 16);
        enc = enc.slice(0, enc.length - 28);
        const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
        decipher.setAuthTag(tag);
        let str = decipher.update(enc, null, 'utf8');
        str += decipher.final('utf8');
        return str;
    };

    return {
        encrypt,
        decrypt,
    };
};

const cipher = aes256gcm("12341234123412341234123412341234"); // just a test key must be 32
const ct = cipher.encrypt('Hello world!!!');
console.log("encrypted message: ", ct.result)
console.log("iv / nonce       : ", ct.iv)

const pt = cipher.decrypt(ct.result);
console.log("decrypted message: ", pt); // this works flawlessly!

Encryption Result by Node.js

encrypted message:  Zf5aB0bbVGGX3k9Yt6x+9daxCGZO0MmwYW8VUsOY4j3gNYXP47hvfGgd
iv / nonce       :  fvXWsQhmTtDJsGFv
decrypted message:  Hello world!!!

The decryption part (Rust) / Receiver Side


use aes_gcm::{
    aead::{Aead, KeyInit, OsRng},
    Aes256Gcm, Nonce,
};
use base64::{engine::general_purpose, Engine as _};

fn main() {

    // should this be random or should I also get the same key from the sender ? 
    let master = "12341234123412341234123412341234".as_bytes();
    let cipher = Aes256Gcm::new_from_slice(master).unwrap();

    // nonce / iv from sender
    let nonce_str_base64 = "fvXWsQhmTtDJsGFv";
    let nonce_str: Vec<u8> = general_purpose::STANDARD.decode(nonce_str_base64).unwrap();
    let nonce = Nonce::from_slice(&nonce_str); // 96-bits; unique per message

    // encrypted text from sender
    let ciphertext_base64 = "Zf5aB0bbVGGX3k9Yt6x+9daxCGZO0MmwYW8VUsOY4j3gNYXP47hvfGgd";
    let ciphertext = general_purpose::STANDARD.decode(ciphertext_base64).unwrap();

    // gets aead::Error here
    match cipher.decrypt(nonce, ciphertext.as_slice()) {
        Ok(decrypted) => {
            let result = String::from_utf8(decrypted).unwrap();
            println!("result: {}", result);
        }
        Err(err) => print!("{}", err), <--- prints error: aead::Error
    };
}

You're not base64 decoding the ciphertext in the rust code

1 Like

You're right! but still... the same aead::Error with no further explanation.

AES-256-GCM consists of three parts:

  1. payload or ciphered text,
  2. iv or nonce, a unique random number that generated once
  3. and the tag that is part of the authentication that ensures the encrypted message has not been altered

It turns out that the aes_gcm crate uses payload + tag for decryption. So the solution is to remove the iv part from the result message. I just change the sender side (Node.js) from this:

let result = Buffer.concat([enc1, enc2, iv, cipher.getAuthTag()]).toString("base64");

to this:

let result = Buffer.concat([enc1, enc2, cipher.getAuthTag()]).toString("base64");

the nonce/iv part is supplied to cipher.decrypt through function parameter

It's not documented in the aes_gcm site. I got suspicious since the result already contains iv/nonce but the cipher.decrypt also require iv as function parameter

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.