diff --git a/src/crypto/keys.test.ts b/src/crypto/keys.test.ts new file mode 100644 index 0000000..d129e9b --- /dev/null +++ b/src/crypto/keys.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from 'vitest'; +import { generateDefaultIdentityKeysHD, generateDefaultIdentityKeys } from './keys.js'; +import { generateNewMnemonic } from './hd.js'; + +/** + * Tests for identity key generation in the create flow. + * + * The create-identity path calls generateDefaultIdentityKeysHD() which must + * produce all required keys including both ENCRYPTION and DECRYPTION. + */ +describe('generateDefaultIdentityKeysHD', () => { + const network = 'testnet' as const; + const mnemonic = generateNewMnemonic(128); + + it('produces the correct key layout for identity creation', () => { + const keys = generateDefaultIdentityKeysHD(network, mnemonic); + const layout = keys.map((k) => ({ + id: k.id, + name: k.name, + purpose: k.purpose, + securityLevel: k.securityLevel, + keyType: k.keyType, + })); + + expect(layout).toEqual([ + { id: 0, name: 'Master', purpose: 'AUTHENTICATION', securityLevel: 'MASTER', keyType: 'ECDSA_SECP256K1' }, + { id: 1, name: 'High Auth', purpose: 'AUTHENTICATION', securityLevel: 'HIGH', keyType: 'ECDSA_SECP256K1' }, + { id: 2, name: 'Critical Auth', purpose: 'AUTHENTICATION', securityLevel: 'CRITICAL', keyType: 'ECDSA_SECP256K1' }, + { id: 3, name: 'Transfer', purpose: 'TRANSFER', securityLevel: 'CRITICAL', keyType: 'ECDSA_SECP256K1' }, + { id: 4, name: 'Encryption', purpose: 'ENCRYPTION', securityLevel: 'MEDIUM', keyType: 'ECDSA_SECP256K1' }, + { id: 5, name: 'Decryption', purpose: 'DECRYPTION', securityLevel: 'MEDIUM', keyType: 'ECDSA_SECP256K1' }, + ]); + }); + +}); + +describe('generateDefaultIdentityKeys (deprecated)', () => { + it('produces the same key layout as the HD variant', () => { + const keys = generateDefaultIdentityKeys('testnet'); + expect(keys).toHaveLength(6); + const purposes = keys.map((k) => k.purpose); + expect(purposes).toContain('ENCRYPTION'); + expect(purposes).toContain('DECRYPTION'); + }); +}); diff --git a/src/crypto/keys.ts b/src/crypto/keys.ts index 16018f6..34c1216 100644 --- a/src/crypto/keys.ts +++ b/src/crypto/keys.ts @@ -125,7 +125,7 @@ export function updateKeyType( } /** - * Generate default identity keys (5 keys) + * Generate default identity keys (6 keys) * @deprecated Use generateDefaultIdentityKeysHD for HD derivation */ export function generateDefaultIdentityKeys( @@ -137,6 +137,7 @@ export function generateDefaultIdentityKeys( generateIdentityKey(2, 'Critical Auth', 'ECDSA_SECP256K1', 'AUTHENTICATION', 'CRITICAL', network), generateIdentityKey(3, 'Transfer', 'ECDSA_SECP256K1', 'TRANSFER', 'CRITICAL', network), generateIdentityKey(4, 'Encryption', 'ECDSA_SECP256K1', 'ENCRYPTION', 'MEDIUM', network), + generateIdentityKey(5, 'Decryption', 'ECDSA_SECP256K1', 'DECRYPTION', 'MEDIUM', network), ]; } @@ -214,6 +215,7 @@ export function generateDefaultIdentityKeysHD( generateIdentityKeyFromMnemonic(2, 'Critical Auth', 'ECDSA_SECP256K1', 'AUTHENTICATION', 'CRITICAL', network, mnemonic, 2), generateIdentityKeyFromMnemonic(3, 'Transfer', 'ECDSA_SECP256K1', 'TRANSFER', 'CRITICAL', network, mnemonic, 3), generateIdentityKeyFromMnemonic(4, 'Encryption', 'ECDSA_SECP256K1', 'ENCRYPTION', 'MEDIUM', network, mnemonic, 4), + generateIdentityKeyFromMnemonic(5, 'Decryption', 'ECDSA_SECP256K1', 'DECRYPTION', 'MEDIUM', network, mnemonic, 5), ]; } diff --git a/src/types.ts b/src/types.ts index 989c30f..337477a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,7 +34,7 @@ export type KeyType = 'ECDSA_SECP256K1' | 'ECDSA_HASH160'; /** * Key purposes supported by Dash Platform */ -export type KeyPurpose = 'AUTHENTICATION' | 'ENCRYPTION' | 'TRANSFER' | 'VOTING' | 'OWNER'; +export type KeyPurpose = 'AUTHENTICATION' | 'ENCRYPTION' | 'DECRYPTION' | 'TRANSFER' | 'VOTING' | 'OWNER'; /** * Security levels supported by Dash Platform diff --git a/src/ui/components.ts b/src/ui/components.ts index eb99212..d08f5a0 100644 --- a/src/ui/components.ts +++ b/src/ui/components.ts @@ -9,7 +9,7 @@ import { getAssetLockDerivationPath } from '../crypto/hd.js'; // Available options for key configuration const KEY_TYPES: KeyType[] = ['ECDSA_SECP256K1', 'ECDSA_HASH160']; -const KEY_PURPOSES: KeyPurpose[] = ['AUTHENTICATION', 'ENCRYPTION', 'TRANSFER', 'VOTING', 'OWNER']; +const KEY_PURPOSES: KeyPurpose[] = ['AUTHENTICATION', 'ENCRYPTION', 'DECRYPTION', 'TRANSFER', 'VOTING', 'OWNER']; const SECURITY_LEVELS: SecurityLevel[] = ['MASTER', 'CRITICAL', 'HIGH', 'MEDIUM']; /** @@ -20,6 +20,9 @@ function getAllowedSecurityLevels(purpose: KeyPurpose, includeMaster = true): Se if (purpose === 'TRANSFER') { return ['CRITICAL']; } + if (purpose === 'ENCRYPTION' || purpose === 'DECRYPTION') { + return ['MEDIUM']; + } return includeMaster ? SECURITY_LEVELS : SECURITY_LEVELS.filter(s => s !== 'MASTER'); } diff --git a/src/ui/state.test.ts b/src/ui/state.test.ts new file mode 100644 index 0000000..812f94c --- /dev/null +++ b/src/ui/state.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest'; +import { createInitialState, setMode, updateIdentityKey, updateManageNewKey } from './state.js'; + +describe('setMode("create") state path', () => { + const initial = createInitialState('testnet'); + + it('transitions to configure_keys with mnemonic and all 6 identity keys including ENCRYPTION and DECRYPTION', () => { + const state = setMode(initial, 'create'); + + expect(state.step).toBe('configure_keys'); + expect(state.mode).toBe('create'); + expect(state.mnemonic).toBeTruthy(); + expect(state.identityKeys).toHaveLength(6); + + const purposes = state.identityKeys.map((k) => k.purpose); + expect(purposes).toContain('ENCRYPTION'); + expect(purposes).toContain('DECRYPTION'); + }); +}); + +describe('updateIdentityKey security level coercion', () => { + const state = setMode(createInitialState('testnet'), 'create'); + + it('coerces DECRYPTION purpose to MEDIUM security level', () => { + const decKey = state.identityKeys.find(k => k.purpose === 'DECRYPTION')!; + const updated = updateIdentityKey(state, decKey.id, { securityLevel: 'CRITICAL' }); + const key = updated.identityKeys.find(k => k.id === decKey.id)!; + expect(key.securityLevel).toBe('MEDIUM'); + }); + + it('coerces ENCRYPTION purpose to MEDIUM security level', () => { + const encKey = state.identityKeys.find(k => k.purpose === 'ENCRYPTION')!; + const updated = updateIdentityKey(state, encKey.id, { securityLevel: 'HIGH' }); + const key = updated.identityKeys.find(k => k.id === encKey.id)!; + expect(key.securityLevel).toBe('MEDIUM'); + }); + + it('coerces security level when purpose is changed to DECRYPTION', () => { + const authKey = state.identityKeys.find(k => k.purpose === 'AUTHENTICATION' && k.securityLevel === 'HIGH')!; + const updated = updateIdentityKey(state, authKey.id, { purpose: 'DECRYPTION' }); + const key = updated.identityKeys.find(k => k.id === authKey.id)!; + expect(key.purpose).toBe('DECRYPTION'); + expect(key.securityLevel).toBe('MEDIUM'); + }); +}); + +describe('updateManageNewKey security level coercion', () => { + it('coerces DECRYPTION purpose to MEDIUM security level', () => { + const state: any = { + manageKeysToAdd: [{ + tempId: 'test-1', + purpose: 'DECRYPTION', + securityLevel: 'CRITICAL', + keyType: 'ECDSA_SECP256K1', + name: 'Test', + }], + }; + const updated = updateManageNewKey(state, 'test-1', { securityLevel: 'HIGH' }); + expect(updated.manageKeysToAdd![0].securityLevel).toBe('MEDIUM'); + }); + + it('coerces security level when purpose is changed to ENCRYPTION', () => { + const state: any = { + manageKeysToAdd: [{ + tempId: 'test-1', + purpose: 'AUTHENTICATION', + securityLevel: 'HIGH', + keyType: 'ECDSA_SECP256K1', + name: 'Test', + }], + }; + const updated = updateManageNewKey(state, 'test-1', { purpose: 'ENCRYPTION' }); + expect(updated.manageKeysToAdd![0].securityLevel).toBe('MEDIUM'); + }); +}); diff --git a/src/ui/state.ts b/src/ui/state.ts index 43c7983..1392667 100644 --- a/src/ui/state.ts +++ b/src/ui/state.ts @@ -215,6 +215,11 @@ export function updateIdentityKey( effectiveSecurityLevel = 'CRITICAL'; } + // ENCRYPTION and DECRYPTION purposes only allow MEDIUM security level + if ((effectivePurpose === 'ENCRYPTION' || effectivePurpose === 'DECRYPTION') && effectiveSecurityLevel !== 'MEDIUM') { + effectiveSecurityLevel = 'MEDIUM'; + } + // If keyType changed, regenerate with new type using HD derivation if (updates.keyType && updates.keyType !== key.keyType) { return generateIdentityKeyFromMnemonic( @@ -939,6 +944,11 @@ export function updateManageNewKey( effectiveSecurityLevel = 'CRITICAL'; } + // ENCRYPTION and DECRYPTION purposes only allow MEDIUM security level + if ((effectivePurpose === 'ENCRYPTION' || effectivePurpose === 'DECRYPTION') && effectiveSecurityLevel !== 'MEDIUM') { + effectiveSecurityLevel = 'MEDIUM'; + } + return { ...k, ...updates,