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

Commit

Permalink
Add JWK support to AES
Browse files Browse the repository at this point in the history
  • Loading branch information
tniessen committed Aug 12, 2019
1 parent e6e483b commit b4ea2d7
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 13 deletions.
74 changes: 64 additions & 10 deletions lib/algorithms/aes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ const {
} = require('crypto');
const { promisify } = require('util');

const { NotSupportedError, OperationError } = require('../errors');
const { DataError, NotSupportedError, OperationError } = require('../errors');
const { kKeyMaterial, CryptoKey } = require('../key');
const { limitUsages, toBuffer } = require('../util');
const {
decodeBase64Url,
encodeBase64Url,
limitUsages,
toBuffer
} = require('../util');

const randomBytes = promisify(randomBytesCallback);

Expand All @@ -28,24 +33,73 @@ const aesBase = {
},

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

limitUsages(keyUsages, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']);

const buf = toBuffer(keyData);
let buf;
if (keyFormat === 'raw') {
buf = toBuffer(keyData);
} else if (keyFormat === 'jwk') {
if (typeof keyData !== 'object' || keyData === null)
throw new DataError();

const {
kty: jwkKty,
k: jwkData,
alg: jwkAlg,
use: jwkUse,
key_ops: jwkKeyOps,
ext: jwkExt
} = keyData;

if (jwkKty !== 'oct')
throw new DataError();

buf = decodeBase64Url(jwkData);

if (jwkAlg !== undefined) {
if (jwkAlg !== `A${buf.length << 3}${this.name.substr(4)}`)
throw new DataError();
}

if (keyUsages.length !== 0 && jwkUse !== undefined && jwkUse !== 'enc')
throw new DataError();

if (jwkKeyOps !== undefined) {
if (!Array.isArray(jwkKeyOps))
throw new DataError();
limitUsages(keyUsages, jwkKeyOps, DataError);
}

if (jwkExt !== undefined && jwkExt === false && extractable) {
throw new DataError();
}
} else {
throw new NotSupportedError();
}

if (buf.length !== 16 && buf.length !== 24 && buf.length !== 32)
throw new OperationError();
throw new DataError();

return new CryptoKey('secret', { name: this.name, length: buf.length << 3 },
extractable, keyUsages,
createSecretKey(toBuffer(keyData)));
createSecretKey(buf));
},

exportKey(format, key) {
if (format !== 'raw')
const buf = key[kKeyMaterial].export();
if (format === 'raw') {
return buf;
} else if (format === 'jwk') {
return {
kty: 'oct',
alg: `A${buf.length << 3}${this.name.substr(4)}`,
k: encodeBase64Url(buf),
key_ops: key.usages,
ext: key.extractable
};
} else {
throw new NotSupportedError();
return key[kKeyMaterial].export();
}
},

'get key length'(algorithm) {
Expand Down
6 changes: 5 additions & 1 deletion lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ OperationError.prototype.name = 'OperationError';
class QuotaExceededError extends Error {}
QuotaExceededError.prototype.name = 'QuotaExceededError';

class DataError extends Error {}
DataError.prototype.name = 'DataError';

module.exports = {
NotSupportedError,
InvalidAccessError,
OperationError,
QuotaExceededError
QuotaExceededError,
DataError
};
34 changes: 32 additions & 2 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,39 @@ module.exports.opensslHashFunctionName = (algorithm) => {
return algorithms.getAlgorithm(algorithm, op)[op]();
};

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();
}
};

module.exports.decodeBase64Url = (enc) => {
enc = enc.split('').map((c) => {
switch (c) {
case '-':
return '+';
case '_':
return '/';
default:
return c;
}
}).join('');

return Buffer.from(enc, 'base64');
};

module.exports.encodeBase64Url = (enc) => {
return enc.toString('base64').split('').map((c) => {
switch (c) {
case '+':
return '-';
case '/':
return '_';
case '=':
return '';
default:
return c;
}
}).join('');
};
47 changes: 47 additions & 0 deletions test/algorithms/aes.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,48 @@ function testGenImportExport(name) {
};
}

function testJsonWebKeys(algorithmName, jwkAlgorithmSuffix) {
async function test(jwk, hexKey, length) {
const key = await subtle.importKey('jwk', jwk, algorithmName, true,
['encrypt', 'decrypt']);
assert.strictEqual(key.algorithm.name, algorithmName);
assert.strictEqual(key.algorithm.length, length);

assert.strictEqual((await subtle.exportKey('raw', key)).toString('hex'),
hexKey);
assert.deepStrictEqual(await subtle.exportKey('jwk', key), jwk);
}

return async () => {
return Promise.all([
// Example A.3 from RFC7517.
test({
kty: 'oct',
alg: `A128${jwkAlgorithmSuffix}`,
k: 'GawgguFyGrWKav7AX4VKUg',
ext: true,
key_ops: ['encrypt', 'decrypt']
}, '19ac2082e1721ab58a6afec05f854a52', 128),

// Generated in Mozilla Firefox.
test({
alg: `A192${jwkAlgorithmSuffix}`,
ext: true,
k: 'bOHt12k8byt-XKHo9kXY_1skBP10eJGC',
key_ops: ['encrypt', 'decrypt'],
kty: 'oct'
}, '6ce1edd7693c6f2b7e5ca1e8f645d8ff5b2404fd74789182', 192)
]);
};
}

describe('AES-CTR', () => {
it('should generate, import and export keys',
testGenImportExport('AES-CTR'));

it('should support JWK import and export',
testJsonWebKeys('AES-CTR', 'CTR'));

it('should encrypt and decrypt', async () => {
const keyData = Buffer.from('36adfe538cc234279e4cbb29e1f27af5', 'hex');
const counter = Buffer.from('2159e5bd415791990e52b5c825572994', 'hex');
Expand Down Expand Up @@ -125,6 +163,9 @@ describe('AES-CBC', () => {
it('should generate, import and export keys',
testGenImportExport('AES-CBC'));

it('should support JWK import and export',
testJsonWebKeys('AES-CBC', 'CBC'));

it('should encrypt and decrypt', async () => {
const keyData = Buffer.from('36adfe538cc234279e4cbb29e1f27af5', 'hex');
const iv = Buffer.from('2159e5bd415791990e52b5c825572994', 'hex');
Expand Down Expand Up @@ -153,6 +194,9 @@ describe('AES-GCM', () => {
it('should generate, import and export keys',
testGenImportExport('AES-GCM'));

it('should support JWK import and export',
testJsonWebKeys('AES-GCM', 'GCM'));

it('should encrypt and decrypt', async () => {
const keyData = Buffer.from('36adfe538cc234279e4cbb29e1f27af5', 'hex');
const iv = Buffer.from('2159e5bd415791990e52b5c825572994', 'hex');
Expand Down Expand Up @@ -231,6 +275,9 @@ describe('AES-KW', () => {
it('should generate, import and export keys',
testGenImportExport('AES-KW'));

it('should support JWK import and export',
testJsonWebKeys('AES-KW', 'KW'));

it('should wrap and unwrap keys', async () => {
const wrappingKey = await subtle.generateKey({
name: 'AES-KW',
Expand Down

0 comments on commit b4ea2d7

Please sign in to comment.