@dwn-protocol/id-sdk
Version:
SDK for accessing the features and capabilities
412 lines (338 loc) • 15.2 kB
text/typescript
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;
}
}