Last active
September 2, 2021 07:05
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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