I encrypt a file in nodejs (v20.10.0) like below,
const Crypto = require("node:crypto");
function encodeText(text) {
return new TextEncoder().encode(text);
}
function encrypt(buffer, passphrase) {
const key = encodeText(passphrase);
const iv = Crypto.randomBytes(16);
const cipher = Crypto.createCipheriv("aes-128-gcm", key, iv);
const result = Buffer.concat([iv, cipher.update(buffer), cipher.final()]);
console.log("Encrypted File Length: ", result.length);
return result;
}
function decrypt(buffer, passphrase) {
const key = encodeText(passphrase);
const iv = buffer.slice(0, 16);
const decipher = Crypto.createDecipheriv("aes-128-gcm", key, iv);
return Buffer.concat([decipher.update(buffer.slice(16))]);
}
module.exports = {
encrypt,
decrypt,
};
const { encrypt, decrypt } = require("./crypto");
const fs = require("node:fs");
const PASSWORD = "some-password".padEnd(16, "0");
const ORIGINAL_FILE_PATH = "/some/path/to/test.png";
const ENCRYPTED_FILE_PATH = "/some/path/to/test.node.enc.png";
const DECRYPTED_FILE_PATH = "/some/path/to/test.node.dec.png";
test("Encrypt and decrypt file", function() {
const original = fs.readFileSync(ORIGINAL_FILE_PATH);
const encrypted = encrypt(original, PASSWORD);
fs.writeFileSync(ENCRYPTED_FILE_PATH, encrypted);
const decrypted = decrypt(encrypted, PASSWORD);
fs.writeFileSync(DECRYPTED_FILE_PATH, decrypted);
expect(decrypted).toEqual(original); // PASS
});
And, I'll decrypt encrypted file in Deno(1.39.0).
import { crypto } from "$std/crypto/mod.ts"; // https://deno.land/std@0.209.0/crypto/mod.ts
function encodeText(text: string): Uint8Array {
return new TextEncoder().encode(text);
}
export async function encrypt(buffer: Uint8Array, passphrase: string): Promise<Uint8Array> {
const key = encodeText(passphrase);
const iv = crypto.getRandomValues(new Uint8Array(16));
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM", length: 128 }, true, ["encrypt"]);
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", length: 128, iv }, cryptoKey, buffer);
return new Uint8Array([...iv, ...new Uint8Array(encrypted)]);
}
export async function decrypt(buffer: Uint8Array, passphrase: string): Promise<Uint8Array> {
const key = encodeText(passphrase);
const iv = buffer.slice(0, 16);
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM", length: 128 }, true, ["decrypt"]);
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", length: 128, iv }, cryptoKey, buffer.slice(16));
return new Uint8Array(decrypted);
}
import * as Assertions from "$std/assert/mod.ts"; // https://deno.land/std@0.209.0/assert/mod.ts
import { encrypt, decrypt } from "./crypto.ts";
const PASSWORD = "some-password".padEnd(16, "0");
const ORIGINAL_FILE_PATH = "/some/path/to/test.png";
const ENCRYPTED_FILE_PATH = "/some/path/to/test.deno.enc.png";
const DECRYPTED_FILE_PATH = "/some/path/to/test.deno.dec.png";
const NODE_ENCRYPTED_FILE_PATH = "/some/path/to/test.node.enc.png"; // encrypted in nodejs
Deno.test("Deno Encryption Test", async function() {
const original = await Deno.readFile(ORIIGNAL_FILE_PATH);
const encrypted = await encrypt(original, PASSWORD);
await Deno.writeFile(ENCRYPTED_FILE, encrypted);
const decrypted = await decrypt(encrypted, PASSWORD);
await Deno.writeFile(DECRYPTED_FILE, decrypted);
Assertions.assertEquals(original, decrypted); // PASS
});
Deno.test("Node 2 Deno Encryption Test", async function() {
const original = await Deno.readFile(ORIGINAL_FILE_PATH);
const encrypted = await Deno.readFile(NODE_ENCRYPTED_FILE_PATH);
const decrypted = await decrypt(encrypted, PASSWORD); // OperationError: Decryption failed
Assertions.assertEquals(original, decrypted);
});
It seems not to be same encryption process(AES-GCM 128bits...) Let me know what I did wrong...
I tried compare thoes file's size (they are same.)
And making iv same as key, then compare encrypted bytes in node and deno. they are not same. (In same key and iv, the result of cipher.update
and the result of crypto.subtle.encrypt
are different.)
GCM is an authenticated encryption mode meaning that the encryption operation produces two pieces of data -- the ciphertext and an authentication tag -- both of which must be supplied for a correct decryption operation.
The nodejs 'traditional' crypto
API, based on OpenSSL, returns and accepts these two values separately -- see https://nodejs.org/docs/latest-v20.x/api/crypto.html#class-cipher and https://nodejs.org/docs/latest-v20.x/api/crypto.html#class-decipher . In contrast in the WebCrypto API encrypt
returns the concatenation of ciphertext||tag and decrypt
requires that same format; see encrypt step 7 and decrypt steps 5 and 6. While there are several possible ways to handle this, since you are already concatenating IV to the beginning of your data, the simplest way here is to adopt the WebCrypto method and have the nodejs code concatenate the tag to the end of the data, something like this (names changed to avoid ambiguity and ts removed for simplicity):
const nodecrypto = require('crypto'), webcrypto = globalThis.crypto;
function node_enc(buffer,pass){
const key = new TextEncoder().encode(pass);
const iv = nodecrypto.randomBytes(16);
const cipher = nodecrypto.createCipheriv("aes-128-gcm", key, iv);
return Buffer.concat([iv, cipher.update(buffer),cipher.final(),cipher.getAuthTag()]);
}
function node_dec(buffer,pass){
const key = new TextEncoder().encode(pass);
const iv = buffer.slice(0,16), ct = buffer.slice(16,buffer.length-16), tag = buffer.slice(buffer.length-16);
const decipher = nodecrypto.createDecipheriv("aes-128-gcm", key, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ct),decipher.final()]);
}
async function webc_enc(buffer,pass){
const key = new TextEncoder().encode(pass);
const iv = webcrypto.getRandomValues(new Uint8Array(16));
const cryptoKey = await webcrypto.subtle.importKey("raw", key, { name: "AES-GCM", length: 128 }, true, ["encrypt"]);
const encrypted = await webcrypto.subtle.encrypt({ name: "AES-GCM", length: 128, iv }, cryptoKey, buffer);
return new Uint8Array([...iv, ...new Uint8Array(encrypted)]);
}
async function webc_dec(buffer,pass){
const key = new TextEncoder().encode(pass);
const iv = buffer.slice(0, 16);
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM", length: 128 }, true, ["decrypt"]);
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", length: 128, iv }, cryptoKey, buffer.slice(16));
return new Uint8Array(decrypted);
}
(async() => {
const pass = "some-passwrod".padEnd(16,"0");
const original = new TextEncoder().encode("some data"), td = new TextDecoder();
var encrypted, decrypted;
encrypted = node_enc(original,pass);
console.log(td.decode(node_dec(encrypted,pass)));
console.log(td.decode(await webc_dec(encrypted,pass)));
encrypted = await webc_enc(original,pass);
console.log(td.decode(node_dec(encrypted,pass)));
console.log(td.decode(await webc_dec(encrypted,pass)));
})().catch(console.error);
BTW GCM is designed and optimized for a 12-byte nonce aka IV; although the standard allows other sizes, like your 16, it is less efficient AND less secure, and thus not recommended. Unless you have some really strong reason for using 16, I recommend following the standard and using 12.