9af036ca50b9

test: adapt recovery-words + recovery.adversarial suites to LV-4 GET_RECOVERY_WORDS password re-auth; full suite green (3141)

time
2026-06-10 03:08 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
audit fix integration
parent
56b407befb6e
2 files src/background/recovery-words.test.ts+28 −28src/background/recovery.adversarial.test.ts+6 −1
src/background/recovery-words.test.ts+28 −28
@@ -116,14 +116,14 @@116116     beforeEach(async () => { await setupUnlockedVault(); });117117 118118     it('returns 12 words', async () => {119-      const result = await dispatch({ type: 'GET_RECOVERY_WORDS' });119+      const result = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });120120       expect(result.ok).toBe(true);121121       const words = (result as { data: string[] }).data;122122       expect(words).toHaveLength(12);123123     });124124 125125     it('all words are from the BIP39 word list', async () => {126-      const result = await dispatch({ type: 'GET_RECOVERY_WORDS' });126+      const result = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });127127       const words = (result as { data: string[] }).data;128128       for (const word of words) {129129         expect(WORD_LIST).toContain(word);@@ -131,14 +131,14 @@131131     });132132 133133     it('returns same words on subsequent calls (deterministic)', async () => {134-      const r1 = await dispatch({ type: 'GET_RECOVERY_WORDS' });135-      const r2 = await dispatch({ type: 'GET_RECOVERY_WORDS' });134+      const r1 = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });135+      const r2 = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });136136       expect((r1 as { data: string[] }).data).toEqual((r2 as { data: string[] }).data);137137     });138138 139139     it('returns error when vault is locked', async () => {140140       await lockVault();141-      const result = await dispatch({ type: 'GET_RECOVERY_WORDS' });141+      const result = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });142142       expect(result.ok).toBe(false);143143     });144144   });@@ -147,7 +147,7 @@147147     beforeEach(async () => { await setupUnlockedVault(); });148148 149149     it('returns no invalid words for correct list', async () => {150-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });150+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });151151       const words = (r as { data: string[] }).data;152152       const result = await dispatch({153153         type: 'VALIDATE_RECOVERY_WORDS',@@ -169,7 +169,7 @@169169     });170170 171171     it('is case-insensitive for validation', async () => {172-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });172+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });173173       const words = (r as { data: string[] }).data;174174       const uppercased = words.map((w: string) => w.toUpperCase());175175       const result = await dispatch({@@ -184,7 +184,7 @@184184   describe('RECOVER_WITH_WORDS handler', () => {185185     it('recovery succeeds with correct words and new passphrase', async () => {186186       await setupUnlockedVault();187-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });187+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });188188       const words = (r as { data: string[] }).data;189189       await lockVault();190190 @@ -211,7 +211,7 @@211211 212212     it('fails with partial word list (fewer than 12)', async () => {213213       await setupUnlockedVault();214-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });214+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });215215       const words = (r as { data: string[] }).data;216216       await lockVault();217217       const partial = words.slice(0, 6);@@ -224,7 +224,7 @@224224 225225     it('new passphrase works after recovery', async () => {226226       await setupUnlockedVault();227-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });227+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });228228       const words = (r as { data: string[] }).data;229229       await lockVault();230230       await dispatch({@@ -241,7 +241,7 @@241241 242242     it('old passphrase does not work after recovery', async () => {243243       await setupUnlockedVault();244-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });244+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });245245       const words = (r as { data: string[] }).data;246246       await lockVault();247247       await dispatch({@@ -258,7 +258,7 @@258258 259259     it('preserves recovery escrow fields after recovery password rewrap', async () => {260260       await setupUnlockedVault();261-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });261+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });262262       const words = (r as { data: string[] }).data;263263       const before = { ...rawSlotA() };264264       await lockVault();@@ -280,7 +280,7 @@280280 281281     it('same recovery words succeed on a second recovery', async () => {282282       await setupUnlockedVault();283-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });283+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });284284       const words = (r as { data: string[] }).data;285285 286286       await lockVault();@@ -307,7 +307,7 @@307307 308308     it('same recovery words succeed on a third recovery', async () => {309309       await setupUnlockedVault();310-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });310+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });311311       const words = (r as { data: string[] }).data;312312 313313       for (let i = 0; i < 3; i += 1) {@@ -322,7 +322,7 @@322322 323323     it('vault remains usable after recovery', async () => {324324       await setupUnlockedVault();325-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });325+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });326326       const words = (r as { data: string[] }).data;327327 328328       await lockVault();@@ -345,19 +345,19 @@345345     // every password change would silently brick recovery.346346     it('recovery words are preserved across CHANGE_PASSWORD', async () => {347347       await setupUnlockedVault();348-      const before = (await dispatch({ type: 'GET_RECOVERY_WORDS' }) as { data: string[] }).data;348+      const before = (await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } }) as { data: string[] }).data;349349       await dispatch({350350         type: 'CHANGE_PASSWORD',351351         payload: { currentPassword: 'test-pass-12345', newPassword: 'new-pass-67890' },352352       });353-      const after = (await dispatch({ type: 'GET_RECOVERY_WORDS' }) as { data: string[] }).data;353+      const after = (await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'new-pass-67890' } }) as { data: string[] }).data;354354       expect(after).toEqual(before);355355     });356356 357357     it('recovery is not Pro-gated', async () => {358358       await setupUnlockedVault();359359       await mockNoPro();360-      const r = await dispatch({ type: 'GET_RECOVERY_WORDS' });360+      const r = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });361361       const words = (r as { data: string[] }).data;362362       await lockVault();363363       const result = await dispatch({@@ -374,7 +374,7 @@374374   describe('changePassword preserves root key and recovery words', () => {375375     it('recovery words work to unlock after password change', async () => {376376       await setupUnlockedVault();377-      const words = (await dispatch({ type: 'GET_RECOVERY_WORDS' }) as { data: string[] }).data;377+      const words = (await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } }) as { data: string[] }).data;378378       const ch = await dispatch({379379         type: 'CHANGE_PASSWORD',380380         payload: { currentPassword: 'test-pass-12345', newPassword: 'new-pass-aaa' },@@ -390,12 +390,12 @@390390 391391     it('recovery words are identical before and after password change', async () => {392392       await setupUnlockedVault();393-      const before = (await dispatch({ type: 'GET_RECOVERY_WORDS' }) as { data: string[] }).data;393+      const before = (await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } }) as { data: string[] }).data;394394       await dispatch({395395         type: 'CHANGE_PASSWORD',396396         payload: { currentPassword: 'test-pass-12345', newPassword: 'another-pass-bbb' },397397       });398-      const after = (await dispatch({ type: 'GET_RECOVERY_WORDS' }) as { data: string[] }).data;398+      const after = (await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'another-pass-bbb' } }) as { data: string[] }).data;399399       expect(after).toEqual(before);400400     });401401 @@ -429,7 +429,7 @@429429 430430     it('GET_RECOVERY_WORDS still works after a password change + lock + unlock', async () => {431431       await setupUnlockedVault();432-      const before = (await dispatch({ type: 'GET_RECOVERY_WORDS' }) as { data: string[] }).data;432+      const before = (await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } }) as { data: string[] }).data;433433       await dispatch({434434         type: 'CHANGE_PASSWORD',435435         payload: { currentPassword: 'test-pass-12345', newPassword: 'pass-after-cycle' },@@ -439,7 +439,7 @@439439         type: 'UNLOCK',440440         payload: { password: 'pass-after-cycle' },441441       });442-      const after = (await dispatch({ type: 'GET_RECOVERY_WORDS' }) as { data: string[] }).data;442+      const after = (await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'pass-after-cycle' } }) as { data: string[] }).data;443443       expect(after).toEqual(before);444444     });445445   });@@ -450,7 +450,7 @@450450   describe('deriveRootKeyFromDerivedKey and recovery persistence', () => {451451     it('GET_RECOVERY_WORDS works after lock and re-unlock', async () => {452452       await setupUnlockedVault();453-      const r1 = await dispatch({ type: 'GET_RECOVERY_WORDS' });453+      const r1 = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });454454       const words1 = (r1 as { data: string[] }).data;455455       expect(words1).toHaveLength(12);456456       await lockVault();@@ -459,14 +459,14 @@459459         payload: { password: 'test-pass-12345' },460460       });461461       expect(unlocked.ok).toBe(true);462-      const r2 = await dispatch({ type: 'GET_RECOVERY_WORDS' });462+      const r2 = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });463463       const words2 = (r2 as { data: string[] }).data;464464       expect(words2).toEqual(words1);465465     });466466 467467     it('GET_RECOVERY_WORDS works after multiple lock/unlock cycles', async () => {468468       await setupUnlockedVault();469-      const original = (await dispatch({ type: 'GET_RECOVERY_WORDS' }) as { data: string[] }).data;469+      const original = (await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } }) as { data: string[] }).data;470470       for (let i = 0; i < 5; i++) {471471         await lockVault();472472         await dispatch({@@ -474,7 +474,7 @@474474           payload: { password: 'test-pass-12345' },475475         });476476       }477-      const after = (await dispatch({ type: 'GET_RECOVERY_WORDS' }) as { data: string[] }).data;477+      const after = (await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } }) as { data: string[] }).data;478478       expect(after).toEqual(original);479479     });480480 @@ -499,7 +499,7 @@499499       });500500       expect(unlocked.ok).toBe(true);501501 502-      const result = await dispatch({ type: 'GET_RECOVERY_WORDS' });502+      const result = await dispatch({ type: 'GET_RECOVERY_WORDS', payload: { password: 'test-pass-12345' } });503503       expect(result.ok).toBe(true);504504       const words = (result as { data: string[] }).data;505505       expect(words).toHaveLength(12);

view file

src/background/recovery.adversarial.test.ts+6 −1
@@ -133,7 +133,12 @@133133   }134134 135135   async function getWords(): Promise<string[]> {136-    const r = await dispatch({ type: "GET_RECOVERY_WORDS" });136+    // LV-4: GET_RECOVERY_WORDS now requires a fresh password proof. Every137+    // caller fetches words right after setupUnlockedVault()'s default password.138+    const r = await dispatch({139+      type: "GET_RECOVERY_WORDS",140+      payload: { password: "test-pass-12345" },141+    });137142     expect(r.ok).toBe(true);138143     return (r as { data: string[] }).data;139144   }

view file