Skip to content

Instantly share code, notes, and snippets.

@bettse
Last active December 7, 2025 21:46
Show Gist options
  • Select an option

  • Save bettse/4a2dfac58985b20181dedab14cb3b1d3 to your computer and use it in GitHub Desktop.

Select an option

Save bettse/4a2dfac58985b20181dedab14cb3b1d3 to your computer and use it in GitHub Desktop.
EAX and EAX-Prime in javascript
exports.bitShiftLeft = function (buffer) {
var shifted = Buffer.alloc(buffer.length);
var last = buffer.length - 1;
for (var index = 0; index < last; index++) {
shifted[index] = buffer[index] << 1;
if (buffer[index + 1] & 0x80) {
shifted[index] += 0x01;
}
}
shifted[last] = buffer[last] << 1;
return shifted;
}
exports.xor = function (bufferA, bufferB) {
var length = Math.min(bufferA.length, bufferB.length);
var output = Buffer.alloc(length);
for (var index = 0; index < length; index++) {
output[index] = bufferA[index] ^ bufferB[index];
}
return output;
}
var bitmasks = [0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01];
exports.toBinaryString = function (buffer) {
var binary = '';
for (var bufferIndex = 0; bufferIndex < buffer.length; bufferIndex++) {
for (var bitmaskIndex = 0; bitmaskIndex < bitmasks.length; bitmaskIndex++) {
binary += (buffer[bufferIndex] & bitmasks[bitmaskIndex]) ? '1' : '0';
}
}
return binary;
}
const { EAXPrime_encrypt, EAXPrime_decrypt } = require('./eax-prime')
const vectors = [{
key: "01020304050607080102030405060708",
plaintext: "A20D060B607C86F7540116007BC175A803020100BE0D280B810984A60C060A607C86F7540116007B040248F3C20403300005",
cleartext:"",
ciphertext:"",
mac:"515AE775",
},{
key: "01020304050607080102030405060708",
plaintext:"a20c060a607c86f7540116001702a703020104a803020102ac0fa20da00ba109800102810448e99388be19281781159aa60d060b607c86f7540116001782110248e99388",
cleartext:"54454d500b40000700051a00000200e4",
ciphertext:"4031cc957d4edf9a357f3db0fa9fe838",
mac:"6555c029",
},{
key:"01020304050607080102030405060708",
plaintext:"a20c060a607c86f7540116007b02a703020104a803020102ac0fa20da00ba109800102810448f3d2f8be19281781159aa60d060b607c86f7540116007b82110248f3d2f8",
cleartext:"54454d500b40000700051a00000200e4",
ciphertext:"8d2fbb7a0a8c4d40edaa10a46431c9b8",
mac:"fec6d9e8",
},{
key:"102030405060708090a0b0c0d0e0f000",
plaintext:"a20e060c6086480186fc2f811caa4e01a806020439a00ebbac0fa20da00ba10980010081044bcee2c3be252823812188a60a06082b06010401828563004bcee2c3",
cleartext:"17513030303030303030303030303030303030303030000003300001",
ciphertext:"9cf32c7ec24c250be7b0749feee71a220d0eee976ec23dbf0caa08ea",
mac:"00543e66"
},{
key:"6624c7e23034e4036fe5cb3a8b5dab44",
plaintext:"a211060f2b060104018285638e7f85f1c24e01a80602042bc81aa1ac0fa20da00ba10980010081044b97d2ccbe392837813588a60906072b060104828563004b97d2cc",
cleartext:"1751303030303030303030303030303030303030303000000330000103300078033000790330007a0330007b0330007d",
ciphertext:"beb0989fadb020eb72ba46353cc0a2ac2a007a101afebaf9680d3b9659f991121b865f254f6ac92cdd213d31e3c4d2ca",
mac:'e6f89b6d'
}]
vectors.forEach((vector, i) => {
const key = Buffer.from(vector.key, 'hex')
const plaintext = Buffer.from(vector.plaintext, 'hex')
const cleartext = Buffer.from(vector.cleartext, 'hex')
const ciphertext = Buffer.from(vector.ciphertext, 'hex')
const mac = Buffer.from(vector.mac, 'hex')
const { ciphertext: ciphertextResult, mac: macResult } = EAXPrime_encrypt(key, cleartext, plaintext)
if (ciphertextResult.equals(ciphertext) && macResult.slice(-mac.length).equals(mac)) {
console.log(`Test ${i+1} passed`)
const { cleartext: cleartextResult } = EAXPrime_decrypt(key, ciphertext, plaintext, mac)
if (cleartextResult.equals(cleartext)) {
console.log('Decryption successful')
} else {
console.log('Decryption failed')
console.log({ cleartext, cleartextResult })
}
} else {
console.log(`Test ${i+1} failed`)
console.log({ ciphertext, ciphertextResult, mac, macResult })
}
console.log('----------------')
})
/*jshint esversion: 8 */
/* jshint node: true */
const crypto = require('crypto');
const bufferTools = require('./buffer-tools.js');
const debug = {
log: require('debug')('EAXPrime:Log'),
error: require('debug')('EAXPrime:Error')
}
let EAXP_REFERENCE_COUNTER_BUG = false;
let EAXP_REFERENCE_DBL_BUG = false;
// based on https://gitlab.com/polfosol/micro-AES
const blockSize = 16;
function setEAXPReferenceCounterBug(value) {
EAXP_REFERENCE_COUNTER_BUG = value;
}
function setEAXPReferenceDblBug(value) {
EAXP_REFERENCE_DBL_BUG = value;
}
function doubleLblock(array) {
let c = 0;
const output = Buffer.from(array);
for (let i = 0; i < output.length; c >>= 8) {
c |= output[i] << 1;
output[i++] = c & 0xFF;
}
output[0] ^= c * 0x87;
return output;
}
function deriveKeyDependentConstants(key) {
const Zero = Buffer.from('00000000000000000000000000000000', 'hex');
var l = encryptAES(Zero, key);
const D = doubleLblock(l);
const Q = doubleLblock(D);
return { D, Q };
};
function xMac(masterKey, seed, data) {
const n = Math.floor(data.length / blockSize);
let result = Buffer.from(seed);
for(let i = 0; i < n; i++) {
const x = data.slice(i * blockSize, i*blockSize + blockSize);
result = bufferTools.xor(result, x);
result = encryptAES(result, masterKey);
}
if (data.length % blockSize !== 0) {
const block = data.slice(n * blockSize);
for (let i = 0; i < block.length; i++) {
result[i] ^= block[i];
}
result = encryptAES(result, masterKey);
}
return result;
}
function cMac(masterKey, K1, K2, data, mac) {
const s = data.length ? (data.length - 1) % blockSize + 1 : 0;
let k = K1
const ps = s ? data.slice(-s) : Buffer.alloc(0);
mac = xMac(masterKey, mac, data.slice(0, data.length - s));
if (s < blockSize) {
mac[s] ^= 0x80;
k = K2;
}
mac = bufferTools.xor(k, mac);
mac = xMac(masterKey, mac, ps)
return mac
}
function OMac(t, masterKey, D, Q, data) {
const zero_mac = t && !data.length;
const K = t ? Q : D;
let mac = zero_mac ? Buffer.alloc(16) : Buffer.from(K);
if (zero_mac) {
return mac;
}
mac = cMac(masterKey, D, Q, data, mac);
return mac;
}
function encryptAES(clear, key, iv = Buffer.alloc(16)) {
try {
let encipher = crypto.createCipheriv('aes-128-cbc', key, iv);
encipher.setAutoPadding(false);
let encrypted = encipher.update(clear);
encrypted = Buffer.concat([encrypted, encipher.final()]);
return encrypted;
} catch (error) {
debug.error(error);
return Buffer.alloc(0);
}
}
function decryptAES(encrypted, key) {
const zeros = Buffer.alloc(blockSize, 0);
const iv = zeros;
try {
let decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
decipher.setAutoPadding(false);
let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted;
} catch (error) {
debug.error(error);
return Buffer.alloc(0);
}
}
function CTR_cipher(key, iv, clear) {
try {
let encipher = crypto.createCipheriv('aes-128-ctr', key, iv);
encipher.setAutoPadding(true);
let encrypted = encipher.update(clear);
encrypted = Buffer.concat([encrypted, encipher.final()]);
return encrypted;
} catch (error) {
debug.error(error);
return Buffer.alloc(0);
}
}
// EAX’ takes as input a Cleartext (N) and an optional Plaintext (P)
function EAXPrime_encrypt(k, n, p) {
const { D, Q } = deriveKeyDependentConstants(k);
const N = OMac(0, k, D, Q, p);
let NPrime = Buffer.from(N);
if (EAXP_REFERENCE_COUNTER_BUG) {
NPrime[11] &= 0x7F;
NPrime[13] &= 0x7F;
} else {
NPrime[12] &= 0x7F;
NPrime[14] &= 0x7F;
}
const C = CTR_cipher(k, NPrime, n);
const CPrime = OMac(2, k, D, Q, C);
let tag = Buffer.alloc(blockSize);
for (let i = 0; i < CPrime.length; i++) {
tag[i] = CPrime[i] ^ N[i];
}
return { ciphertext: C, mac: tag };
}
//N : Cleartext nonce?
function EAXPrime_decrypt(k, c, p, t) {
const { D, Q } = deriveKeyDependentConstants(k);
const CPrime = OMac(2, k, D, Q, c);
const N = OMac(0, k, D, Q, p);
for(let i = blockSize - t.length; i < CPrime.length; i++) {
CPrime[i] ^= N[i];
}
let NPrime = Buffer.from(N);
// TODO: should this have the same bug as the encrypt function?
NPrime[12] &= 0x7F;
NPrime[14] &= 0x7F;
if (CPrime.slice(-t.length).toString('hex') != t.toString('hex')) {
debug.log({CPrime: CPrime.toString('hex')});
throw new Error(`Invalid tag: ${CPrime.slice(-t.length).toString('hex')} != ${t.toString('hex')}`);
}
const cleartext = CTR_cipher(k, NPrime, c);
return { cleartext };
}
module.exports = {
EAXPrime_encrypt,
EAXPrime_decrypt,
setEAXPReferenceCounterBug,
setEAXPReferenceDblBug,
};
/*jshint esversion: 11 */
/* jshint node: true */
const { EAX_encrypt, EAX_decrypt } = require('./eax');
const vectors = [{
msg: '',
key: '233952DEE4D5ED5F9B9C6D6FF80FF478',
nonce: '62EC67F9C3A4A407FCB2A8C49031A8B3',
header: '6BFB914FD07EAE6B',
cipher: 'E037830E8389F27B025A2D6527E79D01'
},{
msg: 'F7FB',
key: '91945D3F4DCBEE0BF45EF52255F095A4',
nonce: 'BECAF043B0A23D843194BA972C66DEBD',
header: 'FA3BFD4806EB53FA',
cipher: '19DD5C4C9331049D0BDAB0277408F67967E5'
},{
msg: '1A47CB4933',
key: '01F74AD64077F2E704C0F60ADA3DD523',
nonce: '70C3DB4F0D26368400A10ED05D2BFF5E',
header: '234A3463C1264AC6',
cipher: 'D851D5BAE03A59F238A23E39199DC9266626C40F80'
},{
msg: '481C9E39B1',
key: 'D07CF6CBB7F313BDDE66B727AFD3C5E8',
nonce: '8408DFFF3C1A2B1292DC199E46B7D617',
header: '33CCE2EABFF5A79D',
cipher: '632A9D131AD4C168A4225D8E1FF755939974A7BEDE'
},{
msg: '40D0C07DA5E4',
key: '35B6D0580005BBC12B0587124557D2C2',
nonce: 'FDB6B06676EEDC5C61D74276E1F8E816',
header: 'AEB96EAEBE2970E9',
cipher: '071DFE16C675CB0677E536F73AFE6A14B74EE49844DD'
},{
msg: '4DE3B35C3FC039245BD1FB7D',
key: 'BD8E6E11475E60B268784C38C62FEB22',
nonce: '6EAC5C93072D8E8513F750935E46DA1B',
header: 'D4482D1CA78DCE0F',
cipher: '835BB4F15D743E350E728414ABB8644FD6CCB86947C5E10590210A4F'
},{
msg: '8B0A79306C9CE7ED99DAE4F87F8DD61636',
key: '7C77D6E813BED5AC98BAA417477A2E7D',
nonce: '1A8C98DCD73D38393B2BF1569DEEFC19',
header: '65D2017990D62528',
cipher: '02083E3979DA014812F59F11D52630DA30137327D10649B0AA6E1C181DB617D7F2'
},{
msg: '1BDA122BCE8A8DBAF1877D962B8592DD2D56',
key: '5FFF20CAFAB119CA2FC73549E20F5B0D',
nonce: 'DDE59B97D722156D4D9AFF2BC7559826',
header: '54B9F04E6A09189A',
cipher: '2EC47B2C4954A489AFC7BA4897EDCDAE8CC33B60450599BD02C96382902AEF7F832A'
},{
msg: '6CF36720872B8513F6EAB1A8A44438D5EF11',
key: 'A4A4782BCFFD3EC5E7EF6D8C34A56123',
nonce: 'B781FCF2F75FA5A8DE97A9CA48E522EC',
header: '899A175897561D7E',
cipher: '0DE18FD0FDD91E7AF19F1D8EE8733938B1E8E7F6D2231618102FDB7FE55FF1991700'
},{
msg: 'CA40D7446E545FFAED3BD12A740A659FFBBB3CEAB7',
key: '8395FCF1E95BEBD697BD010BC766AAC3',
nonce: '22E7ADD93CFC6393C57EC0B3C17D6B44',
header: '126735FCC320D25A',
cipher: 'CB8920F87A6C75CFF39627B56E3ED197C552D295A7CFC46AFC253B4652B1AF3795B124AB6E'
}];
vectors.forEach((vector, i) => {
const msg = Buffer.from(vector.msg, 'hex');
const key = Buffer.from(vector.key, 'hex');
const nonce = Buffer.from(vector.nonce, 'hex');
const header = Buffer.from(vector.header, 'hex');
const cipher = Buffer.from(vector.cipher, 'hex');
const response = EAX_encrypt(key, nonce, msg, header);
if (response.equals(cipher)) {
console.log(`Test ${i+1} passed`);
const msgResult = EAX_decrypt(key, nonce, cipher, header);
if (msgResult.equals(msg)) {
console.log('Decryption successful');
} else {
console.log('Decryption failed');
console.log({ msg, msgResult });
}
} else {
console.log(`Test ${i+1} failed`);
}
console.log('----------------');
});
/*jshint esversion: 8 */
/* jshint node: true */
const { aesCmac } = require('node-aes-cmac');
const crypto = require('crypto');
const bufferTools = require('./buffer-tools.js');
const debug = {
log: require('debug')('EAX:Log'),
error: require('debug')('EAX:Error')
};
const blockSize = 16;
function OMac(k, data, t) {
const tBlock = Buffer.alloc(blockSize);
tBlock[blockSize - 1] = t;
const cmac = aesCmac(k, Buffer.concat([tBlock, data]), {returnAsBuffer: true});
return cmac;
}
function CTR_cipher(key, iv, clear) {
try {
let encipher = crypto.createCipheriv('aes-128-ctr', key, iv.slice(0, key.length));
encipher.setAutoPadding(false);
let encrypted = encipher.update(clear);
encrypted = Buffer.concat([encrypted, encipher.final()]);
return encrypted;
} catch (error) {
debug.error(error);
return Buffer.alloc(0);
}
}
function EAX_encrypt(k, n, m, h = Buffer.alloc(0)) {
const N = OMac(k, n, 0);
const H = OMac(k, h, 1);
const C = CTR_cipher(k, N, m);
const E = OMac(k, C, 2);
const Tag = bufferTools.xor(bufferTools.xor(N, E), H);
const T = Tag;
return Buffer.concat([C, T]);
}
function EAX_decrypt(k, n, ct, h = Buffer.alloc(0)) {
const c = ct.slice(0, -blockSize);
const t = ct.slice(-blockSize);
const N = OMac(k, n, 0);
const H = OMac(k, h, 1);
const E = OMac(k, c, 2);
const Tag = bufferTools.xor(bufferTools.xor(N, E), H);
if (!Tag.equals(t)) {
throw new Error(`Invalid tag: ${Tag.toString('hex')} != ${t.toString('hex')}`);
}
const m = CTR_cipher(k, N, c);
return m;
}
module.exports = {
EAX_encrypt,
EAX_decrypt,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment