UNPKG

@dwn-protocol/id-sdk

Version:

SDK for accessing the features and capabilities

412 lines (338 loc) 15.2 kB
import type { IDCrypto } from '../crypto/index.js'; import type { RequireOnly } from '../common/index.js'; import * as cryptoUtils from '../crypto/utils.js'; import { EcdhAlgorithm, EcdsaAlgorithm, EdDsaAlgorithm, AesCtrAlgorithm, CryptoAlgorithm, } from '../crypto/index.js'; import { ManagedKey, PortableKey, SignOptions, VerifyOptions, DecryptOptions, EncryptOptions, ManagedKeyPair, ManagedKeyStore, GenerateKeyType, PortableKeyPair, ImportKeyOptions, UpdateKeyOptions, DeriveBitsOptions, ManagedPrivateKey, GenerateKeyOptions, KeyManagementSystem, GenerateKeyOptionTypes, } from './types/managed-key.js'; import { isManagedKey, isManagedKeyPair } from './utils.js'; import { KeyStoreMemory, PrivateKeyStoreMemory } from './store-managed-key.js'; import { IDManagedAgent } from './types/agent.js'; export type AlgorithmImplementation = typeof CryptoAlgorithm & { new(): CryptoAlgorithm; }; export type AlgorithmImplementations = { [algorithmName: string]: AlgorithmImplementation; }; export type KmsOptions = { agent?: IDManagedAgent; cryptoAlgorithms?: AlgorithmImplementations; keyStore?: ManagedKeyStore<string, ManagedKey | ManagedKeyPair>; kmsName: string; privateKeyStore?: ManagedKeyStore<string, ManagedPrivateKey>; } // Map key operations to algorithm specs to implementations. export const defaultAlgorithms: AlgorithmImplementations = { 'AES-CTR' : AesCtrAlgorithm, ECDH : EcdhAlgorithm, ECDSA : EcdsaAlgorithm, EdDSA : EdDsaAlgorithm, }; export class LocalKms implements KeyManagementSystem { /** * Holds the instance of a `IDManagedAgent` that represents the current * execution context for the `KeyManager`. This agent is utilized * to interact with other agent components. It's vital * to ensure this instance is set to correctly contextualize * operations within the broader agent framework. */ private _agent?: IDManagedAgent; private _name: string; private _keyStore: ManagedKeyStore<string, ManagedKey | ManagedKeyPair>; private _privateKeyStore: ManagedKeyStore<string, ManagedPrivateKey>; private _supportedAlgorithms: Map<string, AlgorithmImplementation> = new Map(); constructor(options: KmsOptions) { const { agent, kmsName, keyStore, privateKeyStore } = options; this._agent = agent; this._name = kmsName; this._keyStore = keyStore ?? new KeyStoreMemory(); this._privateKeyStore = privateKeyStore ?? new PrivateKeyStoreMemory(); // Merge the default and custom algorithms and register with the KMS. const cryptoAlgorithms = {...defaultAlgorithms, ...options.cryptoAlgorithms}; this.registerSupportedAlgorithms(cryptoAlgorithms); } /** * Retrieves the `IDManagedAgent` execution context. * If the `agent` instance proprety is undefined, it will throw an error. * * @returns The `IDManagedAgent` instance that represents the current execution * context. * * @throws Will throw an error if the `agent` instance property is undefined. */ get agent(): IDManagedAgent { if (this._agent === undefined) { throw new Error('KeyManager: Unable to determine agent execution context.'); } return this._agent; } set agent(agent: IDManagedAgent) { this._agent = agent; } async decrypt(options: DecryptOptions): Promise<Uint8Array> { const { algorithm, data, keyRef } = options; // Retrieve the ManagedKey from the KMS key metadata store. const key = await this.getKey({ keyRef }); if (isManagedKey(key)) { const privateManagedKey = await this._privateKeyStore.getKey({ id : key.id, agent : this.agent }); if (privateManagedKey !== undefined) { // Construct a CryptoKey object from the key metadata and private key material. const privateCryptoKey = this.toCryptoKey({ ...key, material: privateManagedKey.material }); // Decrypt the data. const cryptoAlgorithm = this.getAlgorithm(algorithm); const plaintext = cryptoAlgorithm.decrypt({ algorithm, key: privateCryptoKey, data }); return plaintext; } } throw new Error(`Operation failed: 'decrypt'. Key not found: ${keyRef}`); } async deriveBits(options: DeriveBitsOptions): Promise<Uint8Array> { let { algorithm, baseKeyRef, length } = options; // Retrieve the ManagedKeyPair from the KMS key metadata store. const ownKeyPair = await this.getKey({ keyRef: baseKeyRef }); if (isManagedKeyPair(ownKeyPair)) { const privateManagedKey = await this._privateKeyStore.getKey({ id : ownKeyPair.privateKey.id, agent : this.agent }); if (privateManagedKey !== undefined) { // Construct a CryptoKey object from the key metadata and private key material. const privateCryptoKey = this.toCryptoKey({ ...ownKeyPair.privateKey, material: privateManagedKey.material }); // Derive the shared secret. const cryptoAlgorithm = this.getAlgorithm(algorithm); const sharedSecret = cryptoAlgorithm.deriveBits({ algorithm, baseKey: privateCryptoKey, length: length ?? null }); return sharedSecret; } } throw new Error(`Operation failed: 'deriveBits'. Key not found: ${baseKeyRef}`); } async encrypt(options: EncryptOptions): Promise<Uint8Array> { const { algorithm, data, keyRef } = options; // Retrieve the ManagedKey from the KMS key metadata store. const key = await this.getKey({ keyRef }); if (isManagedKey(key)) { const privateManagedKey = await this._privateKeyStore.getKey({ id : key.id, agent : this.agent }); if (privateManagedKey !== undefined) { // Construct a CryptoKey object from the key metadata and private key material. const privateCryptoKey = this.toCryptoKey({ ...key, material: privateManagedKey.material }); // Encrypt the data. const cryptoAlgorithm = this.getAlgorithm(algorithm); const ciphertext = cryptoAlgorithm.encrypt({ algorithm, key: privateCryptoKey, data }); return ciphertext; } } throw new Error(`Operation failed: 'encrypt'. Key not found: ${keyRef}`); } async generateKey<T extends GenerateKeyOptionTypes>(options: GenerateKeyOptions<T>): Promise<GenerateKeyType<T>> { let { algorithm, alias, extractable, keyUsages, metadata } = options; // Get crypto algorithm implementation. const cryptoAlgorithm = this.getAlgorithm(algorithm); // Generate the key. extractable ??= true; // Default to extractable if not specified. const cryptoKey: any = await cryptoAlgorithm.generateKey({ algorithm, extractable, keyUsages }); // Create a ManagedKey or ManagedKeyPair using the generated key and store the private key material. let managedKeyOrKeyPair: GenerateKeyType<T>; if (cryptoUtils.isCryptoKeyPair(cryptoKey)) { const privateKeyType = cryptoKey.privateKey.type as IDCrypto.PrivateKeyType; const id = await this._privateKeyStore.importKey({ key : { material: cryptoKey.privateKey.material, type: privateKeyType}, agent : this.agent }); const managedKeyPair: ManagedKeyPair = { privateKey : this.toManagedKey({ ...cryptoKey.privateKey, id, alias, metadata }), publicKey : this.toManagedKey({ ...cryptoKey.publicKey, material: cryptoKey.publicKey.material, id, alias, metadata }) }; managedKeyOrKeyPair = managedKeyPair as GenerateKeyType<T>; } else { const keyType = cryptoKey.type as IDCrypto.PrivateKeyType; const id = await this._privateKeyStore.importKey({ key : { material: cryptoKey.material, type: keyType }, agent : this.agent }); managedKeyOrKeyPair = this.toManagedKey({ ...cryptoKey, id, alias, metadata }) as GenerateKeyType<T>; } // Store the ManagedKey or ManagedKeyPair in the KMS key store. await this._keyStore.importKey({ key: managedKeyOrKeyPair, agent: this.agent }); return managedKeyOrKeyPair; } async getKey(options: { keyRef: string }): Promise<ManagedKey | ManagedKeyPair | undefined> { const keyOrKeyPair = this._keyStore.getKey({ id: options.keyRef, agent: this.agent }); return keyOrKeyPair; } async importKey(options: PortableKeyPair): Promise<ManagedKeyPair>; async importKey(options: PortableKey): Promise<ManagedKey>; async importKey(options: ImportKeyOptions): Promise<ManagedKey | ManagedKeyPair> { if ('privateKey' in options) { // Asymmetric key pair import. const { privateKey, publicKey } = options; if (privateKey.type === 'public' && publicKey.type === 'private') throw new Error(`Import failed due to private and public key mismatch`); if (!(privateKey.type === 'private' && publicKey.type === 'public')) throw new TypeError(`Out of range: '${privateKey.type}, ${publicKey.type}'. Must be 'private, public'`); const id = await this._privateKeyStore.importKey({ key : { material: privateKey.material, type: privateKey.type }, agent : this.agent }); const managedKeyPair = { privateKey : this.toManagedKey({ ...privateKey, id, material: undefined }), publicKey : this.toManagedKey({ ...publicKey, material: publicKey.material, id }) }; await this._keyStore.importKey({ key: managedKeyPair, agent: this.agent }); return managedKeyPair; } const keyType = options.type; switch (keyType) { case 'private': { // Asymmetric private key import. const material = options.material; const id = await this._privateKeyStore.importKey({ key : { material, type: keyType }, agent : this.agent }); const privateManagedKey = this.toManagedKey({ ...options, material: undefined, id }); await this._keyStore.importKey({ key: privateManagedKey, agent: this.agent }); return privateManagedKey; } case 'public': { // Asymmetric public key import. const material = options.material; const publicManagedKey = this.toManagedKey({ ...options, material, id: '' }); publicManagedKey.id = await this._keyStore.importKey({ key: publicManagedKey, agent: this.agent }); return publicManagedKey; } case 'secret': { // Symmetric secret key import. const material = options.material; const id = await this._privateKeyStore.importKey({ key : { material, type: keyType }, agent : this.agent }); const secretManagedKey = this.toManagedKey({ ...options, material: undefined, id }); await this._keyStore.importKey({ key: secretManagedKey, agent: this.agent }); return secretManagedKey; } default: throw new TypeError(`Out of range: '${keyType}'. Must be one of 'private, public, secret'`); } } async sign(options: SignOptions): Promise<Uint8Array> { const { algorithm, data, keyRef } = options; // Retrieve the ManagedKeyPair from the KMS key metadata store. const keyPair = await this.getKey({ keyRef }); if (isManagedKeyPair(keyPair)) { const privateManagedKey = await this._privateKeyStore.getKey({ id : keyPair.privateKey.id, agent : this.agent }); if (privateManagedKey !== undefined) { // Construct a CryptoKey object from the key metadata and private key material. const privateCryptoKey = this.toCryptoKey({ ...keyPair.privateKey, material: privateManagedKey.material }); // Sign the data. const cryptoAlgorithm = this.getAlgorithm(algorithm); const signature = cryptoAlgorithm.sign({ algorithm, key: privateCryptoKey, data }); return signature; } } throw new Error(`Operation failed: 'sign'. Key not found: ${keyRef}`); } async updateKey(options: UpdateKeyOptions): Promise<boolean> { const { keyRef, alias, metadata } = options; const keyOrKeyPair = await this.getKey({ keyRef }); if (!keyOrKeyPair) { throw new Error(`Key not found: '${keyRef}'`); } const keyId = (isManagedKeyPair(keyOrKeyPair)) ? keyOrKeyPair.publicKey.id : keyOrKeyPair.id; // Update the KMS key metadata store. return this._keyStore.updateKey({ id: keyId, alias, metadata, agent: this.agent }); } async verify(options: VerifyOptions): Promise<boolean> { const { algorithm, data, keyRef, signature } = options; // Retrieve the ManagedKeyPair from the KMS key metadata store. const keyPair = await this.getKey({ keyRef }); if (isManagedKeyPair(keyPair)) { if (keyPair.publicKey.material === undefined) { throw new Error(`Required property missing: 'material'`); } // Construct a CryptoKey object from the key metadata and private key material. const publicCryptoKey = this.toCryptoKey({ ...keyPair.publicKey, material: keyPair.publicKey.material }); // Verify the signature and data. const cryptoAlgorithm = this.getAlgorithm(algorithm); const isValid = cryptoAlgorithm.verify({ algorithm, key: publicCryptoKey, signature, data }); return isValid; } throw new Error(`Operation failed: 'verify'. Key not found: ${keyRef}`); } private getAlgorithm(algorithmIdentifier: IDCrypto.AlgorithmIdentifier): CryptoAlgorithm { cryptoUtils.checkRequiredProperty({ property: 'name', inObject: algorithmIdentifier }); const algorithm = this._supportedAlgorithms.get(algorithmIdentifier.name.toUpperCase()); if (algorithm === undefined) { throw new Error(`The algorithm '${algorithmIdentifier.name}' is not supported`); } return algorithm.create(); } private registerSupportedAlgorithms(cryptoAlgorithms: AlgorithmImplementations): void { for (const [name, implementation] of Object.entries(cryptoAlgorithms)) { // Add the algorithm name and its implementation to the supported algorithms map, // upper-cased to allow for case-insensitive. this._supportedAlgorithms.set(name.toUpperCase(), implementation); } } private toCryptoKey(managedKey: RequireOnly<ManagedKey, 'algorithm' | 'extractable' | 'material' | 'type' | 'usages'> ): IDCrypto.CryptoKey { const cryptoKey: IDCrypto.CryptoKey = { algorithm : managedKey.algorithm, extractable : managedKey.extractable, material : managedKey.material, type : managedKey.type, usages : managedKey.usages }; return cryptoKey; } private toManagedKey(options: Omit<IDCrypto.CryptoKey, 'material'> & RequireOnly<ManagedKey, 'id'>): ManagedKey { const managedKey: ManagedKey = { id : options.id, algorithm : options.algorithm, alias : options.alias, extractable : options.extractable, kms : this._name, material : (options.type === 'public') ? options.material : undefined, metadata : options.metadata, state : 'Enabled', type : options.type, usages : options.usages }; return managedKey; } }