UNPKG

@dwn-protocol/id-sdk

Version:

SDK for accessing the features and capabilities

738 lines (596 loc) 24.9 kB
import type { RecordsWriteMessage, RecordsWriteOptions } from '@dwn-protocol/id'; import * as cryptoUtils from '../crypto/utils.js'; import { Convert, removeEmptyObjects, removeUndefinedProperties } from '../common/index.js'; import type { ManagedKey, ManagedKeyPair, ManagedKeyStore, ManagedPrivateKey } from './types/managed-key.js'; import { DwnResponse, IDManagedAgent } from './types/agent.js'; import { isManagedKeyPair } from './utils.js'; type EncodedPrivateKey = Omit<ManagedPrivateKey, 'material'> & { // Key material, encoded as Base64Url. material: string; } type EncodedKey = Omit<ManagedKey, 'material'> & { // Key material, encoded as Base64Url. material?: string; } type EncodedKeyPair = { privateKey: EncodedKey; publicKey: EncodedKey; } /** * An implementation of `ManagedKeyStore` that stores key metadata and * public key material in a DWN. * * An instance of this class can be used by `KeyManager` or * an implementation of `KeyManagementSystem`. */ export class KeyStoreDwn implements ManagedKeyStore<string, ManagedKey | ManagedKeyPair> { private _keyRecordProperties = { dataFormat : 'application/json', schema : 'https://identity.foundation/schemas/dwn/managed-key' }; constructor(options?: { schema: string }) { const { schema } = options ?? {}; if (schema) { this._keyRecordProperties.schema = schema; } } async deleteKey(options: { agent: IDManagedAgent, context?: string, id: string }): Promise<boolean> { const { agent, context, id } = options; // Determine which DID to use to author DWN messages. const authorDid = await this.getAuthor({ agent, context }); // Query the DWN for all stored key objects. const { reply: queryReply} = await this.getKeyRecords(agent, context); // Loop through all of the entries and try to find a match. let matchingRecordId: string | undefined; for (const record of queryReply.entries ?? []) { if (record.encodedData) { const storedKey = this.decodeKey(record.encodedData); const storedKeyId = isManagedKeyPair(storedKey) ? storedKey.publicKey.id : storedKey.id; if (storedKey && storedKeyId === id) { matchingRecordId = (record as RecordsWriteMessage).recordId ; break; } } } // Return undefined if the specified key was not found in the store. if (!matchingRecordId) return false; // If a record for the specified key was found, attempt to delete it. const { reply: { status } } = await agent.dwnManager.processRequest({ author : authorDid, target : authorDid, messageType : 'RecordsDelete', messageOptions : { recordId: matchingRecordId } }); // If the key was successfully deleted, return true; if (status.code === 202) return true; // If the key could not be deleted, return false; return false; } async findKey(options: { id: string, agent: IDManagedAgent, context?: string }): Promise<ManagedKey | ManagedKeyPair | undefined>; async findKey(options: { alias: string, agent: IDManagedAgent, context?: string }): Promise<ManagedKey | ManagedKeyPair | undefined>; async findKey(options: { agent: IDManagedAgent, alias?: string, context?: string, id?: string }): Promise<ManagedKey | ManagedKeyPair | undefined> { const { agent, alias, context, id } = options; // Query the DWN for all stored managed key objects. const { reply: queryReply} = await this.getKeyRecords(agent, context); // Loop through all of the entries and return a match, if found. for (const record of queryReply.entries ?? []) { if (record.encodedData) { const storedKey = this.decodeKey(record.encodedData); if (isManagedKeyPair(storedKey)) { if (storedKey.publicKey.id === id) return storedKey; if (storedKey.publicKey.alias === alias) return storedKey; } else { if (storedKey.id === id) return storedKey; if (storedKey.alias === alias) return storedKey; } } } // Return undefined if no matches were found. return undefined; } async getKey(options: { agent: IDManagedAgent, context?: string, id: string }): Promise<ManagedKey | ManagedKeyPair | undefined> { const { agent, context, id } = options; // Query the DWN for all stored managed key objects. const { reply: queryReply} = await this.getKeyRecords(agent, context); // Loop through all of the entries and return a match, if found. for (const record of queryReply.entries ?? []) { if (record.encodedData) { const storedKey = this.decodeKey(record.encodedData); const storedKeyId = isManagedKeyPair(storedKey) ? storedKey.publicKey.id : storedKey.id; if (storedKeyId === id) return storedKey; } } // Return undefined if no matches were found. return undefined; } async importKey(options: { agent: IDManagedAgent, context?: string, key: ManagedKey | ManagedKeyPair }): Promise<string> { const { agent, context, key } = options; let keyId: string; if (isManagedKeyPair(key)) { keyId = key.publicKey.id; } else { // If an ID wasn't specified, generate one. if (!key.id) { key.id = cryptoUtils.randomUuid(); } keyId = key.id; } // Determine which DID to use to author DWN messages. const authorDid = await this.getAuthor({ agent, context }); // Check if the key being imported is already present in the store. const duplicateFound = await this.getKey({ agent, context, id: keyId }); if (duplicateFound) { throw new Error(`KeyStoreDwn: Key with ID already exists: '${keyId}'`); } // Encode the managed key or key pair as bytes. const encodedKey = this.encodeKey(key); const { reply: { status } } = await agent.dwnManager.processRequest({ author : authorDid, target : authorDid, messageType : 'RecordsWrite', messageOptions : { ...this._keyRecordProperties }, dataStream : new Blob([encodedKey]) }); // If the write fails, throw an error. if (status.code !== 202) { throw new Error('DidStoreDwn: Failed to write imported DID to store.'); } return keyId; } async listKeys(options: { agent: IDManagedAgent, context?: string }): Promise<(ManagedKey | ManagedKeyPair)[]> { const { agent, context } = options; // Query the DWN for all stored managed key objects. const { reply: queryReply} = await this.getKeyRecords(agent, context); // Loop through all of the entries and accumulate the key objects. let storedKeys: (ManagedKey | ManagedKeyPair)[] = []; for (const record of queryReply.entries ?? []) { if (record.encodedData) { const storedKey = this.decodeKey(record.encodedData); storedKeys.push(storedKey); } } return storedKeys; } async updateKey(options: { agent: IDManagedAgent, context?: string } & Pick<ManagedKey, 'id' | 'alias' | 'metadata'>): Promise<boolean> { const { agent, context, id } = options; const propertyUpdates = { alias: options.alias, metadata: options.metadata }; // Determine which DID to use to author DWN messages. const authorDid = await this.getAuthor({ agent, context }); // Query the DWN for all stored managed key objects. const { reply: queryReply} = await this.getKeyRecords(agent, context); // Confirm the key being updated is already present in the store. let keyToUpdate: ManagedKey | ManagedKeyPair | undefined; let recordToUpdate: RecordsWriteMessage | undefined; for (const entry of queryReply.entries ?? []) { const { encodedData, ...record } = entry; if (encodedData) { const storedKey = this.decodeKey(encodedData); const storedKeyId = isManagedKeyPair(storedKey) ? storedKey.publicKey.id : storedKey.id; if (storedKey && storedKeyId === id) { keyToUpdate = storedKey; recordToUpdate = record as RecordsWriteMessage ; break; } } } // Key with given ID not present so update operation cannot proceed. if (!recordToUpdate || !keyToUpdate) return false; // Make a deep copy of the update properties to ensure all nested objects do not share references. removeUndefinedProperties(propertyUpdates); removeEmptyObjects(propertyUpdates); const clonedUpdates = structuredClone(propertyUpdates); // Update the given properties of the key. if (isManagedKeyPair(keyToUpdate)) { keyToUpdate.privateKey = { ...keyToUpdate.privateKey, ...clonedUpdates }; keyToUpdate.publicKey = { ...keyToUpdate.publicKey, ...clonedUpdates }; } else { keyToUpdate = { ...keyToUpdate, ...clonedUpdates }; } // Encode the updated key or key pair as bytes. const updatedKeyBytes = this.encodeKey(keyToUpdate); // Assemble the update messsage, including record ID and context ID, if any. let messageOptions = { ...recordToUpdate.descriptor } as Partial<RecordsWriteOptions>; messageOptions.contextId = recordToUpdate.contextId; messageOptions.recordId = recordToUpdate.recordId; /** Remove properties from the update messageOptions to let the DWN SDK * auto-fill. Otherwisse, you will get 409 Conflict errors. */ delete messageOptions.dataCid; delete messageOptions.dataSize; delete messageOptions.data; delete messageOptions.messageTimestamp; // Overwrite the entry in the store with the updated object. const { reply: { status } } = await agent.dwnManager.processRequest({ author : authorDid, target : authorDid, messageType : 'RecordsWrite', messageOptions, dataStream : new Blob([updatedKeyBytes]) }); // If the write fails, throw an error. if (status.code !== 202) { throw new Error('DidStoreDwn: Failed to write updated key to store.'); } return true; } private decodeKey(keyEncodedData: string): ManagedKey | ManagedKeyPair { const encodedKey = Convert.base64Url(keyEncodedData).toObject() as EncodedKey | EncodedKeyPair; if ('publicKey' in encodedKey) { const privateKeyMaterial = encodedKey.privateKey.material ? Convert.base64Url(encodedKey.privateKey.material).toUint8Array() : undefined; const publicKeyMaterial = encodedKey.publicKey.material ? Convert.base64Url(encodedKey.publicKey.material).toUint8Array() : undefined; const managedKeyPair = { privateKey : { ...encodedKey.privateKey, material: privateKeyMaterial }, publicKey : { ...encodedKey.publicKey, material: publicKeyMaterial} } as ManagedKeyPair; return managedKeyPair; } else { const material = encodedKey.material ? Convert.base64Url(encodedKey.material).toUint8Array() : undefined; const managedKey = { ...encodedKey, material } as ManagedKey; return managedKey; } } private encodeKey(managedKey: ManagedKey | ManagedKeyPair): Uint8Array { let encodedKey: EncodedKey | EncodedKeyPair; if (isManagedKeyPair(managedKey)) { const privateKeyMaterial = managedKey.privateKey.material ? Convert.uint8Array(managedKey.privateKey.material).toBase64Url() : undefined; const publicKeyMaterial = managedKey.publicKey.material ? Convert.uint8Array(managedKey.publicKey.material).toBase64Url() : undefined; encodedKey = { privateKey : { ...managedKey.privateKey, material: privateKeyMaterial }, publicKey : { ...managedKey.publicKey, material: publicKeyMaterial } }; } else { const material = managedKey.material ? Convert.uint8Array(managedKey.material).toBase64Url() : undefined; encodedKey = { ...managedKey, material }; } const keyBytes = Convert.object(encodedKey).toUint8Array(); return keyBytes; } private async getAuthor(options: { agent: IDManagedAgent, context?: string }): Promise<string> { const { agent, context } = options; // If `context` is specified, DWN messages will be signed by this DID. if (context) return context; // If Agent has an agentDid, use it to sign DWN messages. if (agent.agentDid) return agent.agentDid; // If `context` and `agent.agentDid`are undefined, throw error. throw new Error(`KeyStoreDwn: Agent property 'agentDid' is undefined and no context was specified.`); } private async getKeyRecords(agent: IDManagedAgent, context?: string): Promise<DwnResponse> { // Determine which DID to use to author DWN messages. const authorDid = await this.getAuthor({ agent, context }); const dwnResponse = await agent.dwnManager.processRequest({ author : authorDid, target : authorDid, messageType : 'RecordsQuery', messageOptions : { filter: { ...this._keyRecordProperties } } }); return dwnResponse; } } /** * An implementation of `ManagedKeyStore` that stores key metadata and * public key material in memory. * * An instance of this class can be used by `KeyManager` or * an implementation of `KeyManagementSystem`. */ export class KeyStoreMemory implements ManagedKeyStore<string, ManagedKey | ManagedKeyPair> { /** * A private field that contains the Map used as the in-memory key-value store. */ private store: Map<string, ManagedKey | ManagedKeyPair> = new Map(); async deleteKey({ id }: { id: string }): Promise<boolean> { if (this.store.has(id)) { // Key with given ID exists so proceed with delete. this.store.delete(id); return true; } // Key with given ID not present so delete operation not possible. return false; } async findKey(options: { id: string }): Promise<ManagedKey | ManagedKeyPair | undefined>; async findKey(options: { alias: string }): Promise<ManagedKey | ManagedKeyPair | undefined>; async findKey(options: { alias?: string, id?: string }): Promise<ManagedKey | ManagedKeyPair | undefined> { let { alias, id } = options; // Get key by ID. if (id) return this.store.get(id); if (alias) { // Search through the store to find a matching entry. for (const key of await this.listKeys()) { if ('alias' in key && key.alias === alias) return key; if ('publicKey' in key && key.publicKey.alias === alias) return key; } } return undefined; } async getKey({ id }: { id: string }): Promise<ManagedKey | ManagedKeyPair | undefined> { return this.store.get(id); } async importKey({ key }: { key: ManagedKey | ManagedKeyPair }): Promise<string> { let id: string; if (isManagedKeyPair(key)) { id = key.publicKey.id; } else { // If an ID wasn't specified, generate one. if (!key.id) { key.id = cryptoUtils.randomUuid(); } id = key.id; } if (this.store.has(id)) { // Key with given ID already exists so import operation cannot proceed. throw new Error(`KeyStoreMemory: Key with ID already exists: '${id}'`); } // Make a deep copy of the key so that the object stored does not share the same references as the input key. const clonedKey = structuredClone(key); this.store.set(id, clonedKey); return id; } async listKeys(): Promise<(ManagedKey | ManagedKeyPair)[]> { return Array.from(this.store.values()); } async updateKey(options: Pick<ManagedKey, 'id' | 'alias' | 'metadata'> ): Promise<boolean> { const id = options.id; const propertyUpdates = { alias: options.alias, metadata: options.metadata }; const keyExists = this.store.has(id); if (!keyExists) { // Key with given ID not present so update operation cannot proceed. return false; } // Retrieve the current value of the key from the store. let key = await this.getKey({ id }) as ManagedKey | ManagedKeyPair; // Make a deep copy of the update properties to ensure all nested objects do not share references. removeUndefinedProperties(propertyUpdates); removeEmptyObjects(propertyUpdates); const clonedUpdates = structuredClone(propertyUpdates); // Update the given properties of the key. if (isManagedKeyPair(key)) { key.privateKey = { ...key.privateKey, ...clonedUpdates }; key.publicKey = { ...key.publicKey, ...clonedUpdates }; } else { key = { ...key, ...clonedUpdates, id: key.id }; } // Overwrite the entry in the store with the updated object. this.store.set(id, key); return true; } } /** * An implementation of `ManagedKeyStore` that stores private key * material in a DWN. * * An instance of this class can be used by an implementation of * `KeyManagementSystem`. */ export class PrivateKeyStoreDwn implements ManagedKeyStore<string, ManagedPrivateKey> { private _keyRecordProperties = { dataFormat : 'application/json', schema : 'https://identity.foundation/schemas/dwn/kms-private-key' }; async deleteKey(options: { agent: IDManagedAgent, context?: string, id: string }): Promise<boolean> { const { agent, context, id } = options; // Determine which DID to use to author DWN messages. const authorDid = await this.getAuthor({ agent, context }); // Query the DWN for all stored key objects. const { reply: queryReply} = await this.getKeyRecords(agent, context); // Loop through all of the entries and try to find a match. let matchingRecordId: string | undefined; for (const record of queryReply.entries ?? []) { if (record.encodedData) { const storedKey = this.decodeKey(record.encodedData); if (storedKey && storedKey.id === id) { matchingRecordId = (record as RecordsWriteMessage).recordId ; break; } } } // Return undefined if the specified key was not found in the store. if (!matchingRecordId) return false; // If a record for the specified key was found, attempt to delete it. const { reply: { status } } = await agent.dwnManager.processRequest({ author : authorDid, target : authorDid, messageType : 'RecordsDelete', messageOptions : { recordId: matchingRecordId } }); // If the key was successfully deleted, return true; if (status.code === 202) return true; // If the key could not be deleted, return false; return false; } async findKey(): Promise<ManagedPrivateKey | undefined> { throw new Error(`PrivateKeyStoreDwn: Method not implemented: 'findKey'`); } async getKey(options: { agent: IDManagedAgent, context?: string, id: string }): Promise<ManagedPrivateKey | undefined> { const { agent, context, id } = options; // Query the DWN for all stored key objects. const { reply: queryReply} = await this.getKeyRecords(agent, context); // Loop through all of the entries and return a match, if found. for (const record of queryReply.entries ?? []) { if (record.encodedData) { const storedKey = this.decodeKey(record.encodedData); if (storedKey.id === id) return storedKey; } } // Return undefined if no matches were found. return undefined; } async importKey(options: { agent: IDManagedAgent, context?: string, key: Omit<ManagedPrivateKey, 'id'> }): Promise<string> { const { agent, context, key } = options; if (!key.material) throw new TypeError(`Required parameter missing: 'material'`); if (!key.type) throw new TypeError(`Required parameter missing: 'type'`); // Determine which DID to use to author DWN messages. const authorDid = await this.getAuthor({ agent, context }); // Encode the managed key or key pair as bytes. const id = cryptoUtils.randomUuid(); // Generate a random ID. const encodedPrivateKey = this.encodeKey({...key, id }); const { reply: { status } } = await agent.dwnManager.processRequest({ author : authorDid, target : authorDid, messageType : 'RecordsWrite', messageOptions : { ...this._keyRecordProperties }, dataStream : new Blob([encodedPrivateKey]) }); // If the write fails, throw an error. if (status.code !== 202) { throw new Error('PrivateKeyStoreDwn: Failed to write imported DID to store.'); } return id; } async listKeys(options: { agent: IDManagedAgent, context?: string }): Promise<ManagedPrivateKey[]> { const { agent, context } = options; // Query the DWN for all stored key objects. const { reply: queryReply} = await this.getKeyRecords(agent, context); // Loop through all of the entries and accumulate the key objects. let storedKeys: ManagedPrivateKey[] = []; for (const record of queryReply.entries ?? []) { if (record.encodedData) { const storedKey = this.decodeKey(record.encodedData); storedKeys.push(storedKey); } } return storedKeys; } async updateKey(): Promise<boolean> { throw new Error(`PrivateKeyStoreMemory: Method not implemented: 'updateKey'`); } private decodeKey(keyEncodedData: string): ManagedPrivateKey { const encodedKey = Convert.base64Url(keyEncodedData).toObject() as EncodedPrivateKey; const privateKey = { ...encodedKey, material: Convert.base64Url(encodedKey.material).toUint8Array() } as ManagedPrivateKey; return privateKey; } private encodeKey(privateKey: ManagedPrivateKey): Uint8Array { const encodedKey = { ...privateKey, material: Convert.uint8Array(privateKey.material).toBase64Url() } as EncodedPrivateKey; const keyBytes = Convert.object(encodedKey).toUint8Array(); return keyBytes; } private async getAuthor(options: { agent: IDManagedAgent, context?: string }): Promise<string> { const { agent, context } = options; // If `context` is specified, DWN messages will be signed by this DID. if (context) return context; // If Agent has an agentDid, use it to sign DWN messages. if (agent.agentDid) return agent.agentDid; // If `context` and `agent.agentDid`are undefined, throw error. throw new Error(`PrivateKeyStoreDwn: Agent property 'agentDid' is undefined and no context was specified.`); } private async getKeyRecords(agent: IDManagedAgent, context?: string): Promise<DwnResponse> { // Determine which DID to use to author DWN messages. const authorDid = await this.getAuthor({ agent, context }); const dwnResponse = await agent.dwnManager.processRequest({ author : authorDid, target : authorDid, messageType : 'RecordsQuery', messageOptions : { filter: { ...this._keyRecordProperties } } }); return dwnResponse; } } /** * An implementation of `ManagedKeyStore` that stores private key * material in memory. * * An instance of this class can be used by an implementation of * `KeyManagementSystem`. */ export class PrivateKeyStoreMemory implements ManagedKeyStore<string, ManagedPrivateKey> { /** * A private field that contains the Map used as the in-memory key-value store. */ private store: Map<string, ManagedPrivateKey> = new Map(); async deleteKey({ id }: { id: string }): Promise<boolean> { if (this.store.has(id)) { // Key with given ID exists so proceed with delete. this.store.delete(id); return true; } // Key with given ID not present so delete operation not possible. return false; } async findKey(): Promise<ManagedPrivateKey | undefined> { throw new Error(`PrivateKeyStoreMemory: Method not implemented: 'findKey'`); } async getKey({ id }: { id: string }): Promise<ManagedPrivateKey | undefined> { return this.store.get(id); } async importKey({ key }: { key: Omit<ManagedPrivateKey, 'id'> }): Promise<string> { if (!key.material) throw new TypeError(`Required parameter missing: 'material'`); if (!key.type) throw new TypeError(`Required parameter missing: 'type'`); // Make a deep copy of the key so that the object stored does not share the same references as the input key. // The private key material is transferred to the new object, making the original obj.material unusable. const clonedKey = structuredClone(key, { transfer: [key.material.buffer] }) as ManagedPrivateKey; clonedKey.id = cryptoUtils.randomUuid(); this.store.set(clonedKey.id, clonedKey); return clonedKey.id; } async listKeys(): Promise<ManagedPrivateKey[]> { return Array.from(this.store.values()); } async updateKey(): Promise<boolean> { throw new Error(`PrivateKeyStoreMemory: Method not implemented: 'updateKey'`); } }