Skip to content

Sam/duress check pin #661

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 1 commit into from
May 22, 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
9 changes: 6 additions & 3 deletions src/core/account/account-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,15 +330,18 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount {
return await checkPassword(ai, stashTree, password, loginKey)
},

async checkPin(pin: string): Promise<boolean> {
async checkPin(
pin: string,
opts: { forDuressAccount?: boolean } = {}
): Promise<boolean> {
lockdown()
const { login, loginTree } = accountState()

// Try to check the PIN locally, then fall back on the server:
if (login.pin != null) {
if (login.pin != null && opts.forDuressAccount == null) {
return pin === login.pin
} else {
return await checkPin2(ai, loginTree, pin)
return await checkPin2(ai, loginTree, pin, opts.forDuressAccount)
}
},

Expand Down
29 changes: 24 additions & 5 deletions src/core/login/pin2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,18 +161,37 @@ export async function changePin(

/**
* Returns true if the given pin is correct.
*
* @param loginTree - The root login tree to check the pin for.
* @param pin - The pin to check.
* @param forDuressAccount - If true, check the pin for the duress account.
* @return A `Promise` for the result of the pin check.
*/
export async function checkPin2(
ai: ApiInput,
login: LoginTree,
pin: string
loginTree: LoginTree,
pin: string,
forDuressAccount?: boolean
): Promise<boolean> {
const { appId, loginId, username } = login
const { loginId, username } = loginTree
if (username == null) return false

// This will never be `.duress.duress` because `loginTree` is the root.
const appId =
forDuressAccount === true ? loginTree.appId + '.duress' : loginTree.appId

// Special case: checking pin against theoretical, non-existent
// '.duress.duress' account should always be faked as `false`.
if (ai.props.state.clientInfo.duressEnabled) {
return false
}

// Find the stash to use:
const { stashTree } = getStashById(ai, loginId)
const stash = findPin2Stash(stashTree, appId)
const stash =
forDuressAccount === true
? findPin2StashDuress(stashTree, appId)
: findPin2Stash(stashTree, appId)
if (stash == null || stash.pin2Key == null) {
throw new PinDisabledError('No PIN set locally for this account')
}
Expand All @@ -182,7 +201,7 @@ export async function checkPin2(
const request: LoginRequestBody = {
pin2Id: makePin2Id(pin2Key, username),
pin2Auth: makePin2Auth(pin2Key, pin),
otp: getLoginOtp(login)
otp: getLoginOtp(loginTree)
}
return await loginFetch(ai, 'POST', '/v2/login', request).then(
good => true,
Expand Down
5 changes: 4 additions & 1 deletion src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1729,7 +1729,10 @@ export interface EdgeAccount {

// Verify existing credentials:
readonly checkPassword: (password: string) => Promise<boolean>
readonly checkPin: (pin: string) => Promise<boolean>
readonly checkPin: (
pin: string,
opts?: { forDuressAccount?: boolean }
) => Promise<boolean>
readonly getPin: () => Promise<string | undefined>

// Remove credentials:
Expand Down
33 changes: 33 additions & 0 deletions test/core/login/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,39 @@ describe('pin', function () {
expect(await account.checkPin(fakeUser.pin + '!')).equals(false)
})

it('check for duress', 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 })

expect(await account.checkPin(fakeUser.pin)).equals(true)
expect(await account.checkPin('0000', { forDuressAccount: true })).equals(
true
)
expect(await account.checkPin('1111', { forDuressAccount: true })).equals(
false
)
})

it('check for duress while in duress', 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()
const duressAccount = await context.loginWithPIN(fakeUser.username, '0000')

expect(await duressAccount.checkPin('0000')).equals(true)
expect(await duressAccount.checkPin(fakeUser.pin)).equals(false)
expect(
await duressAccount.checkPin('0000', { forDuressAccount: true })
).equals(false)
expect(
await duressAccount.checkPin('1111', { forDuressAccount: true })
).equals(false)
})

it('check duress', async function () {
const world = await makeFakeEdgeWorld([fakeUser], quiet)
const context = await world.makeEdgeContext(contextOptions)
Expand Down
Loading