diff --git a/package-lock.json b/package-lock.json index e562911c..80867ae7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18477,7 +18477,6 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", - "license": "Apache-2.0", "engines": { "node": ">=8" } @@ -37552,6 +37551,7 @@ "axios-retry": "^3.2.5", "base64url": "^3.0.1", "cwait": "1.1.2", + "futoin-hkdf": "^1.5.3", "json-rpc-2.0": "^0.2.16", "jsonld": "^6.0.0", "p-limit": "2.3.0", @@ -37601,7 +37601,7 @@ }, "packages/web": { "name": "@docknetwork/wallet-sdk-web", - "version": "0.0.8", + "version": "0.0.10", "license": "https://github.com/docknetwork/wallet-sdk/LICENSE", "devDependencies": { "@docknetwork/wallet-sdk-core": "^1.7.0", diff --git a/packages/core/src/cloud-wallet.ts b/packages/core/src/cloud-wallet.ts index a83d60af..1ba117ec 100644 --- a/packages/core/src/cloud-wallet.ts +++ b/packages/core/src/cloud-wallet.ts @@ -10,15 +10,11 @@ import { } from '@docknetwork/wallet-sdk-data-store/src/types'; import { logger } from '@docknetwork/wallet-sdk-data-store/src/logger'; import { edvService, EDVService } from '@docknetwork/wallet-sdk-wasm/src/services/edv/service'; -import hkdf from 'futoin-hkdf'; -import crypto from '@docknetwork/universal-wallet/crypto'; import { utilCryptoService } from '@docknetwork/wallet-sdk-wasm/src/services/util-crypto'; export const SYNC_MARKER_TYPE = 'SyncMarkerDocument'; export const MNEMONIC_WORD_COUNT = 12; export const KEY_MAPPING_TYPE = 'KeyMappingDocument'; -export const HKDF_LENGTH = 32; -export const HKDF_HASH = 'SHA-256'; const MASTER_KEY_SUFFIX = 'master-key'; /** @@ -31,9 +27,7 @@ export function deriveBiometricKey( biometricData: Buffer, identifier: string, ): Buffer { - const salt = identifier; - - return hkdf(biometricData, HKDF_LENGTH, { salt, hash: HKDF_HASH }); + return edvService.deriveBiometricKey(biometricData, identifier); } /** @@ -61,15 +55,7 @@ export async function deriveBiometricEncryptionKey( biometricData: Buffer, identifier: string ): Promise<{ key: Buffer; iv: Buffer }> { - const key = deriveBiometricKey(biometricData, identifier); - - const randomBytes = crypto.getRandomValues(new Uint8Array(16)); - const iv = Buffer.from(randomBytes); - - return { - key, - iv - }; + return edvService.deriveBiometricEncryptionKey(biometricData, identifier); } /** @@ -84,24 +70,7 @@ export async function encryptMasterKey( encryptionKey: Buffer, iv: Buffer ): Promise { - const keyData = new Uint8Array(encryptionKey); - const ivData = new Uint8Array(iv); - - const key = await crypto.subtle.importKey( - 'raw', - keyData, - { name: 'AES-GCM' }, - false, - ['encrypt'] - ); - - const encryptedBuffer = await crypto.subtle.encrypt( - { name: 'AES-GCM', iv: ivData }, - key, - masterKey - ); - - return new Uint8Array(encryptedBuffer); + return edvService.encryptMasterKey(masterKey, encryptionKey, iv); } /** @@ -116,28 +85,7 @@ export async function decryptMasterKey( decryptionKey: Buffer, iv: Buffer ): Promise { - try { - const keyData = new Uint8Array(decryptionKey); - const ivData = new Uint8Array(iv); - - const key = await crypto.subtle.importKey( - 'raw', - keyData, - { name: 'AES-GCM' }, - false, - ['decrypt'] - ); - - const decryptedBuffer = await crypto.subtle.decrypt( - { name: 'AES-GCM', iv: ivData }, - key, - encryptedKey - ); - - return new Uint8Array(decryptedBuffer); - } catch (error) { - throw new Error('Decryption failed: Invalid key or corrupted data'); - } + return edvService.decryptMasterKey(encryptedKey, decryptionKey, iv); } /** diff --git a/packages/react-native/bundler/index.js b/packages/react-native/bundler/index.js index a9cf16c9..115c07bd 100644 --- a/packages/react-native/bundler/index.js +++ b/packages/react-native/bundler/index.js @@ -10,6 +10,9 @@ const getWebpackConfig = ({entry, path, filename}) => ({ }, resolve: { extensions: ['.tsx', '.ts', '.js', '.json', '.mjs', '.cjs'], + alias: { + '@cheqd/ts-proto': '@cheqd/ts-proto-cjs', + }, fallback: { crypto: require.resolve('crypto-browserify'), stream: require.resolve('stream-browserify'), @@ -28,7 +31,9 @@ const getWebpackConfig = ({entry, path, filename}) => ({ rules: [ { test: /\.(m|c)?(j|t)s$/, - exclude: [/\/node_modules\/(?!@docknetwork|@digitalbazaar|@cheqd)/], + exclude: [ + /\/node_modules\/(?!@docknetwork|@digitalbazaar|@cheqd\/ts-proto)/, + ], use: { loader: require.resolve('babel-loader'), options: { diff --git a/packages/wasm/package.json b/packages/wasm/package.json index bbfd0084..70e6e24e 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -18,36 +18,37 @@ "p-limit": "2.3.0" }, "dependencies": { - "p-limit": "2.3.0", "@astronautlabs/jsonpath": "^1.1.2", - "@docknetwork/universal-wallet": "^2.0.1", - "@docknetwork/wallet-sdk-dids": "^1.7.0", "@cedar-policy/cedar-wasm": "^4.5.0", - "jsonld": "^6.0.0", - "@docknetwork/vc-delegation-engine": "1.0.3", "@cosmjs/proto-signing": "^0.32.4", "@docknetwork/cheqd-blockchain-api": "4.1.1", "@docknetwork/cheqd-blockchain-modules": "4.0.8", - "@scure/bip39": "^1.6.0", - "@noble/hashes": "1.8.0", "@docknetwork/credential-sdk": "0.54.16", - "@sphereon/oid4vci-client": "^0.16.0", - "@sphereon/oid4vci-common": "^0.16.0", + "@docknetwork/universal-wallet": "^2.0.1", + "@docknetwork/vc-delegation-engine": "1.0.3", + "@docknetwork/wallet-sdk-dids": "^1.7.0", + "@noble/hashes": "1.8.0", + "@scure/bip39": "^1.6.0", "@sd-jwt/crypto-nodejs": "^0.15.0", "@sd-jwt/sd-jwt-vc": "^0.15.1", - "base64url": "^3.0.1", + "@sphereon/oid4vci-client": "^0.16.0", + "@sphereon/oid4vci-common": "^0.16.0", + "@sphereon/pex": "^1.1.4", "axios": "^0.25.0", + "axios-retry": "^3.2.5", + "base64url": "^3.0.1", + "cwait": "1.1.2", + "futoin-hkdf": "^1.5.3", "json-rpc-2.0": "^0.2.16", + "jsonld": "^6.0.0", + "p-limit": "2.3.0", "uuid": "^8.3.2", - "winston": "^3.3.3", - "cwait": "1.1.2", - "axios-retry": "^3.2.5", - "@sphereon/pex": "^1.1.4" + "winston": "^3.3.3" }, "devDependencies": { "@babel/node": "^7.16.0", - "sinon": "^12.0.1", "jest": "29.6.1", + "sinon": "^12.0.1", "ts-jest": "29.1.0", "typescript": "^5.0.4" } diff --git a/packages/wasm/src/services/edv/index.test.js b/packages/wasm/src/services/edv/index.test.js index 1d1683b4..c8fe3ec8 100644 --- a/packages/wasm/src/services/edv/index.test.js +++ b/packages/wasm/src/services/edv/index.test.js @@ -47,5 +47,234 @@ describe('EDVService', () => { expect(JSON.stringify(agreementKey)).toBe(derivedTestAgreementKey); }); }); + + describe('deriveBiometricKey', () => { + it('should derive a key from biometric data', () => { + const biometricData = Buffer.from('mock-biometric-data'); + const identifier = 'user@example.com'; + + const key = service.deriveBiometricKey(biometricData, identifier); + + expect(key).toBeDefined(); + expect(Buffer.isBuffer(key)).toBe(true); + expect(key.length).toBe(32); // HKDF_LENGTH + }); + + it('should produce consistent keys for same inputs', () => { + const biometricData = Buffer.from('mock-biometric-data'); + const identifier = 'user@example.com'; + + const key1 = service.deriveBiometricKey(biometricData, identifier); + const key2 = service.deriveBiometricKey(biometricData, identifier); + + expect(key1.equals(key2)).toBe(true); + }); + + it('should produce different keys for different identifiers', () => { + const biometricData = Buffer.from('mock-biometric-data'); + const identifier1 = 'user1@example.com'; + const identifier2 = 'user2@example.com'; + + const key1 = service.deriveBiometricKey(biometricData, identifier1); + const key2 = service.deriveBiometricKey(biometricData, identifier2); + + expect(key1.equals(key2)).toBe(false); + }); + + it('should produce different keys for different biometric data', () => { + const biometricData1 = Buffer.from('mock-biometric-data-1'); + const biometricData2 = Buffer.from('mock-biometric-data-2'); + const identifier = 'user@example.com'; + + const key1 = service.deriveBiometricKey(biometricData1, identifier); + const key2 = service.deriveBiometricKey(biometricData2, identifier); + + expect(key1.equals(key2)).toBe(false); + }); + }); + + describe('deriveBiometricEncryptionKey', () => { + it('should derive encryption key and IV', async () => { + const biometricData = Buffer.from('mock-biometric-data'); + const identifier = 'user@example.com'; + + const result = await service.deriveBiometricEncryptionKey( + biometricData, + identifier, + ); + + expect(result).toBeDefined(); + expect(result.key).toBeDefined(); + expect(result.iv).toBeDefined(); + expect(Buffer.isBuffer(result.key)).toBe(true); + expect(Buffer.isBuffer(result.iv)).toBe(true); + expect(result.key.length).toBe(32); // HKDF_LENGTH + expect(result.iv.length).toBe(16); // AES-GCM IV length + }); + + it('should produce consistent keys but different IVs for same inputs', async () => { + const biometricData = Buffer.from('mock-biometric-data'); + const identifier = 'user@example.com'; + + const result1 = await service.deriveBiometricEncryptionKey( + biometricData, + identifier, + ); + const result2 = await service.deriveBiometricEncryptionKey( + biometricData, + identifier, + ); + + expect(result1.key.equals(result2.key)).toBe(true); + // IVs should be different (random) + expect(result1.iv.equals(result2.iv)).toBe(false); + }); + }); + + describe('encryptMasterKey and decryptMasterKey', () => { + it('should encrypt and decrypt master key successfully', async () => { + const masterKey = new Uint8Array(32).fill(42); + const encryptionKey = Buffer.from(new Uint8Array(32).fill(1)); + const iv = Buffer.from(new Uint8Array(16).fill(2)); + + const encrypted = await service.encryptMasterKey( + masterKey, + encryptionKey, + iv, + ); + expect(encrypted).toBeDefined(); + expect(encrypted instanceof Uint8Array).toBe(true); + expect(encrypted.length).toBeGreaterThan(0); + + const decrypted = await service.decryptMasterKey( + encrypted, + encryptionKey, + iv, + ); + expect(decrypted).toBeDefined(); + expect(decrypted instanceof Uint8Array).toBe(true); + expect(new Uint8Array(decrypted)).toEqual(masterKey); + }); + + it('should fail to decrypt with wrong key', async () => { + const masterKey = new Uint8Array(32).fill(42); + const encryptionKey = Buffer.from(new Uint8Array(32).fill(1)); + const wrongKey = Buffer.from(new Uint8Array(32).fill(99)); + const iv = Buffer.from(new Uint8Array(16).fill(2)); + + const encrypted = await service.encryptMasterKey( + masterKey, + encryptionKey, + iv, + ); + + await expect( + service.decryptMasterKey(encrypted, wrongKey, iv), + ).rejects.toThrow('Decryption failed: Invalid key or corrupted data'); + }); + + it('should fail to decrypt with wrong IV', async () => { + const masterKey = new Uint8Array(32).fill(42); + const encryptionKey = Buffer.from(new Uint8Array(32).fill(1)); + const iv = Buffer.from(new Uint8Array(16).fill(2)); + const wrongIv = Buffer.from(new Uint8Array(16).fill(99)); + + const encrypted = await service.encryptMasterKey( + masterKey, + encryptionKey, + iv, + ); + + await expect( + service.decryptMasterKey(encrypted, encryptionKey, wrongIv), + ).rejects.toThrow('Decryption failed: Invalid key or corrupted data'); + }); + + it('should produce different ciphertext for same plaintext with different IVs', async () => { + const masterKey = new Uint8Array(32).fill(42); + const encryptionKey = Buffer.from(new Uint8Array(32).fill(1)); + const iv1 = Buffer.from(new Uint8Array(16).fill(2)); + const iv2 = Buffer.from(new Uint8Array(16).fill(3)); + + const encrypted1 = await service.encryptMasterKey( + masterKey, + encryptionKey, + iv1, + ); + const encrypted2 = await service.encryptMasterKey( + masterKey, + encryptionKey, + iv2, + ); + + expect(encrypted1).not.toEqual(encrypted2); + }); + + it('should handle empty master key', async () => { + const masterKey = new Uint8Array(0); + const encryptionKey = Buffer.from(new Uint8Array(32).fill(1)); + const iv = Buffer.from(new Uint8Array(16).fill(2)); + + const encrypted = await service.encryptMasterKey( + masterKey, + encryptionKey, + iv, + ); + const decrypted = await service.decryptMasterKey( + encrypted, + encryptionKey, + iv, + ); + + expect(new Uint8Array(decrypted)).toEqual(masterKey); + }); + }); + + describe('integration: biometric encryption workflow', () => { + it('should complete full encryption workflow', async () => { + const biometricData = Buffer.from('mock-biometric-data'); + const identifier = 'user@example.com'; + const masterKey = new Uint8Array(32).fill(123); + + // Derive encryption key and IV from biometric data + const {key, iv} = await service.deriveBiometricEncryptionKey( + biometricData, + identifier, + ); + + // Encrypt master key + const encrypted = await service.encryptMasterKey(masterKey, key, iv); + + // Decrypt master key + const decrypted = await service.decryptMasterKey(encrypted, key, iv); + + // Verify decrypted matches original + expect(new Uint8Array(decrypted)).toEqual(masterKey); + }); + + it('should fail workflow with different biometric data', async () => { + const biometricData1 = Buffer.from('mock-biometric-data-1'); + const biometricData2 = Buffer.from('mock-biometric-data-2'); + const identifier = 'user@example.com'; + const masterKey = new Uint8Array(32).fill(123); + + // Encrypt with first biometric data + const {key: key1, iv} = await service.deriveBiometricEncryptionKey( + biometricData1, + identifier, + ); + const encrypted = await service.encryptMasterKey(masterKey, key1, iv); + + // Try to decrypt with second biometric data (should fail) + const {key: key2} = await service.deriveBiometricEncryptionKey( + biometricData2, + identifier, + ); + + await expect( + service.decryptMasterKey(encrypted, key2, iv), + ).rejects.toThrow('Decryption failed: Invalid key or corrupted data'); + }); + }); }); }); diff --git a/packages/wasm/src/services/edv/service-rpc.js b/packages/wasm/src/services/edv/service-rpc.js index 7d42f4f6..ac6ec6a6 100644 --- a/packages/wasm/src/services/edv/service-rpc.js +++ b/packages/wasm/src/services/edv/service-rpc.js @@ -38,4 +38,27 @@ export class EDVServiceRpc extends RpcService { delete(params: any) { return this.call('delete', params); } + + deriveBiometricKey(biometricData: Buffer, identifier: string) { + return this.call('deriveBiometricKey', {biometricData, identifier}); + } + + deriveBiometricEncryptionKey(biometricData: Buffer, identifier: string) { + return this.call('deriveBiometricEncryptionKey', { + biometricData, + identifier, + }); + } + + encryptMasterKey(masterKey: Uint8Array, encryptionKey: Buffer, iv: Buffer) { + return this.call('encryptMasterKey', {masterKey, encryptionKey, iv}); + } + + decryptMasterKey( + encryptedKey: Uint8Array, + decryptionKey: Buffer, + iv: Buffer, + ) { + return this.call('decryptMasterKey', {encryptedKey, decryptionKey, iv}); + } } diff --git a/packages/wasm/src/services/edv/service.ts b/packages/wasm/src/services/edv/service.ts index 65e55d5a..15523e77 100644 --- a/packages/wasm/src/services/edv/service.ts +++ b/packages/wasm/src/services/edv/service.ts @@ -17,6 +17,11 @@ import {getKeypairFromDoc} from '@docknetwork/universal-wallet/methods/keypairs' import {logger} from '@docknetwork/wallet-sdk-data-store/src/logger'; import {didService} from '@docknetwork/wallet-sdk-wasm/src/services/dids/service'; import {Ed25519Keypair} from '@docknetwork/credential-sdk/keypairs'; +import hkdf from 'futoin-hkdf'; +import crypto from '@docknetwork/universal-wallet/crypto'; + +export const HKDF_LENGTH = 32; +export const HKDF_HASH = 'SHA-256'; /** * Service class for managing Encrypted Data Vaults @@ -39,6 +44,10 @@ export class EDVService { EDVService.prototype.update, EDVService.prototype.insert, EDVService.prototype.delete, + EDVService.prototype.deriveBiometricKey, + EDVService.prototype.deriveBiometricEncryptionKey, + EDVService.prototype.encryptMasterKey, + EDVService.prototype.decryptMasterKey, ]; /** @@ -269,6 +278,116 @@ export class EDVService { delete(params: any) { return this.storageInterface.delete(params); } + + /** + * Derives a key from biometric data using HKDF + * @param {Buffer} biometricData - Biometric data from provider + * @param {string} identifier - User's identifier as salt (email, phone number, etc.) + * @returns {Buffer} Derived key + * @example + * const key = edvService.deriveBiometricKey(biometricData, 'user@example.com'); + */ + deriveBiometricKey(biometricData: Buffer, identifier: string): Buffer { + const salt = identifier; + return hkdf(biometricData, HKDF_LENGTH, { salt, hash: HKDF_HASH }); + } + + /** + * Generates a key for encrypting/decrypting the master key + * @param {Buffer} biometricData - Biometric data from provider + * @param {string} identifier - User's identifier as salt (email, phone number, etc.) + * @returns {Promise} Encryption key and IV for AES encryption + * @returns {Buffer} returns.key - Encryption key + * @returns {Buffer} returns.iv - Initialization vector + * @example + * const { key, iv } = await edvService.deriveBiometricEncryptionKey(biometricData, 'user@example.com'); + */ + async deriveBiometricEncryptionKey( + biometricData: Buffer, + identifier: string + ): Promise<{ key: Buffer; iv: Buffer }> { + const key = this.deriveBiometricKey(biometricData, identifier); + const randomBytes = crypto.getRandomValues(new Uint8Array(16)); + const iv = Buffer.from(randomBytes); + + return { + key, + iv + }; + } + + /** + * Encrypts the master key using a key derived from biometric data + * @param {Uint8Array} masterKey - The CloudWalletVault master key to encrypt + * @param {Buffer} encryptionKey - Key derived from biometric data + * @param {Buffer} iv - Initialization vector + * @returns {Promise} Encrypted master key + * @example + * const encrypted = await edvService.encryptMasterKey(masterKey, encryptionKey, iv); + */ + async encryptMasterKey( + masterKey: Uint8Array, + encryptionKey: Buffer, + iv: Buffer + ): Promise { + const keyData = new Uint8Array(encryptionKey); + const ivData = new Uint8Array(iv); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'AES-GCM' }, + false, + ['encrypt'] + ); + + const encryptedBuffer = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv: ivData }, + key, + masterKey + ); + + return new Uint8Array(encryptedBuffer); + } + + /** + * Decrypts the master key using biometric-derived key + * @param {Uint8Array} encryptedKey - The encrypted master key + * @param {Buffer} decryptionKey - Key derived from biometric data + * @param {Buffer} iv - Initialization vector + * @returns {Promise} The decrypted master key + * @throws {Error} If decryption fails + * @example + * const masterKey = await edvService.decryptMasterKey(encryptedKey, decryptionKey, iv); + */ + async decryptMasterKey( + encryptedKey: Uint8Array, + decryptionKey: Buffer, + iv: Buffer + ): Promise { + try { + const keyData = new Uint8Array(decryptionKey); + const ivData = new Uint8Array(iv); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'AES-GCM' }, + false, + ['decrypt'] + ); + + const decryptedBuffer = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: ivData }, + key, + encryptedKey + ); + + return new Uint8Array(decryptedBuffer); + } catch (error) { + throw new Error('Decryption failed: Invalid key or corrupted data'); + } + } } /**