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

Add JWK for RSA #42

Merged
merged 1 commit into from
Mar 13, 2020
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
89 changes: 14 additions & 75 deletions lib/algorithms/ecdsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,94 +14,33 @@ const {
requireDOMString
} = require('../idl');
const { kKeyMaterial, CryptoKey } = require('../key');
const { limitUsages, opensslHashFunctionName } = require('../util');
const {
limitUsages,
opensslHashFunctionName,
Asn1SequenceDecoder,
Asn1SequenceEncoder
} = require('../util');

const generateKeyPair = promisify(crypto.generateKeyPair);

const byte = (b) => Buffer.from([b]);

function convertSignatureToASN1(signature, n) {
if (signature.length !== 2 * n)
return undefined;

const r = signature.slice(0, n);
const s = signature.slice(n);

function encodeLength(len) {
// Short form.
if (len < 128)
return byte(len);

// Long form.
const buffer = Buffer.alloc(5);
buffer.writeUInt32BE(len, 1);
let offset = 1;
while (buffer[offset] === 0)
offset++;
buffer[offset - 1] = 0x80 | (5 - offset);
return buffer.slice(offset - 1);
}

function encodeUnsignedInteger(integer) {
// ASN.1 integers are signed, so in order to encode unsigned integers, we
// need to make sure that the MSB is not set.
if (integer[0] & 0x80) {
return Buffer.concat([
byte(0x02),
encodeLength(integer.length + 1),
byte(0x00), integer
]);
} else {
// If the MSB is not set, enforce a minimal representation of the integer.
let i = 0;
while (integer[i] === 0 && (integer[i + 1] & 0x80) === 0)
i++;
return Buffer.concat([
byte(0x02),
encodeLength(integer.length - i),
integer.slice(i)
]);
}
}

const seq = Buffer.concat([
encodeUnsignedInteger(r),
encodeUnsignedInteger(s)
]);

return Buffer.concat([byte(0x30), encodeLength(seq.length), seq]);
const enc = new Asn1SequenceEncoder();
enc.unsignedInteger(r);
enc.unsignedInteger(s);
return enc.end();
}

function convertSignatureFromASN1(signature, n) {
let offset = 2;
if (signature[1] & 0x80)
offset += signature[1] & ~0x80;

function decodeUnsignedInteger() {
let length = signature[offset + 1];
offset += 2;
if (length & 0x80) {
// Long form.
const nBytes = length & ~0x80;
length = 0;
for (let i = 0; i < nBytes; i++)
length = (length << 8) | signature[offset + 2 + i];
offset += nBytes;
}

// There may be exactly one leading zero (if the next byte's MSB is set).
if (signature[offset] === 0) {
offset++;
length--;
}

const result = signature.slice(offset, offset + length);
offset += length;
return result;
}

const r = decodeUnsignedInteger();
const s = decodeUnsignedInteger();
const dec = new Asn1SequenceDecoder(signature);
const r = dec.unsignedInteger();
const s = dec.unsignedInteger();
dec.end();

const result = Buffer.alloc(2 * n, 0);
r.copy(result, n - r.length);
Expand Down
144 changes: 133 additions & 11 deletions lib/algorithms/rsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { promisify } = require('util');

const algorithms = require('../algorithms');
const {
DataError,
InvalidAccessError,
NotSupportedError,
OperationError
Expand All @@ -14,7 +15,14 @@ const {
toUnsignedLongEnforceRange,
bufferFromBufferSource
} = require('../idl');
const { limitUsages, opensslHashFunctionName } = require('../util');
const {
decodeBase64Url,
encodeBase64Url,
limitUsages,
opensslHashFunctionName,
Asn1SequenceDecoder,
Asn1SequenceEncoder
} = require('../util');

const generateKeyPair = promisify(crypto.generateKeyPair);

Expand All @@ -38,6 +46,89 @@ function uint32ToUint8Array(integer) {
return result.slice(i + 1);
}

function jwkToDer(jwk, jwkHashMapping) {
if (jwk.kty !== 'RSA')
throw new DataError();

if (jwk.use !== undefined && jwk.use !== 'sig')
throw new DataError();

let hash;
if (jwk.alg !== undefined) {
hash = jwkHashMapping[jwk.alg];
if (hash === undefined)
throw new DataError();
}

const enc = new Asn1SequenceEncoder();

if (jwk.d === undefined) {
enc.unsignedInteger(decodeBase64Url(jwk.n));
enc.unsignedInteger(decodeBase64Url(jwk.e));
} else {
enc.unsignedInteger(Buffer.from([0]));
enc.unsignedInteger(decodeBase64Url(jwk.n));
enc.unsignedInteger(decodeBase64Url(jwk.e));
enc.unsignedInteger(decodeBase64Url(jwk.d));
enc.unsignedInteger(decodeBase64Url(jwk.p));
enc.unsignedInteger(decodeBase64Url(jwk.q));
enc.unsignedInteger(decodeBase64Url(jwk.dp));
enc.unsignedInteger(decodeBase64Url(jwk.dq));
enc.unsignedInteger(decodeBase64Url(jwk.qi));
}

return enc.end();
}

const kInverted = Symbol('kInverted');
function swapKeysAndValues(obj) {
if (obj[kInverted])
return obj[kInverted];
const entries = Object.entries(obj).map(([k, v]) => [v, k]);
return obj[kInverted] = Object.fromEntries(entries);
}

function derToJwk(cryptoKey, jwkHashMapping) {
const der = cryptoKey[kKeyMaterial].export({
format: 'der',
type: 'pkcs1'
});

const dec = new Asn1SequenceDecoder(der);

if (cryptoKey.type === 'private') {
dec.unsignedInteger(); // TODO: Don't ignore this
}

const n = encodeBase64Url(dec.unsignedInteger());
const e = encodeBase64Url(dec.unsignedInteger());
let keyProps;

if (cryptoKey.type === 'public') {
keyProps = { n, e };
} else {
const d = encodeBase64Url(dec.unsignedInteger());
const p = encodeBase64Url(dec.unsignedInteger());
const q = encodeBase64Url(dec.unsignedInteger());
const dp = encodeBase64Url(dec.unsignedInteger());
const dq = encodeBase64Url(dec.unsignedInteger());
const qi = encodeBase64Url(dec.unsignedInteger());
keyProps = { n, e, d, p, q, dp, dq, qi };
}

dec.end();

const alg = swapKeysAndValues(jwkHashMapping)[cryptoKey.algorithm.hash.name];

return {
kty: 'RSA',
alg,
key_ops: [...cryptoKey.usages],
ext: cryptoKey.extractable,
...keyProps
};
}

const rsaBase = {
async generateKey(algorithm, extractable, usages) {
let privateUsages, publicUsages;
Expand Down Expand Up @@ -91,28 +182,38 @@ const rsaBase = {
name: algorithms.getAlgorithm(hashAlg, 'digest').name
};

const opts = {
key: bufferFromBufferSource(keyData),
format: 'der',
type: keyFormat
};
const isPublic = keyFormat === 'spki' ||
(keyFormat === 'jwk' && keyData.d === undefined);

let key;
if (keyFormat === 'spki') {
if (isPublic)
limitUsages(keyUsages, this.sign ? ['verify'] : ['encrypt', 'wrapKey']);
key = crypto.createPublicKey(opts);
} else if (keyFormat === 'pkcs8') {
else
limitUsages(keyUsages, this.sign ? ['sign'] : ['decrypt', 'unwrapKey']);
key = crypto.createPrivateKey(opts);

if (keyFormat === 'spki' || keyFormat === 'pkcs8') {
keyData = bufferFromBufferSource(keyData);
} else if (keyFormat === 'jwk') {
keyData = jwkToDer(keyData, this.jwkHashMapping);
keyFormat = 'pkcs1';
} else {
throw new NotSupportedError();
}

const key = (isPublic ? crypto.createPublicKey : crypto.createPrivateKey)({
key: keyData,
format: 'der',
type: keyFormat
});

return new CryptoKey(key.type, { name: this.name, hash },
extractable, keyUsages, key);
},

exportKey(format, key) {
if (format === 'jwk') {
return derToJwk(key, this.jwkHashMapping);
}

if (format !== 'spki' && format !== 'pkcs8')
throw new NotSupportedError();

Expand All @@ -132,6 +233,13 @@ module.exports.RSASSA_PKCS1 = {
name: 'RSASSA-PKCS1-v1_5',
...rsaBase,

jwkHashMapping: {
RS1: 'SHA-1',
RS256: 'SHA-256',
RS384: 'SHA-384',
RS512: 'SHA-512'
},

sign(algorithm, key, data) {
const hashFn = opensslHashFunctionName(key.algorithm.hash);
return crypto.sign(hashFn, bufferFromBufferSource(data), key[kKeyMaterial]);
Expand All @@ -149,6 +257,13 @@ module.exports.RSA_PSS = {
name: 'RSA-PSS',
...rsaBase,

jwkHashMapping: {
PS1: 'SHA-1',
PS256: 'SHA-256',
PS384: 'SHA-384',
PS512: 'SHA-512'
},

sign(algorithm, key, data) {
let { saltLength } = algorithm;
saltLength = toUnsignedLongEnforceRange(saltLength);
Expand Down Expand Up @@ -179,6 +294,13 @@ module.exports.RSA_OAEP = {
name: 'RSA-OAEP',
...rsaBase,

jwkHashMapping: {
'RSA-OAEP': 'SHA-1',
'RSA-OAEP-256': 'SHA-256',
'RSA-OAEP-384': 'SHA-384',
'RSA-OAEP-512': 'SHA-512'
},

encrypt(params, key, data) {
try {
return crypto.publicEncrypt({
Expand Down
Loading