Skip to content

Commit e89b64b

Browse files
committed
Fix pin enable/disable while in duress mode
Completely fake enable and disable pin settings stash by looking at the duress stash within `localUser` reducer and respecting PinDisabledError that is thrown in `loginWithPIN`.
1 parent 25cfbcb commit e89b64b

File tree

8 files changed

+244
-70
lines changed

8 files changed

+244
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- fixed: Disabling pin-login while in duress mode disables pin-login for the main login.
56
- fixed: `loginWithPassword` bug disabled duress mode when doing an online login.
67

78
## 2.27.4 (2025-05-13)

src/core/context/context-api.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { checkPasswordRules, fixUsername } from '../../client-side'
44
import {
55
asChallengeErrorPayload,
66
asMaybePasswordError,
7-
asMaybePinDisabledError,
87
EdgeAccount,
98
EdgeAccountOptions,
109
EdgeContext,
@@ -327,10 +326,7 @@ export function makeContextApi(ai: ApiInput): EdgeContext {
327326
: await loginMainAccount(stashTree, mainStash)
328327
} catch (error) {
329328
// If the error is not a failed login, rethrow it:
330-
if (
331-
asMaybePasswordError(error) == null &&
332-
asMaybePinDisabledError(error) == null
333-
) {
329+
if (asMaybePasswordError(error) == null) {
334330
throw error
335331
}
336332
const account = inDuressMode

src/core/login/login-reducer.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { RootState } from '../root-reducer'
88
import { searchTree } from './login'
99
import { LoginStash } from './login-stash'
1010
import { WalletInfoFullMap } from './login-types'
11-
import { findPin2Stash } from './pin2'
11+
import { findPin2Stash, findPin2StashDuress } from './pin2'
1212

1313
export interface LoginState {
1414
readonly apiKey: string
@@ -50,7 +50,12 @@ export const login = buildReducer<LoginState, RootAction, RootState>({
5050
const keyLoginEnabled =
5151
stash != null &&
5252
(stash.passwordAuthBox != null || stash.loginAuthBox != null)
53-
const pin2Stash = findPin2Stash(stashTree, appId)
53+
54+
// This allows us to lie about PIN being enabled or disabled while in
55+
// duress mode!
56+
const pin2Stash = clientInfo.duressEnabled
57+
? findPin2StashDuress(stashTree, appId)
58+
: findPin2Stash(stashTree, appId)
5459

5560
return {
5661
keyLoginEnabled,

src/core/login/login-stash.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export interface LoginStash {
6161
// PIN v2 login:
6262
pin2Key?: Uint8Array
6363
pin2TextBox?: EdgeBox
64+
pinEnabledLie?: boolean
6465

6566
// Recovery v2 login:
6667
recovery2Key?: Uint8Array
@@ -84,11 +85,13 @@ export async function loadStashes(
8485
const paths = await disklet.list('logins').then(justFiles)
8586
for (const path of paths) {
8687
try {
87-
out.push(asLoginStash(JSON.parse(await disklet.getText(path))))
88+
const stash = asLoginStash(JSON.parse(await disklet.getText(path)))
89+
out.push(stash)
8890
} catch (error: unknown) {
8991
log.error(`Could not load ${path}: ${String(error)}`)
9092
}
9193
}
94+
9295
return out
9396
}
9497

src/core/login/login.ts

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -436,34 +436,6 @@ export async function applyKit(
436436
return newStashTree
437437
}
438438

439-
/**
440-
* Applies changes to a login, but only temporarily (locally).
441-
* This is used for duress account changes which need to pretend to make real
442-
* changes to a login, only for them to be reverted once the device synchronizes
443-
* with the login server
444-
*/
445-
export async function applyKitTemporarily(
446-
ai: ApiInput,
447-
kit: LoginKit
448-
): Promise<LoginStash> {
449-
const { stashTree } = getStashById(ai, kit.loginId)
450-
451-
const newStashTree = updateTree<LoginStash, LoginStash>(
452-
stashTree,
453-
stash => verifyData(stash.loginId, kit.loginId),
454-
stash => ({
455-
...stash,
456-
...kit.stash,
457-
children: softCat(stash.children, kit.stash.children),
458-
keyBoxes: softCat(stash.keyBoxes, kit.stash.keyBoxes)
459-
}),
460-
(stash, children) => ({ ...stash, children })
461-
)
462-
await saveStash(ai, newStashTree)
463-
464-
return newStashTree
465-
}
466-
467439
/**
468440
* Applies an array of kits to a login, one after another.
469441
* We can't use `Promise.all`, since `applyKit` doesn't handle
@@ -481,19 +453,6 @@ export async function applyKits(
481453
}
482454
}
483455

484-
/**
485-
* Applies an array of kits to a login, _temporarily_ (locally).
486-
*/
487-
export async function applyKitsTemporarily(
488-
ai: ApiInput,
489-
kits: Array<LoginKit | undefined>
490-
): Promise<void> {
491-
for (const kit of kits) {
492-
if (kit == null) continue
493-
await applyKitTemporarily(ai, kit)
494-
}
495-
}
496-
497456
/**
498457
* Refreshes a login with data from the server.
499458
*/

src/core/login/pin2.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,7 @@ import { decrypt, encrypt } from '../../util/crypto/crypto'
1212
import { hmacSha256 } from '../../util/crypto/hashes'
1313
import { utf8 } from '../../util/encoding'
1414
import { ApiInput } from '../root-pixie'
15-
import {
16-
applyKits,
17-
applyKitsTemporarily,
18-
searchTree,
19-
serverLogin
20-
} from './login'
15+
import { applyKits, searchTree, serverLogin } from './login'
2116
import { loginFetch } from './login-fetch'
2217
import { getStashById } from './login-selectors'
2318
import { LoginStash } from './login-stash'
@@ -48,6 +43,21 @@ export function findPin2Stash(
4843
if (stash?.pin2Key != null) return stash
4944
}
5045

46+
/**
47+
* Returns a copy of the PIN login key if one exists on the local device and
48+
* the app id matches the duress appId.
49+
*/
50+
export function findPin2StashDuress(
51+
stashTree: LoginStash,
52+
appId: string
53+
): LoginStash | undefined {
54+
const duressAppId = appId.endsWith('.duress') ? appId : appId + '.duress'
55+
if (stashTree.pin2Key != null && duressAppId === stashTree.appId)
56+
return stashTree
57+
const stash = searchTree(stashTree, stash => stash.appId === duressAppId)
58+
if (stash?.pin2Key != null) return stash
59+
}
60+
5161
/**
5262
* Logs a user in using their PIN.
5363
* @return A `Promise` for the new root login.
@@ -100,16 +110,24 @@ export async function changePin(
100110

101111
// Deleting PIN logins while in duress account should delete PIN locally for
102112
// all nodes:
103-
if (inDuressMode && !forDuressAccount && !enableLogin) {
104-
if (pin == null) {
105-
await applyKitsTemporarily(ai, makeDeletePin2Kits(loginTree))
113+
if (inDuressMode && !forDuressAccount) {
114+
if (enableLogin) {
115+
if (pin != null) {
116+
await applyKits(
117+
ai,
118+
sessionKey,
119+
makeChangePin2Kits(ai, loginTree, username, pin, enableLogin, true)
120+
)
121+
}
106122
} else {
107-
await applyKitsTemporarily(ai, [
108-
// Delete for other apps:
109-
...makeDeletePin2Kits(loginTree, false),
110-
// Change PIN for duress app:
111-
...makeChangePin2Kits(ai, loginTree, username, pin, enableLogin, true)
112-
])
123+
if (pin != null) {
124+
// Change and disable PIN for duress app for real:
125+
await applyKits(
126+
ai,
127+
sessionKey,
128+
makeChangePin2Kits(ai, loginTree, username, pin, enableLogin, true)
129+
)
130+
}
113131
}
114132
return
115133
}

test/core/account/account.test.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ describe('account', function () {
368368
).deep.include.members([{ username: 'js test 0', pinLoginEnabled: false }])
369369
})
370370

371-
it('disable pin while in duress account is temporary', async function () {
371+
it('disable pin while in duress account is not temporary', async function () {
372372
const world = await makeFakeEdgeWorld([fakeUser], quiet)
373373
const context = await world.makeEdgeContext(contextOptions)
374374

@@ -384,10 +384,31 @@ describe('account', function () {
384384
await duressAccount.logout()
385385
// Forget account:
386386
await context.forgetAccount(account.rootLoginId)
387+
387388
// Login with password:
388-
await context.loginWithPassword(fakeUser.username, fakeUser.password, {
389-
otpKey: 'HELLO'
390-
})
389+
const topicAccount = await context.loginWithPassword(
390+
fakeUser.username,
391+
fakeUser.password,
392+
{
393+
otpKey: 'HELLO'
394+
}
395+
)
396+
// Pin should be disabled for account because it is still in duress mode:
397+
expect(
398+
context.localUsers.map(({ pinLoginEnabled, username }) => ({
399+
pinLoginEnabled,
400+
username
401+
}))
402+
).deep.include.members([
403+
{ username: fakeUser.username.toLowerCase(), pinLoginEnabled: false }
404+
])
405+
406+
await topicAccount.changePin({ enableLogin: true })
407+
await topicAccount.logout()
408+
409+
// Login with non-duress PIN:
410+
await context.loginWithPIN(fakeUser.username, fakeUser.pin)
411+
391412
// Pin should be enabled for account:
392413
expect(
393414
context.localUsers.map(({ pinLoginEnabled, username }) => ({

0 commit comments

Comments
 (0)