Skip to content

Sam/duress pin errors #666

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- fixed: Fix change-server reliability issues by fallback to pull-based syncing as a trade-off.
- fixed: Show the correct login errors when duress account is disabled.

## 2.31.0 (2025-06-05)

Expand Down
33 changes: 21 additions & 12 deletions src/core/context/context-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { checkPasswordRules, fixUsername } from '../../client-side'
import {
asChallengeErrorPayload,
asMaybePasswordError,
asMaybePinDisabledError,
EdgeAccount,
EdgeAccountOptions,
EdgeContext,
Expand Down Expand Up @@ -324,21 +325,29 @@ export function makeContextApi(ai: ApiInput): EdgeContext {
return inDuressMode
? await loginDuressAccount(stashTree, duressStash)
: await loginMainAccount(stashTree, mainStash)
} catch (error) {
} catch (originalError) {
// If the error is not a failed login, rethrow it:
if (asMaybePasswordError(error) == null) {
throw error
if (asMaybePasswordError(originalError) == null) {
throw originalError
}
const account = inDuressMode
? await loginMainAccount(stashTree, mainStash)
: await loginDuressAccount(stashTree, duressStash)
// Only Enable/Disable duress mode if account creation was success.
if (inDuressMode) {
await disableDuressMode()
} else {
await enableDuressMode()
try {
const account = inDuressMode
? await loginMainAccount(stashTree, mainStash)
: await loginDuressAccount(stashTree, duressStash)
// Only Enable/Disable duress mode if account creation was success.
if (inDuressMode) {
await disableDuressMode()
} else {
await enableDuressMode()
}
return account
} catch (error) {
// Throw the original error if pin-login is disabled:
if (asMaybePinDisabledError(error) != null) {
throw originalError
}
throw error
}
return account
}
},

Expand Down
234 changes: 142 additions & 92 deletions test/core/login/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,52 +295,66 @@ describe('pin', function () {
expect(successAccount.id).equals(account.id)
})

it('disable pin in duress mode disables pin-login for duress mode', async function () {
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext(contextOptions)
const account = await context.loginWithPIN(fakeUser.username, fakeUser.pin)
await account.changePin({ pin: '0000', forDuressAccount: true })
await account.logout()
describe('disable pin in duress mode', function () {
it('will disable pin-login for duress account', async function () {
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext(contextOptions)
const account = await context.loginWithPIN(
fakeUser.username,
fakeUser.pin
)
await account.changePin({ pin: '0000', forDuressAccount: true })
await account.logout()

// Disable PIN login:
const duressAccount = await context.loginWithPIN(fakeUser.username, '0000')
await duressAccount.changePin({ enableLogin: false })
await expectRejection(
context.loginWithPIN(fakeUser.username, '0000'),
'PinDisabledError: PIN login is not enabled for this account on this device'
)
})
// Disable PIN login:
const duressAccount = await context.loginWithPIN(
fakeUser.username,
'0000'
)
await duressAccount.changePin({ enableLogin: false })
await expectRejection(
context.loginWithPIN(fakeUser.username, '0000'),
'PinDisabledError: PIN login is not enabled for this account on this device'
)
})

it('disable pin in duress mode disables pin-login for local user and disables pin-login for main account', async function () {
this.timeout(30000)
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext(contextOptions)
const account = await context.loginWithPIN(fakeUser.username, fakeUser.pin)
await account.changePin({ pin: '0000', forDuressAccount: true })
await account.logout()
it('will disable pin-login for local user and main account', async function () {
this.timeout(30000)
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext(contextOptions)
const account = await context.loginWithPIN(
fakeUser.username,
fakeUser.pin
)
await account.changePin({ pin: '0000', forDuressAccount: true })
await account.logout()

// Disable PIN login:
const duressAccount = await context.loginWithPIN(fakeUser.username, '0000')
await duressAccount.changePin({ enableLogin: false })
// Disable PIN login:
const duressAccount = await context.loginWithPIN(
fakeUser.username,
'0000'
)
await duressAccount.changePin({ enableLogin: false })

// Check the PIN login is disabled:
expect(
context.localUsers.map(user => ({
username: user.username,
pinLoginEnabled: user.pinLoginEnabled
}))
).deep.include.members([
{
username: 'js test 0',
pinLoginEnabled: false
}
])
// Check the PIN login is disabled:
expect(
context.localUsers.map(user => ({
username: user.username,
pinLoginEnabled: user.pinLoginEnabled
}))
).deep.include.members([
{
username: 'js test 0',
pinLoginEnabled: false
}
])

// Pin is disabled for main account:
await expectRejection(
context.loginWithPIN(fakeUser.username, fakeUser.pin),
'PinDisabledError: PIN login is not enabled for this account on this device'
)
// Pin is disabled for main account:
await expectRejection(
context.loginWithPIN(fakeUser.username, fakeUser.pin),
'PinDisabledError: PIN login is not enabled for this account on this device'
)
})
})

describe('disable pin in duress mode disables pin-login for _only_ its local user', function () {
Expand Down Expand Up @@ -548,51 +562,59 @@ describe('pin', function () {
])
})

it('exiting duress mode with a disable pin-login in duress account re-enables pin-login for local user but not for the duress account', async function () {
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext(contextOptions)
// Create a second account with duress setup:
const otherAccount = await context.createAccount({
username: 'other-account',
pin: '1111'
})
await otherAccount.changePin({ pin: '0000', forDuressAccount: true })
await otherAccount.logout()
describe('when exiting duress mode from a duress account that has disabled pin-login', function () {
it('will re-enable pin-login for local user but not for the duress account', async function () {
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext(contextOptions)
// Create a second account with duress setup:
const otherAccount = await context.createAccount({
username: 'other-account',
pin: '1111'
})
await otherAccount.changePin({ pin: '0000', forDuressAccount: true })
await otherAccount.logout()

// Setup duress on main account:
const account = await context.loginWithPIN(fakeUser.username, fakeUser.pin)
await account.changePin({ pin: '0000', forDuressAccount: true })
await account.logout()
// Setup duress on main account:
const account = await context.loginWithPIN(
fakeUser.username,
fakeUser.pin
)
await account.changePin({ pin: '0000', forDuressAccount: true })
await account.logout()

// Disable PIN login for duress account on the main account:
const duressAccount = await context.loginWithPIN(fakeUser.username, '0000')
await duressAccount.changePin({ enableLogin: false })
await duressAccount.logout()
// Disable PIN login for duress account on the main account:
const duressAccount = await context.loginWithPIN(
fakeUser.username,
'0000'
)
await duressAccount.changePin({ enableLogin: false })
await duressAccount.logout()

// Login/logout to other account in non-duress mode to disable duress mode:
await (await context.loginWithPIN('other-account', '1111')).logout()
// Login/logout to other account in non-duress mode to disable duress mode:
await (await context.loginWithPIN('other-account', '1111')).logout()

// Check the PIN login is enabled for local user:
expect(
context.localUsers.map(user => ({
username: user.username,
pinLoginEnabled: user.pinLoginEnabled
}))
).deep.include.members([
{
username: 'js test 0',
pinLoginEnabled: true
}
])
// Check the PIN login is enabled for local user:
expect(
context.localUsers.map(user => ({
username: user.username,
pinLoginEnabled: user.pinLoginEnabled
}))
).deep.include.members([
{
username: 'js test 0',
pinLoginEnabled: true
}
])

// Pin is disabled for duress account:
await expectRejection(
context.loginWithPIN(fakeUser.username, '0000'),
'PinDisabledError: PIN login is not enabled for this account on this device'
)
// Pin is disabled for duress account:
await expectRejection(
context.loginWithPIN(fakeUser.username, '0000'),
'PasswordError: Invalid password'
)

// Pin is enabled for main account:
await context.loginWithPIN(fakeUser.username, fakeUser.pin)
// Pin is enabled for main account:
await context.loginWithPIN(fakeUser.username, fakeUser.pin)
})
})

it('checkPin still works after disabling pin-login while in duress mode', async function () {
Expand Down Expand Up @@ -706,19 +728,47 @@ describe('pin', function () {
expect(successAccount.id).equals(account.id)
})

it('disable duress does not disable pin-login', async function () {
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext(contextOptions)
expect(context.localUsers[0].pinLoginEnabled).equals(true)
// Setup duress mode:
const account = await context.loginWithPIN(fakeUser.username, fakeUser.pin)
await account.changePin({ pin: '0000', forDuressAccount: true })
// Disable duress mode:
await account.changePin({
enableLogin: false,
forDuressAccount: true
describe('disable duress mode', function () {
it('will not disable pin-login for main account', async function () {
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext(contextOptions)
expect(context.localUsers[0].pinLoginEnabled).equals(true)
// Setup duress mode:
const account = await context.loginWithPIN(
fakeUser.username,
fakeUser.pin
)
await account.changePin({ pin: '0000', forDuressAccount: true })
// Disable duress mode:
await account.changePin({
enableLogin: false,
forDuressAccount: true
})
expect(context.localUsers[0].pinLoginEnabled).equals(true)
})

it('will not show duress-account pin errors', async function () {
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext(contextOptions)
expect(context.localUsers[0].pinLoginEnabled).equals(true)
// Setup duress mode:
const account = await context.loginWithPIN(
fakeUser.username,
fakeUser.pin
)
await account.changePin({ pin: '0000', forDuressAccount: true })
// Disable duress mode:
await account.changePin({
enableLogin: false,
forDuressAccount: true
})
await account.logout()

await expectRejection(
context.loginWithPIN(fakeUser.username, '4444'),
'PasswordError: Invalid password'
)
})
expect(context.localUsers[0].pinLoginEnabled).equals(true)
})

it('check', async function () {
Expand Down
Loading