Skip to content

Commit

Permalink
should fix bug in dcx identity vault
Browse files Browse the repository at this point in the history
  • Loading branch information
Bnonni committed Aug 26, 2024
1 parent f089ebb commit 0365c69
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 286 deletions.
73 changes: 61 additions & 12 deletions packages/common/src/dcx-identity-vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { generateMnemonic, mnemonicToSeed, validateMnemonic } from '@scure/bip39
import { wordlist } from '@scure/bip39/wordlists/english';
import {
AgentCryptoApi,
HdIdentityVault,
IdentityVault,
IdentityVaultBackup,
IdentityVaultBackupData,
IdentityVaultParams,
IdentityVaultStatus,
LocalKeyManager,
LocalKeyManager
} from '@web5/agent';
import { Convert, KeyValueStore, LevelStore } from '@web5/common';
import { Jwk } from '@web5/crypto';
Expand All @@ -18,10 +17,10 @@ import {
isEmptyString,
isIdentityVaultBackup,
isIdentityVaultStatus,
isPortableDid,
Logger
isPortableDid
} from './index.js';
import { CompactJwe } from './prototyping/crypto/jose/jwe-compact.js';
import { JweHeaderParams } from './prototyping/crypto/jose/jwe.js';
import { DeterministicKeyGenerator } from './prototyping/crypto/utils.js';

export type DcxIdentityVaultInitializeParams = {
Expand Down Expand Up @@ -49,7 +48,8 @@ export type DcxIdentityVaultInitializeParams = {
dwnEndpoints: string[];
};

export class DcxIdentityVault extends HdIdentityVault implements IdentityVault<{ InitializeResult: string }> {
export type DcxIdentityVaultParams = IdentityVaultParams & { location?: string };
export class DcxIdentityVault implements IdentityVault<{ InitializeResult: string }> {
/** Provides cryptographic functions needed for secure storage and management of the vault. */
public crypto = new AgentCryptoApi();

Expand All @@ -62,21 +62,70 @@ export class DcxIdentityVault extends HdIdentityVault implements IdentityVault<{
/** The cryptographic key used to encrypt and decrypt the vault's content securely. */
contentEncryptionKey: Jwk | undefined;

constructor({ keyDerivationWorkFactor, store }: IdentityVaultParams = {}) {
super({ keyDerivationWorkFactor, store });
constructor({
keyDerivationWorkFactor,
store,
location
}: DcxIdentityVaultParams = { location: 'DATA/DCX/AGENT/VAULT_STORE' }) {
this.keyDerivationWorkFactor = keyDerivationWorkFactor ?? 210_000;
this.store = store ?? new LevelStore<string, string>({ location: 'DATA/DCX/VAULT_STORE' });
this.store = store ?? new LevelStore<string, string>({ location });
}

public static create(): DcxIdentityVault {
return new DcxIdentityVault();
}

public changePassword(params: { oldPassword: string; newPassword: string; }): Promise<void> {
Logger.debug('DcxIdentityVault: Changing password...', params);
throw new Error('Method not implemented.');
}
public async changePassword({ oldPassword, newPassword }: {
oldPassword: string;
newPassword: string;
}): Promise<void> {
// Verify the identity vault has already been initialized.
if (await this.isInitialized() === false) {
throw new Error(
'DcxIdentityVault: Unable to proceed with the change password operation because the ' +
'identity vault has not been initialized. Please ensure the vault is properly ' +
'initialized with a secure password before trying again.'
);
}

// Lock the vault.
await this.lock();

// Retrieve the content encryption key (CEK) record as a compact JWE from the data store.
const cekJwe = await this.getStoredContentEncryptionKeyDcx();

// Decrypt the compact JWE using the given `oldPassword` to verify it is correct.
let protectedHeader: JweHeaderParams;
let contentEncryptionKey: Jwk;
try {
let contentEncryptionKeyBytes: Uint8Array;
({ plaintext: contentEncryptionKeyBytes, protectedHeader } = await CompactJwe.decrypt({
jwe : cekJwe,
key : Convert.string(oldPassword).toUint8Array(),
crypto : this.crypto,
keyManager : new LocalKeyManager()
}));
contentEncryptionKey = Convert.uint8Array(contentEncryptionKeyBytes).toObject() as Jwk;

} catch (error: any) {
throw new Error(`DcxIdentityVault: Unable to change the vault password due to an incorrectly entered old password.`);
}

// Re-encrypt the vault content encryption key (CEK) using the new password.
const newCekJwe = await CompactJwe.encrypt({
key : Convert.string(newPassword).toUint8Array(),
protectedHeader, // Re-use the protected header from the original JWE.
plaintext : Convert.object(contentEncryptionKey).toUint8Array(),
crypto : this.crypto,
keyManager : new LocalKeyManager()
});

// Update the vault with the new CEK JWE.
await this.store.set('contentEncryptionKey', newCekJwe);

// Update the vault CEK in memory, effectively unlocking the vault.
this.contentEncryptionKey = contentEncryptionKey;
}
public async getStatus(): Promise<IdentityVaultStatus> {
const storedStatus = await this.store.get('vaultStatus');
if (!storedStatus) {
Expand Down
70 changes: 0 additions & 70 deletions packages/common/tests/dcx-agent.spec.ts

This file was deleted.

51 changes: 0 additions & 51 deletions packages/common/tests/dcx-config.spec.ts

This file was deleted.

124 changes: 100 additions & 24 deletions packages/common/tests/dcx-identity-vault.spec.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,116 @@
import { LevelStore, MemoryStore } from '@web5/common';
import { expect } from 'chai';
import { DcxIdentityVault, FileSystem } from '../src/index.js';
import { DcxIdentityVault, Mnemonic, FileSystem } from '../src/index.js';
import { LevelStore, MemoryStore } from '@web5/common';

type DxcxIdentityVaultStatus = { initialized: boolean; lastBackup: string | null; lastRestore: string | null };

describe('DcxIdentityVault class', () => {
const location = '__TEST_DATA__/DCX_COMMON/AGENT/DATASTORE';
process.env.NODE_ENV = 'test';

describe('DcxIdentityVault', () => {
const location = '__TEST_DATA__/DCX/AGENT/VAULT_STORE';
const dwnEndpoints = ['http://localhost:3000'];
let recoveryPhrase = Mnemonic.createRecoveryPhrase();
let password = Mnemonic.createPassword();
let vaultStatus: DxcxIdentityVaultStatus;
let returnedRecoveryPhrase: string;

afterEach(async () => {
await FileSystem.rm('__TEST_DATA__', { recursive: true, force: true });
});

describe('takes two constructor arguments', () => {
it('should initialize successfully with default IdentityVaultParams', () => {
const defaultVault = new DcxIdentityVault();
describe(`defaultVault = new DcxIdentityVault({ location: ${location} })`, () => {
recoveryPhrase = Mnemonic.createRecoveryPhrase();
password = Mnemonic.createPassword();
const defaultVault = new DcxIdentityVault({ location });

it('should be instanceof DcxIdentityVault', () => {
expect(defaultVault).to.be.instanceof(DcxIdentityVault);
expect(defaultVault).to.have.property('store');
expect(defaultVault).to.have.property('keyDerivationWorkFactor');
expect(defaultVault).to.have.property('contentEncryptionKey');
});

it('should initialize successfully with default IdentityVaultParams', () => {
const levelVault = new DcxIdentityVault({ store: new MemoryStore<string, string>() });
expect(levelVault).to.be.instanceof(DcxIdentityVault);
expect(levelVault).to.have.property('store');
expect(levelVault).to.have.property('keyDerivationWorkFactor');
expect(levelVault).to.have.property('contentEncryptionKey');
it('should have property "store" as instanceof LevelStore', () => {
expect(defaultVault).to.have.property('store').that.is.instanceof(LevelStore);
});

it('should have property "keyDerivationWorkFactor" as number equal to 210_000', async () => {
expect(defaultVault).to.have.property('keyDerivationWorkFactor').that.is.a('number').and.equals(210_000);
});

describe('await defaultVault.getStatus()', () => {
it('should have status of initialized=false lastBackup=null lastRestore=null', async () => {
vaultStatus = await defaultVault.getStatus();
expect(vaultStatus.initialized).to.be.false;
expect(vaultStatus.lastBackup).to.be.null;
expect(vaultStatus.lastRestore).to.be.null;
});
});

describe('await defaultVault.initialize({ password, recoveryPhrase, dwnEndpoints })', () => {
it('should initialize successfully with params { password, recoveryPhrase, dwnEndpoints }', async () => {
returnedRecoveryPhrase = await defaultVault.initialize({ password, recoveryPhrase, dwnEndpoints });
});

it('should return a matching recoveryPhrase', async () => {
expect(recoveryPhrase).to.equal(returnedRecoveryPhrase);
});

it('should have property "contentEncryptionKey" after initialization', async () => {
expect(defaultVault).to.have.property('contentEncryptionKey');
});

it('should have updated status of initialized=true lastBackup=null lastRestore=null', async () => {
vaultStatus = await defaultVault.getStatus();
expect(vaultStatus.initialized).to.be.true;
expect(vaultStatus.lastBackup).to.be.null;
expect(vaultStatus.lastRestore).to.be.null;
});
});
});

describe(`customVault = new DcxIdentityVault({ store: new MemoryStore(), location: ${location} })`, () => {
recoveryPhrase = Mnemonic.createRecoveryPhrase();
password = Mnemonic.createPassword();
const customVault = new DcxIdentityVault({ store: new MemoryStore(), location });

it('should be instanceof DcxIdentityVault', () => {
expect(customVault).to.be.instanceof(DcxIdentityVault);
});

it('should have property "store" as instanceof MemoryStore', () => {
expect(customVault).to.have.property('store').that.is.instanceof(MemoryStore);
});

it('should have property "keyDerivationWorkFactor" as number equal to 210_000', async () => {
expect(customVault).to.have.property('keyDerivationWorkFactor').that.is.a('number').and.equals(210_000);
});

it('should initialize successfully with custom IdentityVaultParams', () => {
const customAgentVault = new DcxIdentityVault({
keyDerivationWorkFactor : 420_000,
store : new LevelStore({ location }),
describe('await customVault.getStatus()', () => {
it('should have status of initialized=false lastBackup=null lastRestore=null', async () => {
vaultStatus = await customVault.getStatus();
expect(vaultStatus.initialized).to.be.false;
expect(vaultStatus.lastBackup).to.be.null;
expect(vaultStatus.lastRestore).to.be.null;
});
});

describe('await customVault.initialize({ password, recoveryPhrase, dwnEndpoints })', () => {
it('should initialize successfully with params { password, recoveryPhrase, dwnEndpoints }', async () => {
returnedRecoveryPhrase = await customVault.initialize({ password, recoveryPhrase, dwnEndpoints });
});

it('should return a matching recoveryPhrase', () => {
expect(recoveryPhrase).to.equal(returnedRecoveryPhrase);
});

it('should have property "contentEncryptionKey" after initialization', () => {
expect(customVault).to.have.property('contentEncryptionKey');
});

it('should have updated status of initialized=true lastBackup=null lastRestore=null', async () => {
vaultStatus = await customVault.getStatus();
expect(vaultStatus.initialized).to.be.true;
expect(vaultStatus.lastBackup).to.be.null;
expect(vaultStatus.lastRestore).to.be.null;
});
expect(customAgentVault).to.be.instanceof(DcxIdentityVault);
expect(customAgentVault).to.have.property('store');
expect(customAgentVault).to.have.property('keyDerivationWorkFactor');
expect(customAgentVault).to.have.property('contentEncryptionKey');
});
});
});
Loading

0 comments on commit 0365c69

Please sign in to comment.