@microsoft/useragent-sdk
Version:
SDK for building decentralized identity wallets and enterprise agents.
265 lines (236 loc) • 11.6 kB
text/typescript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import ISubtleCryptoExtension from './ISubtleCryptoExtension'
import IKeyStore, { CryptoAlgorithm } from '../keyStore/IKeyStore';
import CryptoFactory from './CryptoFactory';
import CryptoHelpers from '../utilities/CryptoHelpers';
import PublicKey from '../keys/PublicKey';
import PrivateKey from '../keys/PrivateKey';
import { KeyType } from '../keys/KeyTypeFactory';
import PairwiseKey from '../keys/PairwiseKey';
import { SubtleCrypto } from 'webcrypto-core';
import CryptoError from '../CryptoError';
import JwsToken from '../protocols/jose/jws/JwsToken';
import IKeyContainer, { CryptographicKey } from '../keys/IKeyContainer';
const clone = require('clone');
// Named curves
const CURVE_P256K = 'P-256K';
const CURVE_K256 = 'K-256';
/**
* The class extends the @class SubtleCrypto with addtional methods.
* Adds methods to work with key references.
* Extends SubtleCrypto to work with JWK keys.
*/
export default class SubtleCryptoExtension extends SubtleCrypto implements ISubtleCryptoExtension {
private keyStore: IKeyStore;
private cryptoFactory: CryptoFactory;
constructor(cryptoFactory: CryptoFactory) {
super();
this.keyStore = cryptoFactory.keyStore;
this.cryptoFactory = cryptoFactory;
}
/**
* Generate a pairwise key for the algorithm
* @param algorithm for the key
* @param seedReference Reference to the seed
* @param personaId Id for the persona
* @param peerId Id for the peer
* @param extractable True if key is exportable
* @param keyops Key operations
*/
public async generatePairwiseKey(algorithm: EcKeyGenParams | RsaHashedKeyGenParams, seedReference: string, personaId: string, peerId: string): Promise<PrivateKey> {
const pairwiseKey = new PairwiseKey(this.cryptoFactory);
return pairwiseKey.generatePairwiseKey(algorithm, seedReference, personaId, peerId);
}
/**
* Sign with a key referenced in the key store
* @param algorithm used for signature
* @param keyReference points to key in the key store
* @param data to sign
* @returns The signature in the requested algorithm
*/
public async signByKeyStore(algorithm: CryptoAlgorithm, keyReference: string, data: BufferSource): Promise<ArrayBuffer> {
const jwk: PrivateKey = (await <Promise<IKeyContainer>>this.keyStore.get(keyReference, false)).getKey<PrivateKey>();
const crypto: SubtleCrypto = CryptoHelpers.getSubtleCryptoForAlgorithm(this.cryptoFactory, algorithm);
const keyImportAlgorithm = SubtleCryptoExtension.normalizeAlgorithm(CryptoHelpers.getKeyImportAlgorithm(algorithm, jwk));
const key = await crypto.importKey('jwk', SubtleCryptoExtension.normalizeJwk(jwk), keyImportAlgorithm, true, ['sign']);
const signature = await <PromiseLike<ArrayBuffer>>crypto.sign(jwk.kty === KeyType.EC || jwk.kty === KeyType.OKP ? <EcdsaParams>SubtleCryptoExtension.normalizeAlgorithm(algorithm): <RsaPssParams>algorithm,
key,
<ArrayBuffer>data);
// only applicable for EC algorithms and when no encoding is applied
const isElliptic = algorithm.name === 'ECDSA' || algorithm.name === 'EDDSA';
// EDDSA/ECDSA returns two 32 bit values R & S. Some API's will encode these values in DER
const format: string = (<any>algorithm).format;
if (isElliptic && signature.byteLength <= 64 && format) {
if (format.toUpperCase() !== 'DER') {
throw new CryptoError(algorithm, 'Only DER format supported for signature');
}
// DER format needed for signature, specied in algorithm
const r = signature.slice(0, signature.byteLength / 2);
const s = signature.slice(signature.byteLength / 2, signature.byteLength)
return SubtleCryptoExtension.toDer([r, s]);
}
if (isElliptic && signature.byteLength > 64 && format) {
// DER encoded is not requested and signature is DER encoded
// In this case the encoding is removed and returned as 64 bytes
const decodedSignature = SubtleCryptoExtension.fromDer(new Uint8Array(signature));
const signed = new Uint8Array(decodedSignature[0].length + decodedSignature[1].length);
signed.set(decodedSignature[0]);
signed.set(decodedSignature[1], decodedSignature[1].length);
return signed;
}
return signature;
}
/**
* format the signature output to DER format
* @param elements Array of elements to encode in DER
*/
private static toDer(elements: ArrayBuffer[]): ArrayBuffer {
let index: number = 0;
// calculate total size.
let lengthOfRemaining = 0;
for (let element = 0 ; element < elements.length; element++) {
// Add element format bytes
lengthOfRemaining += 2;
const buffer = new Uint8Array(elements[element]);
const size = (buffer[0] & 0x80) === 0x80 ? buffer.length + 1 : buffer.length;
lengthOfRemaining += size;
}
// Prepare output
index = 0;
const result = new Uint8Array(lengthOfRemaining + 2);
result.set([0x30, lengthOfRemaining], index);
index += 2;
for (let element = 0 ; element < elements.length; element++) {
// Add element format bytes
const buffer = new Uint8Array(elements[element]);
const size = (buffer[0] & 0x80) === 0x80 ? buffer.length + 1 : buffer.length;
result.set([0x02, size], index);
index += 2;
if (size > buffer.length) {
result.set([0x0], index++);
}
result.set(buffer, index);
index += buffer.length;
}
return result;
}
/**
* Verify with JWK.
* @param algorithm used for verification
* @param jwk Json web key used to verify
* @param signature to verify
* @param payload which was signed
*/
public async verifyByJwk(algorithm: CryptoAlgorithm, jwk: JsonWebKey, signature: BufferSource, payload: BufferSource): Promise<boolean> {
const crypto: SubtleCrypto = CryptoHelpers.getSubtleCryptoForAlgorithm(this.cryptoFactory, algorithm);
const keyImportAlgorithm = SubtleCryptoExtension.normalizeAlgorithm(CryptoHelpers.getKeyImportAlgorithm(algorithm, jwk));
const key = await crypto.importKey('jwk', SubtleCryptoExtension.normalizeJwk(jwk), keyImportAlgorithm, true, ['verify']);
const isElliptic = algorithm.name === 'ECDSA' || algorithm.name === 'EDDSA';
// The underlying signature validation does not support DER encoding so needs to be removed
if (isElliptic && signature.byteLength > 64) {
const elements = SubtleCryptoExtension.fromDer(<Uint8Array>signature);
signature = new Uint8Array(elements[0].length + elements[1].length);
(<Uint8Array>signature).set(elements[0]);
(<Uint8Array>signature).set(elements[1], elements[1].length);
}
return crypto.verify(isElliptic?
<EcdsaParams>SubtleCryptoExtension.normalizeAlgorithm(algorithm):
<RsaPssParams>algorithm, key, <ArrayBuffer>signature, <ArrayBuffer>payload);
}
/**
* format the signature output from DER format
* @param signature to decode from DER
*/
private static fromDer(signature: Uint8Array): Uint8Array[] {
if (signature[0] !== 0x30) {
throw new Error('No DER format to decode');
}
const lengthOfRemaining = signature[1];
const results: Uint8Array[] = [];
let index: number = 2;
while (index < lengthOfRemaining) {
const marker = signature[index++];
if (marker !== 0x02) {
throw new Error(`Marker on index ${index-1} must be 0x02`);
}
let length = signature[index++];
while (signature[index] === 0) {
index ++;
length --;
}
const data = signature.slice(index, index + length);
results.push(data);
index = index + length;
}
return results;
}
/**
* Decrypt with a key referenced in the key store.
* The referenced key must be a jwk key.
* @param algorithm used for signature
* @param keyReference points to key in the key store
* @param cipher to decrypt
*/
public async decryptByKeyStore(algorithm: CryptoAlgorithm, keyReference: string, cipher: BufferSource): Promise<ArrayBuffer> {
const jwk: PrivateKey = (await this.keyStore.get(keyReference, false)).getKey<PrivateKey>();
const crypto: SubtleCrypto = CryptoHelpers.getSubtleCryptoForAlgorithm(this.cryptoFactory, algorithm);
const keyImportAlgorithm = SubtleCryptoExtension.normalizeAlgorithm(CryptoHelpers.getKeyImportAlgorithm(algorithm, jwk));
const key = await crypto.importKey('jwk', SubtleCryptoExtension.normalizeJwk(jwk), SubtleCryptoExtension.normalizeAlgorithm(keyImportAlgorithm), true, ['decrypt']);
return crypto.decrypt(algorithm, key, <ArrayBuffer>cipher);
}
/**
* Decrypt with JWK.
* @param algorithm used for decryption
* @param jwk Json web key to decrypt
* @param cipher to decrypt
*/
public async decryptByJwk(algorithm: CryptoAlgorithm, jwk: JsonWebKey, cipher: BufferSource): Promise<ArrayBuffer> {
const crypto: SubtleCrypto = CryptoHelpers.getSubtleCryptoForAlgorithm(this.cryptoFactory, algorithm);
const keyImportAlgorithm = SubtleCryptoExtension.normalizeAlgorithm(CryptoHelpers.getKeyImportAlgorithm(algorithm, jwk));
const key = await crypto.importKey('jwk', SubtleCryptoExtension.normalizeJwk(jwk), SubtleCryptoExtension.normalizeAlgorithm(keyImportAlgorithm), true, ['decrypt']);
return crypto.decrypt(algorithm, key, <ArrayBuffer>cipher);
}
/**
* Encrypt with a jwk key referenced in the key store
* @param algorithm used for encryption
* @param jwk Json web key public key
* @param data to encrypt
*/
public async encryptByJwk(algorithm: CryptoAlgorithm, jwk: PublicKey | JsonWebKey, data: BufferSource): Promise<ArrayBuffer> {
const keyImportAlgorithm = CryptoHelpers.getKeyImportAlgorithm(algorithm, jwk);
const crypto: SubtleCrypto = CryptoHelpers.getSubtleCryptoForAlgorithm(this.cryptoFactory, algorithm);
const key = await crypto.importKey('jwk', SubtleCryptoExtension.normalizeJwk(jwk), SubtleCryptoExtension.normalizeAlgorithm(keyImportAlgorithm), true, ['encrypt']);
return <PromiseLike<ArrayBuffer>>crypto.encrypt(algorithm, key, <ArrayBuffer>data);
}
/**
* Normalize the algorithm so it can be used by underlying crypto.
* @param algorithm Algorithm to be normalized
*/
public static normalizeAlgorithm (algorithm: any) {
if (algorithm.namedCurve) {
if (algorithm.namedCurve === CURVE_P256K) {
const alg = clone(algorithm);
alg.namedCurve = CURVE_K256;
return alg;
}
}
return algorithm;
}
/**
* Normalize the JWK parameters so it can be used by underlying crypto.
* @param jwk Json web key to be normalized
*/
public static normalizeJwk (jwk: any) {
if (jwk.crv) {
if (jwk.crv === CURVE_P256K) {
const clonedKey = clone(jwk);
clonedKey.crv = CURVE_K256;
return clonedKey;
}
}
return jwk;
}
}