Last active
December 7, 2025 21:46
-
-
Save bettse/4a2dfac58985b20181dedab14cb3b1d3 to your computer and use it in GitHub Desktop.
EAX and EAX-Prime in javascript
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
| 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; | |
| } |
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 { 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('----------------') | |
| }) |
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
| /*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, | |
| }; |
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
| /*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('----------------'); | |
| }); |
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
| /*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