From 727089b7891b017491390f1e38770eccf677fdd3 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Tue, 6 May 2025 16:34:17 -0700 Subject: [PATCH] Support duress mode for `loginWithKey` --- CHANGELOG.md | 1 + src/core/context/context-api.ts | 40 +++++++++++++++++++++++++++------ test/core/login/login.test.ts | 24 ++++++++++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dcf11a7..e5cedb22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - fixed: Made `changePassword` a no-op for duress accounts - fixed: Made `enableOtp`/`disableOtp` a no-op for duress accounts. - fixed: Spoof `changeRecovery` for duress account. +- fixed: Support duress mode when logging in with `loginWithKey` (biometric login). - fixed: Unintentional pin timeouts caused by duress mode pin-logins. ## 2.27.1 (2025-04-30) diff --git a/src/core/context/context-api.ts b/src/core/context/context-api.ts index 141f26fc..ba17c79b 100644 --- a/src/core/context/context-api.ts +++ b/src/core/context/context-api.ts @@ -136,6 +136,7 @@ export function makeContextApi(ai: ApiInput): EdgeContext { opts: EdgeAccountOptions & { useLoginId?: boolean } = {} ): Promise { const { now = new Date(), useLoginId = false } = opts + const inDuressMode = ai.props.state.clientInfo.duressEnabled const stashTree = useLoginId ? getStashById(ai, base58.parse(usernameOrLoginId)).stashTree @@ -148,13 +149,35 @@ export function makeContextApi(ai: ApiInput): EdgeContext { if (appStash == null) { throw new Error(`Cannot find requested appId: "${appId}"`) } - const sessionKey: SessionKey = { - loginId: appStash.loginId, - loginKey: base58.parse(loginKey) - } - // Verify that the provided key works for decryption: - makeAuthJson(stashTree, sessionKey) + let sessionKey: SessionKey + try { + sessionKey = { + loginId: appStash.loginId, + loginKey: base58.parse(loginKey) + } + + // Verify that the provided key works for decryption: + makeAuthJson(stashTree, sessionKey) + } catch (error) { + if (error instanceof Error && error.message === 'Invalid checksum') { + const duressStash = searchTree(stashTree, stash => + stash.appId.endsWith('.duress') + ) + if (duressStash == null) { + throw error + } + sessionKey = { + loginId: duressStash.loginId, + loginKey: base58.parse(loginKey) + } + + // Verify that the provided key works for decryption: + makeAuthJson(stashTree, sessionKey) + } else { + throw error + } + } // Save the date: stashTree.lastLogin = now @@ -163,7 +186,10 @@ export function makeContextApi(ai: ApiInput): EdgeContext { // Since we logged in offline, update the stash in the background: syncLogin(ai, sessionKey).catch(error => ai.props.onError(error)) - return await makeAccount(ai, sessionKey, 'keyLogin', opts) + return await makeAccount(ai, sessionKey, 'keyLogin', { + ...opts, + duressMode: inDuressMode + }) }, async loginWithPassword( diff --git a/test/core/login/login.test.ts b/test/core/login/login.test.ts index de8c06ed..01308279 100644 --- a/test/core/login/login.test.ts +++ b/test/core/login/login.test.ts @@ -523,6 +523,30 @@ describe('duress', function () { expect(topicAccount.isDuressAccount).equals(true) }) + it('persist duress mode when using loginWithKey', async function () { + const world = await makeFakeEdgeWorld([fakeUser], quiet) + const context = await world.makeEdgeContext(contextOptions) + const account = await context.loginWithPIN(fakeUser.username, fakeUser.pin) + const loginKey = await account.getLoginKey() + await account.changePin({ pin: '0000', forDuressAccount: true }) + const duressAccount = await context.loginWithPIN(fakeUser.username, '0000') + + // Login to the main account using the loginKey: + const topicAccount = await context.loginWithKey(fakeUser.username, loginKey) + expect(topicAccount.isDuressAccount).equals(true) + + // Get the loginKey for the duress account: + const duressLoginKey = await duressAccount.getLoginKey() + expect(duressLoginKey).not.equals(loginKey) + + // Login to the duress account using the duressLoginKey: + const topicAccount2 = await context.loginWithKey( + fakeUser.username, + duressLoginKey + ) + expect(topicAccount2.isDuressAccount).equals(true) + }) + it('check password', async function () { const world = await makeFakeEdgeWorld([fakeUser], quiet) const context = await world.makeEdgeContext(contextOptions)