From 6a075dd5689b1095c79a3ac7bec76cd396d87090 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 18 Jun 2025 14:55:09 -0700 Subject: [PATCH] Fake PIN disable while in duress mode --- src/core/account/account-api.ts | 16 +++++-- src/core/context/context-api.ts | 18 ++++++-- src/core/login/login-reducer.ts | 23 +++++----- src/core/login/login-stash.ts | 1 + src/core/login/login.ts | 12 +++-- src/core/login/pin2.ts | 76 ++++++++++++++++++++++++------- test/core/account/account.test.ts | 4 +- 7 files changed, 107 insertions(+), 43 deletions(-) diff --git a/src/core/account/account-api.ts b/src/core/account/account-api.ts index 3e5f3f15..19a371b8 100644 --- a/src/core/account/account-api.ts +++ b/src/core/account/account-api.ts @@ -261,14 +261,22 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount { const duressAppId = activeAppId.endsWith('.duress') ? activeAppId : activeAppId + '.duress' - // Ensure the duress account exists: - if (forDuressAccount) { + // Fakes for duress mode: + if (this.isDuressAccount) { // Fake duress mode setup if this is a duress account: - if (this.isDuressAccount) { + if (forDuressAccount) { fakeDuressModeSetup = opts.enableLogin ?? opts.pin != null ai.props.dispatch({ type: 'UPDATE_NEXT' }) return '' } + // Fake disable PIN login: + // if (opts.enableLogin === false) { + // // await fakeDisablePin(ai, accountId, opts) + // return '' + // } + } + // Ensure the duress account exists: + if (forDuressAccount) { await ensureAccountExists( ai, accountState().stashTree, @@ -276,7 +284,7 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount { duressAppId ) } - await changePin(ai, accountId, { ...opts, forDuressAccount }) + await changePin(ai, accountId, opts) const login = forDuressAccount ? searchTree(accountState().login, stash => stash.appId === duressAppId) : accountState().login diff --git a/src/core/context/context-api.ts b/src/core/context/context-api.ts index 6335ab55..038732f6 100644 --- a/src/core/context/context-api.ts +++ b/src/core/context/context-api.ts @@ -192,8 +192,9 @@ export function makeContextApi(ai: ApiInput): EdgeContext { return await makeAccount(ai, sessionKey, 'keyLogin', { ...opts, - // We must require that the duress account exists: - duressMode: inDuressMode && duressStash != null + // We must require that the duress account is active. + // Duress account is active if it exists and has a PIN key: + duressMode: inDuressMode && duressStash?.pin2Key != null }) }, @@ -223,11 +224,12 @@ export function makeContextApi(ai: ApiInput): EdgeContext { stash, stash => stash.appId === duressAppId ) - // We may still be in duress mode but not log in into a duress account + // We may still be in duress mode but do not log-in to a duress account // if it does not exist. It's important that we do not disable duress // mode from this routine to make sure other accounts with duress mode // still are protected. - if (duressStash != null) { + // Duress account is active if it exists and has a PIN key: + if (duressStash?.pin2Key != null) { const duressSessionKey = decryptChildKey( stash, sessionKey, @@ -283,6 +285,12 @@ export function makeContextApi(ai: ApiInput): EdgeContext { stashTree: LoginStash, duressStash: LoginStash ): Promise { + if (duressStash.fakePinDisabled === true) { + throw new PinDisabledError( + 'PIN login is not enabled for this account on this device' + ) + } + // Try login with duress account const sessionKey = await loginPin2( ai, @@ -310,7 +318,7 @@ export function makeContextApi(ai: ApiInput): EdgeContext { } // No duress account configured, so just login to the main account: - if (duressStash == null) { + if (duressStash?.pin2Key == null) { // It's important that we don't disable duress mode here because // we want to protect account that have duress mode enabled and only // allow those accounts to suspend duress mode. diff --git a/src/core/login/login-reducer.ts b/src/core/login/login-reducer.ts index 49d534b6..dfb3904a 100644 --- a/src/core/login/login-reducer.ts +++ b/src/core/login/login-reducer.ts @@ -51,17 +51,18 @@ export const login = buildReducer({ stash != null && (stash.passwordAuthBox != null || stash.loginAuthBox != null) - // This allows us to lie about PIN being enabled or disabled while in - // duress mode! - const pin2Stash = clientInfo.duressEnabled - ? // Use the duress stash's PIN settings, but fallback for accounts - // that don't have duress mode setup - findDuressStash(stashTree, appId) ?? findPin2Stash(stashTree, appId) - : // If we're not in duress mode, then do a normal search - findPin2Stash(stashTree, appId) - // If we have found a pin2Stash, or the duress stash we found has - // a pin2Key defined: - const pinLoginEnabled = pin2Stash?.pin2Key != null + // Only look at the duress stash if we're in duress mode: + const duressStash = clientInfo.duressEnabled + ? findDuressStash(stashTree, appId) + : undefined + // Only fake pin disabled if the duress stash is present and has a + // pin2Key: + const fakePinDisabled = + duressStash?.pin2Key != null && duressStash?.fakePinDisabled === true + const pin2Stash = findPin2Stash(stashTree, appId) + // Disable PIN login if we're faking it from the duress stash, or we + // don't have a pin2Key on the account's pin2Stash: + const pinLoginEnabled = !fakePinDisabled && pin2Stash?.pin2Key != null return { keyLoginEnabled, diff --git a/src/core/login/login-stash.ts b/src/core/login/login-stash.ts index 9e297f56..eb4f0287 100644 --- a/src/core/login/login-stash.ts +++ b/src/core/login/login-stash.ts @@ -62,6 +62,7 @@ export interface LoginStash { // PIN v2 login: pin2Key?: Uint8Array pin2TextBox?: EdgeBox + fakePinDisabled?: boolean // Recovery v2 login: recovery2Key?: Uint8Array diff --git a/src/core/login/login.ts b/src/core/login/login.ts index 5b456d1e..f4dc9e6d 100644 --- a/src/core/login/login.ts +++ b/src/core/login/login.ts @@ -415,10 +415,14 @@ export async function applyKit( const { serverMethod = 'POST', serverPath } = kit const { stashTree } = getStashById(ai, kit.loginId) - const childKey = decryptChildKey(stashTree, sessionKey, kit.loginId) - const request = makeAuthJson(stashTree, childKey) - request.data = kit.server - await loginFetch(ai, serverMethod, serverPath, request) + + // Don't make server-side changes if the server path is faked: + if (serverPath !== '') { + const childKey = decryptChildKey(stashTree, sessionKey, kit.loginId) + const request = makeAuthJson(stashTree, childKey) + request.data = kit.server + await loginFetch(ai, serverMethod, serverPath, request) + } const newStashTree = updateTree( stashTree, diff --git a/src/core/login/pin2.ts b/src/core/login/pin2.ts index 00c347e2..4cd2ca92 100644 --- a/src/core/login/pin2.ts +++ b/src/core/login/pin2.ts @@ -96,23 +96,17 @@ export async function changePin( // Deleting PIN logins while in duress account should delete PIN locally for // all nodes: if (isDuressAccount && !forDuressAccount) { - if (enableLogin) { - if (pin != null) { - await applyKits( - ai, - sessionKey, - makeChangePin2Kits(ai, loginTree, username, pin, enableLogin, true) - ) - } - } else { - if (pin != null) { - // Change and disable PIN for duress app for real: - await applyKits( - ai, - sessionKey, - makeChangePin2Kits(ai, loginTree, username, pin, enableLogin, true) - ) - } + await applyKits( + ai, + sessionKey, + makeFakeDisablePinKits(ai, loginTree, username, enableLogin, true) + ) + if (pin != null) { + await applyKits( + ai, + sessionKey, + makeChangePin2Kits(ai, loginTree, username, pin, true, true) + ) } return } @@ -239,6 +233,35 @@ export function makeChangePin2Kits( return out } +export function makeFakeDisablePinKits( + ai: ApiInput, + loginTree: LoginTree, + username: string | undefined, + enableLogin: boolean, + forDuressAccount: boolean +): LoginKit[] { + const out: LoginKit[] = [] + + // Only include pin change if the app id matches the duress account flag: + if (forDuressAccount === loginTree.appId.endsWith('.duress')) { + out.push(makeFakeDisablePinKit(loginTree, enableLogin)) + } + + for (const child of loginTree.children) { + out.push( + ...makeFakeDisablePinKits( + ai, + child, + username, + enableLogin, + forDuressAccount + ) + ) + } + + return out +} + /** * Used when changing the username. * This won't return anything if the PIN is missing. @@ -312,6 +335,25 @@ export function makeChangePin2Kit( } } +/** + * Creates the data needed to attach a PIN to a login. + */ +export function makeFakeDisablePinKit( + login: LoginTree, + enableLogin: boolean +): LoginKit { + const { loginId } = login + + return { + loginId, + server: undefined, + serverPath: '', + stash: { + fakePinDisabled: !enableLogin + } + } +} + /** * Creates the data needed to delete a PIN from a tree of logins. * @param loginTree - The login tree to create the kits for. diff --git a/test/core/account/account.test.ts b/test/core/account/account.test.ts index 2f35dd28..cc56cc02 100644 --- a/test/core/account/account.test.ts +++ b/test/core/account/account.test.ts @@ -368,7 +368,7 @@ describe('account', function () { ).deep.include.members([{ username: 'js test 0', pinLoginEnabled: false }]) }) - it('disable pin while in duress account is not temporary', async function () { + it('disable pin while in duress account is temporary', async function () { const world = await makeFakeEdgeWorld([fakeUser], quiet) const context = await world.makeEdgeContext(contextOptions) @@ -400,7 +400,7 @@ describe('account', function () { username })) ).deep.include.members([ - { username: fakeUser.username.toLowerCase(), pinLoginEnabled: false } + { username: fakeUser.username.toLowerCase(), pinLoginEnabled: true } ]) await topicAccount.changePin({ enableLogin: true })