Skip to content

Instantly share code, notes, and snippets.

@sify21
Last active September 2, 2021 07:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sify21/d322b33ffe64318803b01e24134b7318 to your computer and use it in GitHub Desktop.
Save sify21/d322b33ffe64318803b01e24134b7318 to your computer and use it in GitHub Desktop.
aes-gcm-siv implementation using node-forge. A direct translation of https://github.com/bjornedstrom/aes-gcm-siv-py
const forge = require('node-forge');
forge.options.usePureJavaScript = true;
class Field {
static _MOD = [0n, 121n, 126n, 127n, 128n].map(i => 1n << i).reduce(((previousValue, currentValue) => previousValue + currentValue));
static _INV = [0n, 114n, 121n, 124n, 127n].map(i => 1n << i).reduce(((previousValue, currentValue) => previousValue + currentValue));
static add(x, y) {
return x ^ y;
}
static mul(x, y) {
let res = 0n;
for (let bit = 0n; bit < 128n; bit++) {
if (((y >> bit) & 1n) > 0) {
res ^= (2n ** bit) * x;
}
}
return Field.mod(res, Field._MOD);
}
static dot(a, b) {
return Field.mul(Field.mul(a, b), Field._INV);
}
static mod(a, m) {
let m2 = m;
let i = 0n;
while (m2 < a) {
m2 <<= 1n;
i += 1n;
}
while (i >= 0n) {
let a2 = a ^ m2;
if (a2 < a) {
a = a2;
}
m2 >>= 1n;
i -= 1n;
}
return a;
}
}
class PolyvalIUF {
constructor(h, nonce) {
this._s = 0n;
this._h = b2i(h);
this._nonce = nonce;
}
_split16(inp) {
let ret = [];
for (let i = 0; i < inp.length; i += 16) {
ret.push(inp.slice(i, i + 16));
}
return ret;
}
_update16(inp) {
this._s = Field.dot(Field.add(this._s, b2i(inp)), this._h);
}
update(inp) {
this._split16(inp).forEach((block) => {
this._update16(right_pad_to_16(block));
});
}
digest() {
let S_s = i2b(this._s);
for (let i = 0; i < 12; i++) {
S_s[i] ^= this._nonce[i];
}
S_s[15] &= 0x7f;
return S_s;
}
}
function right_pad_to_16(b) {
if (b.length < 16) {
b = Buffer.concat([b, Buffer.alloc(16 - b.length, 0)])
}
return b;
}
//Buffer to BigInt
function b2i(s) {
let res = 0n;
Buffer.from(s).reverse().forEach((c) => {
res <<= 8n;
res |= BigInt(c);
});
return res;
}
//BigInt to Buffer
function i2b(i) {
if (i === 0n) {
return Buffer.alloc(16, 0);
}
let s = [];
while (i) {
s.push(Number(i & 0xffn))
i >>= 8n;
}
return Buffer.from(s);
}
function le_uint32(i) {
let buf = Buffer.alloc(4, 0);
buf.writeUInt32LE(i);
return buf;
}
function read_le_uint32(b) {
return b.readUInt32LE();
}
function le_uint64(i) {
let buf = Buffer.alloc(8, 0);
buf.writeBigUInt64LE(BigInt(i) & 0xffffffffffffffffn)
return buf;
}
class AES_GCM_SIV {
// Buffer
constructor(key_gen_key, nonce) {
let aes_obj = forge.cipher.createCipher('AES-ECB', new forge.util.ByteBuffer(key_gen_key));
aes_obj.start();
aes_obj.update(new forge.util.ByteBuffer(Buffer.concat([le_uint32(0), nonce])));
aes_obj.finish();
let p1 = Buffer.from(aes_obj.output.getBytes(), 'binary').slice(0, 8);
aes_obj.start();
aes_obj.update(new forge.util.ByteBuffer(Buffer.concat([le_uint32(1), nonce])));
aes_obj.finish();
let p2 = Buffer.from(aes_obj.output.getBytes(), 'binary').slice(0, 8);
let msg_auth_key = Buffer.concat([p1, p2]);
aes_obj.start();
aes_obj.update(new forge.util.ByteBuffer(Buffer.concat([le_uint32(2), nonce])));
aes_obj.finish();
p1 = Buffer.from(aes_obj.output.getBytes(), 'binary').slice(0, 8);
aes_obj.start();
aes_obj.update(new forge.util.ByteBuffer(Buffer.concat([le_uint32(3), nonce])));
aes_obj.finish();
p2 = Buffer.from(aes_obj.output.getBytes(), 'binary').slice(0, 8);
let msg_enc_key = Buffer.concat([p1, p2]);
if (key_gen_key.length === 32) {
aes_obj.start();
aes_obj.update(new forge.util.ByteBuffer(Buffer.concat([le_uint32(4), nonce])));
aes_obj.finish();
p1 = Buffer.from(aes_obj.output.getBytes(), 'binary').slice(0, 8);
aes_obj.start();
aes_obj.update(new forge.util.ByteBuffer(Buffer.concat([le_uint32(5), nonce])));
aes_obj.finish();
p2 = Buffer.from(aes_obj.output.getBytes(), 'binary').slice(0, 8);
msg_enc_key = Buffer.concat([msg_enc_key, p1, p2]);
}
this.msg_auth_key = msg_auth_key;
this.msg_enc_key = msg_enc_key;
this.nonce = nonce;
}
_polyval_calc(plaintext, additional_data) {
let pvh = new PolyvalIUF(this.msg_auth_key, this.nonce);
pvh.update(additional_data);
pvh.update(plaintext);
let length_block = Buffer.concat([le_uint64(additional_data.length * 8), le_uint64(plaintext.length * 8)]);
pvh.update(length_block);
return pvh.digest();
}
_aes_ctr(key, initial_block, inp) {
let block = Buffer.from(initial_block);
let cipher = forge.cipher.createCipher('AES-ECB', new forge.util.ByteBuffer(key));
let output = []
while (inp.length > 0) {
cipher.start();
cipher.update(new forge.util.ByteBuffer(block));
cipher.finish();
let keystream_block = Buffer.from(cipher.output.getBytes(), 'binary').slice(0, block.length);
block = Buffer.concat([le_uint32(((read_le_uint32(block.slice(0, 4)) + 1) & 0xffffffff) >>> 0), block.slice(4)]);
let todo = Math.min(inp.length, keystream_block.length);
for (let i = 0; i < todo; i++) {
output.push(keystream_block[i] ^ inp [i]);
}
inp = inp.slice(todo);
}
return Buffer.from(output);
}
encrypt(plaintext, additional_data) {
if (plaintext.length > 2 ** 36) {
console.log("plaintext too large");
process.exit(1);
}
if (additional_data.length > 2 ** 36) {
console.log("additional_data too large");
process.exit(1);
}
let S_s = this._polyval_calc(plaintext, additional_data);
let cipher = forge.cipher.createCipher('AES-ECB', new forge.util.ByteBuffer(this.msg_enc_key));
cipher.start();
cipher.update(new forge.util.ByteBuffer(S_s));
cipher.finish();
// TODO: probably a bug? In python I don't need to slice
let tag = Buffer.from(cipher.output.getBytes(), 'binary').slice(0, S_s.length);
let counter_block = Buffer.from(tag);
counter_block[15] |= 0x80;
return Buffer.concat([this._aes_ctr(this.msg_enc_key, counter_block, plaintext), tag]);
}
decrypt(ciphertext, additional_data) {
if (ciphertext.length < 16 || ciphertext.length > 2 ** 36 + 16) {
console.log("ciphertext too small or too large");
process.exit(1);
}
if (additional_data.length > 2 ** 36) {
console.log("additional_data too large");
process.exit(1);
}
let cipherdata = ciphertext.slice(0, -16);
let tag = ciphertext.slice(-16);
let counter_block = Buffer.from(tag);
counter_block[15] |= 0x80;
let plaintext = this._aes_ctr(this.msg_enc_key, counter_block, cipherdata);
let S_s = this._polyval_calc(plaintext, additional_data);
let cipher = forge.cipher.createCipher('AES-ECB', new forge.util.ByteBuffer(this.msg_enc_key));
cipher.start();
cipher.update(new forge.util.ByteBuffer(S_s));
cipher.finish();
//TODO: same bug
let expected_tag = Buffer.from(cipher.output.getBytes(), 'binary').slice(0, S_s.length);
let xor_sum = 0;
for (let i = 0; i < expected_tag.length; i++) {
xor_sum |= expected_tag[i] ^ tag[i];
}
if (xor_sum !== 0) {
console.log("auth fail");
process.exit(1);
}
return plaintext;
}
}
module.exports = AES_GCM_SIV;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment