Skip to content

Commit

Permalink
Create biometric derived keyMapping to unlock the cloud wallet.
Browse files Browse the repository at this point in the history
  • Loading branch information
gasher committed Mar 4, 2025
1 parent 4adf558 commit e314117
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 7 deletions.
106 changes: 99 additions & 7 deletions integration-tests/cloud-wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
initializeCloudWallet,
generateCloudWalletMasterKey,
recoverCloudWalletMasterKey,
createKeyMapping,
recoverMasterKeyWithMapping,
deriveBiometricKey,
} from '@docknetwork/wallet-sdk-core/src/cloud-wallet';
import {createDataStore} from '@docknetwork/wallet-sdk-data-store-typeorm/src';
import {edvService} from '@docknetwork/wallet-sdk-wasm/src/services/edv';
Expand Down Expand Up @@ -58,18 +61,107 @@ describe('Cloud wallet', () => {
});
});

it('should be able to generate a masterKey and a mnemonic', async () => {
const { masterKey, mnemonic } = await generateCloudWalletMasterKey();
expect(masterKey).toBeDefined();
describe('Key Mapping', () => {
let masterKey: string;
let secondaryKey: string;

beforeEach(async () => {
const result = await generateCloudWalletMasterKey();
masterKey = result.masterKey;

// Create a mock biometric key
const mockBiometricData = { fingerprintId: '12345', timestamp: Date.now() };
secondaryKey = await deriveBiometricKey(mockBiometricData);
});

it('should recover the master key using secondary key and mapping', async () => {
const keyMapping = await createKeyMapping(masterKey, secondaryKey);

expect(keyMapping).toBeDefined();
expect(typeof keyMapping).toBe('string');

const recoveredKey = await recoverMasterKeyWithMapping(secondaryKey, keyMapping);

expect(recoveredKey).toBe(masterKey);
});

it('should work with different length keys', async () => {
const longBiometricData = {
fingerprintId: '12345',
facialData: Array(100).fill('x').join(''),
timestamp: Date.now()
};
const longSecondaryKey = await deriveBiometricKey(longBiometricData);

const keyMapping = await createKeyMapping(masterKey, longSecondaryKey);
const recoveredKey = await recoverMasterKeyWithMapping(longSecondaryKey, keyMapping);

expect(recoveredKey).toBe(masterKey);
});

it('should handle master key being longer than secondary key', async () => {
const shortBiometricData = { id: '123' };
const shortSecondaryKey = await deriveBiometricKey(shortBiometricData);

const keyMapping = await createKeyMapping(masterKey, shortSecondaryKey);
const recoveredKey = await recoverMasterKeyWithMapping(shortSecondaryKey, keyMapping);

expect(recoveredKey).toBe(masterKey);
});

it('should throw error when missing required parameters', async () => {
await expect(createKeyMapping('', secondaryKey)).rejects.toThrow();
await expect(createKeyMapping(masterKey, '')).rejects.toThrow();
await expect(recoverMasterKeyWithMapping('', 'mapping')).rejects.toThrow();
await expect(recoverMasterKeyWithMapping(secondaryKey, '')).rejects.toThrow();
});

it('should throw error when secondary key is too short for recovery', async () => {
const keyMapping = await createKeyMapping(masterKey, secondaryKey);

const shorterBiometricData = { id: '1' };
const shorterKey = await deriveBiometricKey(shorterBiometricData);

await expect(recoverMasterKeyWithMapping(shorterKey, keyMapping))
.rejects.toThrow('Secondary key is shorter than the one used for mapping creation');
});
});

it('should generate a valid master key with mnemonic', async () => {
const { mnemonic, masterKey } = await generateCloudWalletMasterKey();

expect(mnemonic).toBeDefined();
expect(typeof mnemonic).toBe('string');
expect(mnemonic.split(' ').length).toBe(12);

expect(masterKey).toBeDefined();
expect(typeof masterKey).toBe('string');
});

it('should be able to recover a masterKey from a mnemonic', async () => {
const { masterKey, mnemonic } = await generateCloudWalletMasterKey();
const recoveredMasterKey = await recoverCloudWalletMasterKey(mnemonic);
expect(recoveredMasterKey).toEqual(masterKey);
it('should recover the same master key from a mnemonic', async () => {
const { mnemonic, masterKey } = await generateCloudWalletMasterKey();
const recoveredKey = await recoverCloudWalletMasterKey(mnemonic);

expect(recoveredKey).toBe(masterKey);
});

it('should allow unlocking the same wallet with different keys', async () => {
const { mnemonic, masterKey } = await generateCloudWalletMasterKey();

const biometricData = { deviceId: 'device123', timestamp: Date.now() };
const biometricKey = await deriveBiometricKey(biometricData);

const keyMapping = await createKeyMapping(masterKey, biometricKey);

const mnemonicRecoveredKey = await recoverCloudWalletMasterKey(mnemonic);
const biometricRecoveredKey = await recoverMasterKeyWithMapping(biometricKey, keyMapping);

expect(mnemonicRecoveredKey).toBe(masterKey);
expect(biometricRecoveredKey).toBe(masterKey);
expect(mnemonicRecoveredKey).toBe(biometricRecoveredKey);
});


it('should see a document added directly to the EDV appear in the wallet after pulling', async () => {
const newDocId = `${Date.now()}`;
const newDoc = {
Expand Down
99 changes: 99 additions & 0 deletions packages/core/src/cloud-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,105 @@ export async function recoverCloudWalletMasterKey(mnemonic: string): Promise<str
return masterKey;
}

/**
* Generates a key from biometric data that can unlock the same master key
* Note: The actual biometric processing is handled by the platform's secure enclave
* This function would be implemented by the cloud wallet partner integrating with
* platform-specific biometric APIs
*/
export async function deriveBiometricKey(biometricData: any): Promise<string> {
// In reality, this would use platform-specific biometric APIs
// This is a simplified implementation that wallet providers would replace
return base64url.encode(Buffer.from(JSON.stringify(biometricData)));
}

/**
* Creates a secure mapping between a biometrically-derived key and the master key
* This allows the same wallet to be unlocked via multiple authentication methods
* @param masterKey The master key derived from the mnemonic
* @param secondaryKey The key derived from biometrics or other authentication method
* @returns A key mapping that allows recovery of the master key
*/
export async function createKeyMapping(masterKey: string, secondaryKey: string): Promise<string> {
if (!masterKey || !secondaryKey) {
throw new Error('Both masterKey and secondaryKey are required');
}

const masterKeyBytes = base64url.decode(masterKey);
const secondaryKeyBytes = base64url.decode(secondaryKey);

// Create mapping bytes with the length of the master key to preserve all data
const mappingBytes = new Uint8Array(masterKeyBytes.length);

// Store the original lengths to handle recovery correctly
const lengthPrefix = new Uint8Array(2);
lengthPrefix[0] = masterKeyBytes.length;
lengthPrefix[1] = secondaryKeyBytes.length;

// XOR the overlapping parts of both keys
const overlapLength = Math.min(masterKeyBytes.length, secondaryKeyBytes.length);
for (let i = 0; i < overlapLength; i++) {
mappingBytes[i] = masterKeyBytes[i] ^ secondaryKeyBytes[i];
}

// For any remaining master key bytes beyond the secondary key length,
// we need to store them directly (but obfuscated)
for (let i = overlapLength; i < masterKeyBytes.length; i++) {
mappingBytes[i] = masterKeyBytes[i] ^ 0xFF;
}

const result = new Uint8Array(lengthPrefix.length + mappingBytes.length);
result.set(lengthPrefix, 0);
result.set(mappingBytes, lengthPrefix.length);

return base64url.encode(Buffer.from(result));
}

/**
* Recovers the master key using a secondary key and the key mapping
* @param secondaryKey The key derived from biometrics or other authentication method
* @param keyMapping The mapping created by createKeyMapping
* @returns The recovered master key
*/
export async function recoverMasterKeyWithMapping(secondaryKey: string, keyMapping: string): Promise<string> {
if (!secondaryKey || !keyMapping) {
throw new Error('Both secondaryKey and keyMapping are required');
}

const secondaryKeyBytes = base64url.decode(secondaryKey);
const mappingData = base64url.decode(keyMapping);

// Extract the length prefix
const masterKeyLength = mappingData[0];
const originalSecondaryKeyLength = mappingData[1];
const mappingBytes = mappingData.slice(2);

if (mappingBytes.length === 0 || mappingBytes.length !== masterKeyLength) {
throw new Error('Invalid key mapping format');
}

// Verify secondary key length matches what was used to create the mapping
if (secondaryKeyBytes.length < originalSecondaryKeyLength) {
throw new Error('Secondary key is shorter than the one used for mapping creation');
}

// Recover the master key using XOR
const masterKeyBytes = new Uint8Array(masterKeyLength);

// Recover the overlapping part using XOR with secondary key
const overlapLength = Math.min(masterKeyLength, originalSecondaryKeyLength);
for (let i = 0; i < overlapLength; i++) {
masterKeyBytes[i] = secondaryKeyBytes[i] ^ mappingBytes[i];
}

// Recover the non-overlapping part using the obfuscation method
for (let i = overlapLength; i < masterKeyLength; i++) {
masterKeyBytes[i] = mappingBytes[i] ^ 0xFF;
}

return base64url.encode(Buffer.from(masterKeyBytes));
}

export async function initializeCloudWallet({
dataStore,
edvUrl,
Expand Down

0 comments on commit e314117

Please sign in to comment.