Skip to content
This repository has been archived by the owner on Oct 23, 2020. It is now read-only.

Add HMAC #12

Merged
merged 2 commits into from
Aug 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/algorithms.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const { AES_CTR, AES_CBC, AES_GCM, AES_KW } = require('./algorithms/aes');
const { HKDF } = require('./algorithms/hkdf');
const { HMAC } = require('./algorithms/hmac');
const { PBKDF2 } = require('./algorithms/pbkdf2');
const { SHA_1, SHA_256, SHA_384, SHA_512 } = require('./algorithms/sha');
const { NotSupportedError } = require('./errors');
Expand All @@ -14,6 +15,8 @@ const algorithms = [

HKDF,

HMAC,

PBKDF2,

SHA_1,
Expand Down Expand Up @@ -46,7 +49,8 @@ const supportedAlgorithms = objectFromArray([
'get key length',

// The following APIs are for internal use only.
'get hash function'
'get hash function',
'get hash block size'
], (opsByName, op) => {
opsByName[op] = objectFromArray(algorithms, (algsByName, alg) => {
if (typeof alg[op] === 'function')
Expand Down
95 changes: 95 additions & 0 deletions lib/algorithms/hmac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use strict';

const {
createHmac,
randomBytes: randomBytesCallback,
timingSafeEqual
} = require('crypto');
const { promisify } = require('util');

const algorithms = require('../algorithms');
const { DataError, NotSupportedError } = require('../errors');
const { kKeyMaterial, CryptoKey } = require('../key');
const {
limitUsages,
opensslHashFunctionName,
getHashBlockSize
} = require('../util');

const randomBytes = promisify(randomBytesCallback);

module.exports.HMAC = {
name: 'HMAC',

sign(algorithm, key, data) {
const hashFn = opensslHashFunctionName(key.algorithm.hash);
return createHmac(hashFn, key[kKeyMaterial]).update(data).digest();
},

verify(algorithm, key, signature, data) {
return timingSafeEqual(this.sign(algorithm, key, data), signature);
},

async generateKey(algorithm, extractable, usages) {
let { length } = algorithm;

limitUsages(usages, ['sign', 'verify']);

const hashAlg = {
name: algorithms.getAlgorithm(algorithm.hash, 'digest').name
};

if (length === undefined)
length = getHashBlockSize(hashAlg.name);
else if (length === 0)
throw new DataError();

const bits = await randomBytes(length >> 3);

return new CryptoKey('secret', {
name: this.name,
hash: hashAlg,
length
}, extractable, usages, bits);
},

importKey(keyFormat, keyData, params, extractable, keyUsages) {
if (keyFormat !== 'raw')
throw new NotSupportedError();

let { length } = params;

limitUsages(keyUsages, ['sign', 'verify']);

const hashAlg = {
name: algorithms.getAlgorithm(params.hash, 'digest').name
};

const data = Buffer.from(keyData);
if (data.length === 0)
throw new DataError();

const dataLength = data.length << 3;
if (length === undefined) {
length = dataLength;
} else if (length > dataLength || length <= dataLength - 8) {
throw new DataError();
}

const alg = {
name: this.name,
hash: hashAlg,
length
};

return new CryptoKey('secret', alg, extractable, keyUsages, data);
},

exportKey(format, key) {
if (format !== 'raw')
throw new NotSupportedError();

const bits = key[kKeyMaterial];
return Buffer.from(bits);
}
};
14 changes: 9 additions & 5 deletions lib/algorithms/sha.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const { createHash } = require('crypto');

function implement(name, opensslName) {
function implement(name, opensslName, blockSize) {
return {
name,

Expand All @@ -12,11 +12,15 @@ function implement(name, opensslName) {

'get hash function'() {
return opensslName;
},

'get hash block size'() {
return blockSize;
}
};
}

module.exports.SHA_1 = implement('SHA-1', 'sha1');
module.exports.SHA_256 = implement('SHA-256', 'sha256');
module.exports.SHA_384 = implement('SHA-384', 'sha384');
module.exports.SHA_512 = implement('SHA-512', 'sha512');
module.exports.SHA_1 = implement('SHA-1', 'sha1', 512);
module.exports.SHA_256 = implement('SHA-256', 'sha256', 512);
module.exports.SHA_384 = implement('SHA-384', 'sha384', 1024);
module.exports.SHA_512 = implement('SHA-512', 'sha512', 1024);
14 changes: 8 additions & 6 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ module.exports.toBuffer = (source) => {
}
};

module.exports.opensslHashFunctionName = (algorithm) => {
const op = 'get hash function';
return algorithms.getAlgorithm(algorithm, op)[op]();
};
function callOp(op) {
return (algorithm) => algorithms.getAlgorithm(algorithm, op)[op]();
}

module.exports.opensslHashFunctionName = callOp('get hash function');
module.exports.getHashBlockSize = callOp('get hash block size');

module.exports.limitUsages = (usages, allowed) => {
module.exports.limitUsages = (usages, allowed, err = SyntaxError) => {
for (const usage of usages) {
if (!allowed.includes(usage))
throw new SyntaxError();
throw new err();
}
};
66 changes: 66 additions & 0 deletions test/algorithms/hmac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

const assert = require('assert');
const { randomBytes } = require('crypto');

const { subtle } = require('../../');

describe('HMAC', () => {
it('should generate, import and export keys', async () => {
for (const [length, keyLength] of [[undefined, 1024], [3000, 3000]]) {
const key = await subtle.generateKey({
name: 'HMAC',
hash: 'SHA-384',
length
}, true, ['sign', 'verify']);

assert.strictEqual(key.type, 'secret');
assert.strictEqual(key.algorithm.name, 'HMAC');
assert.strictEqual(key.algorithm.length, keyLength);
assert.strictEqual(key.algorithm.hash.name, 'SHA-384');

const expKey = await subtle.exportKey('raw', key);
assert(Buffer.isBuffer(expKey));
assert.strictEqual(expKey.length, keyLength >> 3);

const impKey = await subtle.importKey('raw', expKey, {
name: 'HMAC',
hash: 'SHA-384'
}, true, ['verify']);

assert.deepStrictEqual(await subtle.exportKey('raw', impKey), expKey);
}
});

it('should sign and verify data', async () => {
const key = await subtle.generateKey({
name: 'HMAC',
hash: 'SHA-256'
}, false, ['sign', 'verify']);

const data = randomBytes(200);
const signature = await subtle.sign('HMAC', key, data);

const ok = await subtle.verify('HMAC', key, signature, data);
assert.strictEqual(ok, true);
});

it('should verify externally signed data', async () => {
const keyData = '99fc199e37c3a4ba9b15ba215758e2f55df4debc3f5aeffa45d4cb84' +
'6328b1e297092a076efa70fa414da741d945d2defa9c4c5fa44e5222' +
'e4548c43e7c35e82';
const keyBuffer = Buffer.from(keyData, 'hex');
const key = await subtle.importKey('raw', keyBuffer, {
name: 'HMAC',
hash: 'SHA-256'
}, false, ['verify']);

const data = Buffer.from('0a0b0c0d0e0f', 'hex');
const signatureData = 'f4c0917359ed070531cc079660b9e88541073c7d31ff417be1' +
'fc6c7d5c5aa94f';
const signature = Buffer.from(signatureData, 'hex');

const ok = await subtle.verify('HMAC', key, signature, data);
assert.strictEqual(ok, true);
});
});