Skip to content

Commit

Permalink
Delete Identity/DID/Key from DWN Stores (#744)
Browse files Browse the repository at this point in the history
* ability to delete keys from key manager, identity data and did data

- `delete` method added to `DidApi` which deletes the `portableDid` stored in the `DidStore`
  - Optionally, if the `deleteKey` option is set to false the keys associated with the DID stored by the `KeyStore` are deleted. **default is true**
- `delete` method added to `IdentityApi` which deletes the `IdentityMetadata` stored in the `IdentityStore`
- `deleteKey` method added to the `LocalKeyManager` and `AgentKeyManager` interface.
  • Loading branch information
LiranCohen authored Jul 13, 2024
1 parent 44604a4 commit 1708050
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/mean-ducks-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@web5/agent": patch
---

Add ability to delete IdentityMetadata, PortableDid, and Jwk from Dwn Stores in Agent
5 changes: 3 additions & 2 deletions packages/agent/.c8rc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
"tests/compiled/**/src/index.js",
"tests/compiled/**/src/types.js",
"tests/compiled/**/src/types/**",
"tests/compiled/**/src/prototyping/clients/*-types.js"
"tests/compiled/**/src/prototyping/clients/*-types.js",
"tests/compiled/**/src/prototyping/**/types/**"
],
"reporter": [
"cobertura",
"text"
]
}
}
33 changes: 33 additions & 0 deletions packages/agent/src/did-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,39 @@ export class AgentDidApi<TKeyManager extends AgentKeyManager = AgentKeyManager>
return bearerDid;
}

public async delete({ didUri, tenant, deleteKey = true }: {
didUri: string;
tenant?: string;
deleteKey?: boolean;
}): Promise<void> {
const portableDid = await this._store.get({ id: didUri, agent: this.agent, tenant, useCache: false });
if(!portableDid) {
throw new Error('AgentDidApi: Could not delete, DID not found');
}

// Delete the data before deleting the associated keys.
await this._store.delete({ id: didUri, agent: this.agent, tenant });

if (deleteKey) {
// Delete the keys associated with the DID
// TODO: this could be made a static method on `BearerDid` class
await this.deleteKeys({ portableDid });
}
}

public async deleteKeys({ portableDid }: {
portableDid: PortableDid;
}): Promise<void> {
for (const verificationMethod of portableDid.document.verificationMethod || []) {
if (!verificationMethod.publicKeyJwk) {
continue;
}
// Compute the key URI of the verification method's public key.
const keyUri = await this.agent.keyManager.getKeyUri({ key: verificationMethod.publicKeyJwk });
await this.agent.keyManager.deleteKey({ keyUri });
}
}

public async processRequest<T extends DidInterface>(
request: DidRequest<T>
): Promise<DidResponse<T>> {
Expand Down
14 changes: 14 additions & 0 deletions packages/agent/src/identity-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,18 @@ export class AgentIdentityApi<TKeyManager extends AgentKeyManager = AgentKeyMana

return identity;
}

public async delete({ didUri, tenant }:{
didUri: string;
tenant?: string;
}): Promise<void> {
// Attempt to retrieve the Identity from the Agent's Identity store.
const storedIdentity = await this._store.get({ id: didUri, agent: this.agent, tenant, useCache: true });
if (!storedIdentity) {
throw new Error(`AgentIdentityApi: Failed to purge due to Identity not found: ${didUri}`);
}

// Delete the Identity from the Agent's Identity store.
await this._store.delete({ id: didUri, agent: this.agent, tenant });
}
}
10 changes: 10 additions & 0 deletions packages/agent/src/local-key-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,16 @@ export class LocalKeyManager implements AgentKeyManager {
return wrappedKeyBytes;
}

public async deleteKey({ keyUri }:{ keyUri: KeyIdentifier }): Promise<void> {
// Get the private key from the key store.
const jwk = await this._keyStore.get({ id: keyUri, agent: this.agent, useCache: true });
if (!jwk) {
throw new Error(`Key not found: ${keyUri}`);
}

await this._keyStore.delete({ id: keyUri, agent: this.agent });
}

/**
* Retrieves an algorithm implementation instance based on the provided algorithm name.
*
Expand Down
13 changes: 13 additions & 0 deletions packages/agent/src/prototyping/crypto/types/key-io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,17 @@ export interface KeyImporter<ImportKeyInput, ImportKeyOutput = void> {
* @returns A Promise resolving to the key identifier of the imported key.
*/
importKey(params: ImportKeyInput): Promise<ImportKeyOutput>;
}

export interface KeyDeleter<DeleteKeyInput> {
/**
* Deletes a cryptographic key.
*
* @remarks
* The `deleteKey()` method of the {@link KeyDeleter | `KeyDeleter`} interface deletes a cryptographic
* key from the key store.
*
* @param params - The parameters for the key deletion operation.
*/
deleteKey(params: DeleteKeyInput): Promise<void>;
}
5 changes: 5 additions & 0 deletions packages/agent/src/prototyping/crypto/types/params-kms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,9 @@ export interface KmsWrapKeyParams {

/** An object defining the algorithm-specific parameters for encrypting the `unwrappedKey`. */
encryptParams?: unknown
}

export interface KmsDeleteKeyParams {
/** Identifier for the key to be deleted in the KMS. */
keyUri: KeyIdentifier;
}
5 changes: 3 additions & 2 deletions packages/agent/src/types/key-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import type { Cipher, Jwk, KeyIdentifier, KeyWrapper, KmsExportKeyParams, KmsImp

import type { Web5PlatformAgent } from './agent.js';
import type { KeyManager } from '../prototyping/crypto/types/key-manager.js';
import type { KeyExporter, KeyImporter } from '../prototyping/crypto/types/key-io.js';
import type { KmsCipherParams, KmsUnwrapKeyParams, KmsWrapKeyParams } from '../prototyping/crypto/types/params-kms.js';
import type { KeyDeleter, KeyExporter, KeyImporter } from '../prototyping/crypto/types/key-io.js';
import type { KmsCipherParams, KmsDeleteKeyParams, KmsUnwrapKeyParams, KmsWrapKeyParams } from '../prototyping/crypto/types/params-kms.js';
export interface AgentKeyManager extends KeyManager,
Cipher<KmsCipherParams, KmsCipherParams>,
KeyImporter<KmsImportKeyParams, KeyIdentifier>,
KeyExporter<KmsExportKeyParams, Jwk>,
KeyDeleter<KmsDeleteKeyParams>,
KeyWrapper<KmsWrapKeyParams, KmsUnwrapKeyParams> {

agent: Web5PlatformAgent;
Expand Down
127 changes: 125 additions & 2 deletions packages/agent/tests/did-api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

import sinon from 'sinon';
import { expect } from 'chai';
import { BearerDid, DidJwk } from '@web5/dids';

Expand Down Expand Up @@ -60,11 +60,13 @@ describe('AgentDidApi', () => {
});

beforeEach(async () => {
sinon.restore();
await testHarness.clearStorage();
await testHarness.createAgentDid();
});

after(async () => {
sinon.restore();
await testHarness.clearStorage();
await testHarness.closeStorage();
});
Expand Down Expand Up @@ -174,7 +176,128 @@ describe('AgentDidApi', () => {
});

describe('delete()', () => {
xit('should be implemented');
it('should delete a DID', async () => {
// we use the agentDid as the tenant for this test
// that way when we delete the DID we cna still issue a `get()` and agent's key is still there
const agentDid = testHarness.agent.agentDid.uri;
// Generate a new DID.
const did = await testHarness.agent.did.create({tenant: agentDid, method: 'jwk', store: true }); // store

// attempt to get the DID
let storedDid = await testHarness.agent.did.get({ didUri: did.uri, tenant: agentDid });
expect(storedDid).to.not.be.undefined;
expect(storedDid!.uri).to.equal(did.uri);

// delete the DID
await testHarness.agent.did.delete({ didUri: did.uri, tenant: agentDid });

// attempt to get the DID again
storedDid = await testHarness.agent.did.get({ didUri: did.uri, tenant: agentDid });
expect(storedDid).to.be.undefined;
});

it('should throw not found if the DID does not exist', async () => {
try {
await testHarness.agent.did.delete({ didUri: 'did:method:abc123', tenant: testHarness.agent.agentDid.uri });
expect.fail('Expected an error to be thrown');
} catch(error: any) {
expect(error.message).to.include('AgentDidApi: Could not delete, DID not found');
}
});

it('should not be able to get signer for tenant after the tenant DID is deleted and the deleteKey parameter is set not false', async function () {
// This test is only relevant for the DWN store as it needs a signer to perform storage operations
if (agentStoreType !== 'dwn') {
this.skip();
}
// Generate a new DID, since no tenant is provided it will be stored under its own tenant
const did = await testHarness.agent.did.create({ method: 'jwk', store: true }); // store

// attempt to get the DID
let storedDid = await testHarness.agent.did.get({ didUri: did.uri, tenant: did.uri });
expect(storedDid).to.not.be.undefined;
expect(storedDid!.uri).to.equal(did.uri);

// delete the DID
await testHarness.agent.did.delete({ didUri: did.uri, tenant: did.uri });

console.log('deleted');
// attempt to get the DID again
try {
storedDid = await testHarness.agent.did.get({ didUri: did.uri, tenant: did.uri });
expect.fail('Expected an error to be thrown');
} catch(error:any) {
expect(error.message).to.include('Unable to get signer for author');
}
});

it('should keep key if deleteKey parameter is false', async () => {
// Generate a new DID.
const did = await testHarness.agent.did.create({ method: 'jwk', store: true }); // store

// attempt to get the DID
let storedDid = await testHarness.agent.did.get({ didUri: did.uri, tenant: did.uri});
expect(storedDid).to.not.be.undefined;
expect(storedDid!.uri).to.equal(did.uri);

// spy on deleteKey
const keyManagerSpy = sinon.spy(testHarness.agent.keyManager, 'deleteKey');

// delete the DID without deleting the key
await testHarness.agent.did.delete({ didUri: did.uri, tenant: did.uri, deleteKey: false });

expect(keyManagerSpy.called).to.be.false;

// attempt to get the DID again this will not fail because the key still exists
storedDid = await testHarness.agent.did.get({ didUri: did.uri, tenant: did.uri });
expect(storedDid).to.be.undefined;
});

it('should skip non Jwk encoded verification methods', async () => {
// stub store to return a portable did with non-jwk verification methods
sinon.stub(testHarness.agent.did['_store'], 'get').resolves({
uri : 'did:method:abc123',
metadata : {},
document : {
id : 'did:method:abc123',
verificationMethod : [{
id : 'did:method:abc123#key1',
type : 'Ed25519VerificationKey2018',
controller : 'did:method:abc123',
publicKeyMultibase : 'z6Mkq'
}]
}
});

sinon.stub(testHarness.agent.did['_store'], 'delete').resolves();

// spy on deleteKey
const keyManagerSpy = sinon.spy(testHarness.agent.keyManager, 'deleteKey');
// delete the DID
await testHarness.agent.did.delete({ didUri: 'did:example:123' });

expect(keyManagerSpy.called).to.be.false;
});

it('skips if verificationMethod is not defined', async () => {
// stub store to return a portable did with non-jwk verification methods
sinon.stub(testHarness.agent.did['_store'], 'get').resolves({
uri : 'did:method:abc123',
metadata : {},
document : {
id: 'did:method:abc123',
}
});

sinon.stub(testHarness.agent.did['_store'], 'delete').resolves();

// spy on deleteKey
const keyManagerDeleteSpy = sinon.spy(testHarness.agent.keyManager, 'deleteKey');
// delete the DID
await testHarness.agent.did.delete({ didUri: 'did:example:123' });

expect(keyManagerDeleteSpy.called).to.be.false;
});
});

describe('export()', () => {
Expand Down
42 changes: 42 additions & 0 deletions packages/agent/tests/identity-api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sinon from 'sinon';
import { expect } from 'chai';

import { TestAgent } from './utils/test-agent.js';
Expand Down Expand Up @@ -51,11 +52,13 @@ describe('AgentIdentityApi', () => {
});

beforeEach(async () => {
sinon.restore();
await testHarness.clearStorage();
await testHarness.createAgentDid();
});

after(async () => {
sinon.restore();
await testHarness.clearStorage();
await testHarness.closeStorage();
});
Expand Down Expand Up @@ -145,6 +148,45 @@ describe('AgentIdentityApi', () => {
expect(storedIdentities).to.be.empty;
});
});

describe('delete()', () => {
it('deletes an Identity', async () => {
// Create a new Identity.
const identity = await testHarness.agent.identity.create({
didMethod : 'jwk',
metadata : { name: 'Test Identity' },
store : true
});

// Verify that the Identity exists.
let storedIdentity = await testHarness.agent.identity.get({ didUri: identity.did.uri, tenant: identity.did.uri });
expect(storedIdentity).to.exist;
expect(storedIdentity?.did.uri).to.equal(identity.did.uri);

// Delete the Identity.
await testHarness.agent.identity.delete({ didUri: identity.did.uri, tenant: identity.did.uri });

// Verify that the Identity no longer exists.
storedIdentity = await testHarness.agent.identity.get({ didUri: identity.did.uri, tenant: identity.did.uri });
expect(storedIdentity).to.not.exist;

// Verify that the DID still exists
const storedDid = await testHarness.agent.did.get({ didUri: identity.did.uri, tenant: identity.did.uri });
expect(storedDid).to.not.be.undefined;
expect(storedDid!.uri).to.equal(identity.did.uri);
});

it('fails with not found error if the Identity does not exist', async () => {
// Delete an Identity that does not exist.
const didUri = 'did:method:xyz123';
try {
await testHarness.agent.identity.delete({ didUri });
expect.fail('Expected an error to be thrown');
} catch (error: any) {
expect(error.message).to.include('AgentIdentityApi: Failed to purge due to Identity not found');
}
});
});
});
}
});
Loading

0 comments on commit 1708050

Please sign in to comment.