diff --git a/package.json b/package.json index 8f134c2..8fa6a82 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,9 @@ "./esm/biginteger/*": { "import": "./esm/biginteger/*.js" }, + "./hmac": { + "import": "./esm/hmac.js" + }, "./ripemd160": { "import": "./esm/ripemd160.js" }, diff --git a/src/hmac.ts b/src/hmac.ts new file mode 100644 index 0000000..da8d76a --- /dev/null +++ b/src/hmac.ts @@ -0,0 +1,81 @@ +import assert from './_assert.js'; +import { Hash, CHash, Input, toBytes } from './utils.js'; +// HMAC (RFC 2104) +export class HMAC> extends Hash> { + oHash: T; + iHash: T; + blockLen: number; + outputLen: number; + private finished = false; + private destroyed = false; + + constructor(hash: CHash, _key: Input) { + super(); + assert.hash(hash); + const key = toBytes(_key); + this.iHash = hash.create() as T; + if (typeof this.iHash.update !== 'function') + throw new Error('Expected instance of class which extends utils.Hash'); + this.blockLen = this.iHash.blockLen; + this.outputLen = this.iHash.outputLen; + const blockLen = this.blockLen; + const pad = new Uint8Array(blockLen); + // blockLen can be bigger than outputLen + pad.set(key.length > blockLen ? hash.create().update(key).digest() : key); + for (let i = 0; i < pad.length; i++) pad[i] ^= 0x36; + this.iHash.update(pad); + // By doing update (processing of first block) of outer hash here we can re-use it between multiple calls via clone + this.oHash = hash.create() as T; + // Undo internal XOR && apply outer XOR + for (let i = 0; i < pad.length; i++) pad[i] ^= 0x36 ^ 0x5c; + this.oHash.update(pad); + pad.fill(0); + } + update(buf: Input) { + assert.exists(this); + this.iHash.update(buf); + return this; + } + digestInto(out: Uint8Array) { + assert.exists(this); + assert.bytes(out, this.outputLen); + this.finished = true; + this.iHash.digestInto(out); + this.oHash.update(out); + this.oHash.digestInto(out); + this.destroy(); + } + digest() { + const out = new Uint8Array(this.oHash.outputLen); + this.digestInto(out); + return out; + } + _cloneInto(to?: HMAC): HMAC { + // Create new instance without calling constructor since key already in state and we don't know it. + to ||= Object.create(Object.getPrototypeOf(this), {}); + const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this; + to = to as this; + to.finished = finished; + to.destroyed = destroyed; + to.blockLen = blockLen; + to.outputLen = outputLen; + to.oHash = oHash._cloneInto(to.oHash); + to.iHash = iHash._cloneInto(to.iHash); + return to; + } + destroy() { + this.destroyed = true; + this.oHash.destroy(); + this.iHash.destroy(); + } +} + +/** + * HMAC: RFC2104 message authentication code. + * @param hash - function that would be used e.g. sha256 + * @param key - message key + * @param message - message data + */ +export const hmac = (hash: CHash, key: Input, message: Input): Uint8Array => + new HMAC(hash, key).update(message).digest(); +hmac.create = (hash: CHash, key: Input) => new HMAC(hash, key); \ No newline at end of file diff --git a/test/hmac.test.js b/test/hmac.test.js new file mode 100644 index 0000000..656b7b1 --- /dev/null +++ b/test/hmac.test.js @@ -0,0 +1,223 @@ +import assert, { deepStrictEqual } from 'assert'; +import { should } from 'micro-should'; +import { sha256 } from '../esm/sha256.js'; +import { sha512, sha384 } from '../esm/sha512.js'; +import { hmac } from '../esm/hmac.js'; +import { + utf8ToBytes, + hexToBytes, + bytesToHex, + truncate, + concatBytes, + TYPE_TEST, + SPACE, + EMPTY, +} from './utils.js'; + +// HMAC test vectors from RFC 4231 +const HMAC_VECTORS = [ + { + key: hexToBytes('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), + data: [utf8ToBytes('Hi There')], + sha256: 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7', + sha512: + '87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cde' + + 'daa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854', + }, + { + key: utf8ToBytes('Jefe'), + data: [utf8ToBytes('what do ya want '), utf8ToBytes('for nothing?')], + sha256: '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843', + sha512: + '164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea250554' + + '9758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737', + }, + { + key: hexToBytes('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), + data: [ + hexToBytes( + 'dddddddddddddddddddddddddddddddddddddddddddddddddd' + + 'dddddddddddddddddddddddddddddddddddddddddddddddddd' + ), + ], + sha256: '773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe', + sha512: + 'fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39' + + 'bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb', + }, + { + key: hexToBytes('0102030405060708090a0b0c0d0e0f10111213141516171819'), + data: [ + hexToBytes( + 'cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd' + ), + ], + sha256: '82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b', + sha512: + 'b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3db' + + 'a91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd', + }, + { + key: hexToBytes('0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c'), + data: [utf8ToBytes('Test With Trunca'), utf8ToBytes('tion')], + sha256: 'a3b6167473100ee06e0c796c2955552b', + sha512: '415fad6271580a531d4179bc891d87a6', + truncate: 16, + }, + { + key: hexToBytes( + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaa' + ), + data: [ + utf8ToBytes('Test Using Large'), + utf8ToBytes('r Than Block-Siz'), + utf8ToBytes('e Key - Hash Key'), + utf8ToBytes(' First'), + ], + sha256: '60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54', + sha512: + '80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f352' + + '6b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598', + }, + { + key: hexToBytes( + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + + 'aaaaaa' + ), + data: [ + utf8ToBytes('This is a test u'), + utf8ToBytes('sing a larger th'), + utf8ToBytes('an block-size ke'), + utf8ToBytes('y and a larger t'), + utf8ToBytes('han block-size d'), + utf8ToBytes('ata. The key nee'), + utf8ToBytes('ds to be hashed '), + utf8ToBytes('before being use'), + utf8ToBytes('d by the HMAC al'), + utf8ToBytes('gorithm.'), + ], + sha256: '9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2', + sha512: + 'e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944' + + 'b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58', + }, +]; + +for (let i = 0; i < HMAC_VECTORS.length; i++) { + const t = HMAC_VECTORS[i]; + should(`HMAC vector (${i}) sha256 full`, () => { + const h256 = hmac.create(sha256, t.key).update(concatBytes(...t.data)); + assert.deepStrictEqual(truncate(h256.digest(), t.truncate), hexToBytes(t.sha256)); + }); + should(`HMAC vector (${i}) sha256 partial`, () => { + const h256 = hmac.create(sha256, t.key); + for (let d of t.data) h256.update(d); + assert.deepStrictEqual(truncate(h256.digest(), t.truncate), hexToBytes(t.sha256)); + }); + should(`HMAC vector (${i}) sha256 partial (cleanup=true)`, () => { + const h256 = hmac.create(sha256, t.key, { cleanup: true }); + for (let d of t.data) h256.update(d); + assert.deepStrictEqual(truncate(h256.digest(), t.truncate), hexToBytes(t.sha256)); + }); + should(`HMAC vector (${i}) sha256 partial (cleanup=false)`, () => { + const h256 = hmac.create(sha256, t.key, { cleanup: false }); + for (let d of t.data) h256.update(d); + assert.deepStrictEqual(truncate(h256.digest(), t.truncate), hexToBytes(t.sha256)); + }); + should(`HMAC vector (${i}) sha512 full`, () => { + const h512 = hmac.create(sha512, t.key).update(concatBytes(...t.data)); + assert.deepStrictEqual(truncate(h512.digest(), t.truncate), hexToBytes(t.sha512)); + }); + should(`HMAC vector (${i}) sha512 partial`, () => { + const h512 = hmac.create(sha512, t.key); + for (let d of t.data) h512.update(d); + assert.deepStrictEqual(truncate(h512.digest(), t.truncate), hexToBytes(t.sha512)); + }); + should(`HMAC vector (${i}) sha512 partial (cleanup=false)`, () => { + const h512 = hmac.create(sha512, t.key, { cleanup: false }); + for (let d of t.data) h512.update(d); + assert.deepStrictEqual(truncate(h512.digest(), t.truncate), hexToBytes(t.sha512)); + }); + should(`HMAC vector (${i}) sha512 partial (cleanup=true)`, () => { + const h512 = hmac.create(sha512, t.key, { cleanup: true }); + for (let d of t.data) h512.update(d); + assert.deepStrictEqual(truncate(h512.digest(), t.truncate), hexToBytes(t.sha512)); + }); +} + +should('HMAC types', () => { + hmac(sha256, 'key', 'msg'); + hmac.create(sha256, 'key'); + for (const t of TYPE_TEST.bytes) { + assert.throws(() => hmac(sha256, t, 'msg'), `hmac(key=${t})`); + assert.throws(() => hmac(sha256, 'key', t), `hmac(msg=${t})`); + assert.throws(() => hmac.create(sha256, t), `hmac.create(key=${t})`); + } + assert.throws(() => hmac(sha256, undefined, 'msg'), `hmac(key=undefined)`); + assert.throws(() => hmac(sha256, 'key'), `hmac(msg=undefined)`); + assert.throws(() => hmac.create(sha256, undefined), `hmac.create(key=undefined)`); + // for (const t of TYPE_TEST.opts) { + // assert.throws(() => hmac(sha256, 'key', 'salt', t), `hmac(opt=${t})`); + // assert.throws(() => hmac.create(sha256, 'key', t), `hmac.create(opt=${t})`); + // } + for (const t of TYPE_TEST.hash) assert.throws(() => hmac(t, 'key', 'salt'), `hmac(hash=${t})`); + assert.deepStrictEqual( + hmac(sha512, SPACE.str, SPACE.str), + hmac(sha512, SPACE.bytes, SPACE.bytes), + 'hmac.SPACE' + ); + assert.deepStrictEqual( + hmac(sha512, EMPTY.str, EMPTY.str), + hmac(sha512, EMPTY.bytes, EMPTY.bytes), + 'hmac.EMPTY' + ); + assert.deepStrictEqual( + hmac(sha512, SPACE.str, SPACE.str), + hmac.create(sha512, SPACE.str).update(SPACE.bytes).digest(), + 'hmac.SPACE (full form bytes)' + ); + assert.deepStrictEqual( + hmac(sha512, SPACE.str, SPACE.str), + hmac.create(sha512, SPACE.str).update(SPACE.str).digest(), + 'hmac.SPACE (full form stingr)' + ); +}); + +should('Sha512/384 issue', () => { + const h = hmac.create( + sha384, + hexToBytes( + '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + ) + ); + h.update( + hexToBytes( + '010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101' + ) + ); + h.update(hexToBytes('00')); + h.update( + hexToBytes( + '6b9d3dad2e1b8c1c05b19875b6659f4de23c3b667bf297ba9aa47740787137d896d5724e4c70a825f872c9ea60d2edf59a9083505bc92276aec4be312696ef7bf3bf603f4bbd381196a029f340585312313bca4a9b5b890efee42c77b1ee25fe' + ) + ); + deepStrictEqual( + bytesToHex(h.digest()), + 'a1ae63339c4fac449464e302c61e8ceb5b28c04d108e022179ce6dabb2d3e310cb3bf41cd6013b3006f33c037e6b7fa8' + ); +}); diff --git a/test/index.js b/test/index.js index 99b8ae5..31b66d0 100644 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,6 @@ import { should } from 'micro-should'; // // Tests generated from rust +import './hmac.test.js'; import './clone.test.js'; import './u64.test.js'; import './utils.test.js';