Skip to content
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

Better sync #568

Merged
merged 3 commits into from
Oct 10, 2023
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- added: Support optimized login syncing, checking to see if our credentials are up-to-date before performing a periodic login.

## 1.8.0 (2023-10-02)

- added: Export cleaners for server types and testing data types.
Expand Down
7 changes: 5 additions & 2 deletions src/core/account/account-pixie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { makePeriodicTask } from '../../util/periodic-task'
import { snooze } from '../../util/snooze'
import { ExchangeState } from '../exchange/exchange-reducer'
import { syncAccount } from '../login/login'
import { syncLogin } from '../login/login'
import { waitForPlugins } from '../plugins/plugins-selectors'
import { RootProps, toApiInput } from '../root-pixie'
import { addStorageWallet, syncStorageWallet } from '../storage/storage-actions'
Expand Down Expand Up @@ -137,8 +137,11 @@ const accountPixie: TamePixie<AccountProps> = combinePixies({
}

async function doLoginSync(): Promise<void> {
const ai = toApiInput(input)
const { accountId } = input.props
await syncAccount(toApiInput(input), accountId)
if (input.props.state.accounts[accountId] == null) return
const { login, loginTree } = input.props.state.accounts[accountId]
await syncLogin(ai, loginTree, login)
}

// We don't report sync failures, since that could be annoying:
Expand Down
12 changes: 7 additions & 5 deletions src/core/account/lobby-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { EdgeLobby, EdgeLoginRequest } from '../../types/types'
import { shuffle } from '../../util/shuffle'
import { asLobbyLoginPayload } from '../login/edge'
import { fetchLobbyRequest, sendLobbyReply } from '../login/lobby'
import { sanitizeLoginStash, syncAccount } from '../login/login'
import { sanitizeLoginStash, syncLogin } from '../login/login'
import { getStashById } from '../login/login-selectors'
import { ApiInput } from '../root-pixie'
import { ensureAccountExists, findAppLogin } from './account-init'
Expand Down Expand Up @@ -72,10 +72,10 @@ async function approveLoginRequest(
lobbyId: string,
lobbyJson: EdgeLobbyRequest
): Promise<void> {
const { loginTree } = ai.props.state.accounts[accountId]
const { login, loginTree } = ai.props.state.accounts[accountId]

// Ensure that the login object & account repo exist:
await syncAccount(ai, accountId)
await syncLogin(ai, loginTree, login)

const newLoginTree = await ensureAccountExists(ai, loginTree, appId)
const requestedLogin = findAppLogin(newLoginTree, appId)
Expand Down Expand Up @@ -104,11 +104,13 @@ async function approveLoginRequest(

timeout = setTimeout(() => {
timeout = undefined
syncAccount(ai, accountId)
syncLogin(ai, newLoginTree, requestedLogin)
.then(() => {
timeout = setTimeout(() => {
timeout = undefined
syncAccount(ai, accountId).catch(error => ai.props.onError(error))
syncLogin(ai, newLoginTree, requestedLogin).catch(error =>
ai.props.onError(error)
)
}, 20000)
})
.catch(error => ai.props.onError(error))
Expand Down
1 change: 1 addition & 0 deletions src/core/login/login-stash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface LoginStash {
created?: Date
lastLogin?: Date
loginId: Uint8Array
syncToken?: string
userId?: Uint8Array
username?: string

Expand Down
44 changes: 29 additions & 15 deletions src/core/login/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Functions for working with login data in its on-disk format.
*/

import { asBoolean } from 'cleaners'
import { base64 } from 'rfc4648'

import { asLoginPayload } from '../../types/server-cleaners'
Expand Down Expand Up @@ -98,7 +99,8 @@ function applyLoginPayloadInner(
keyBoxes = [],
mnemonicBox,
rootKeyBox,
syncKeyBox
syncKeyBox,
syncToken
} = loginReply

const out: LoginStash = {
Expand All @@ -107,20 +109,21 @@ function applyLoginPayloadInner(
loginId,
loginAuthBox,
userId,
otpKey,
otpKey: otpKey === true ? stash.otpKey : otpKey,
otpResetDate,
otpTimeout,
pendingVouchers,
parentBox,
passwordAuthBox,
passwordAuthSnrp,
passwordBox,
passwordBox: passwordBox === true ? stash.passwordBox : passwordBox,
passwordKeySnrp,
pin2TextBox,
keyBoxes, // We should be more picky about these
mnemonicBox,
rootKeyBox,
syncKeyBox
syncKeyBox,
syncToken
}

// Preserve client-only data:
Expand Down Expand Up @@ -510,15 +513,6 @@ export async function applyKits(
}
}

export async function syncAccount(
ai: ApiInput,
accountId: string
): Promise<void> {
if (ai.props.state.accounts[accountId] == null) return
const { login, loginTree } = ai.props.state.accounts[accountId]
await syncLogin(ai, loginTree, login)
}

/**
* Refreshes a login with data from the server.
*/
Expand All @@ -529,6 +523,21 @@ export async function syncLogin(
): Promise<LoginTree> {
const { stashTree, stash } = getStashById(ai, login.loginId)

// First, hit the fast endpoint to see if we even need to sync:
const { syncToken } = stash
if (syncToken != null) {
try {
const reply = await loginFetch(ai, 'POST', '/v2/sync', {
loginId: stash.loginId,
syncToken
})
if (asBoolean(reply)) return loginTree
} catch (error) {
// We can fall back on a full sync if we fail here.
}
}

// If we do need to sync, prepare for a full login:
const request = makeAuthJson(stashTree, login)
const opts: EdgeAccountOptions = {
// Avoid updating the lastLogin date:
Expand All @@ -548,15 +557,19 @@ export function makeAuthJson(
login: LoginTree
): LoginRequestBody {
const stash = searchTree(stashTree, stash => stash.appId === login.appId)
const { voucherAuth, voucherId } =
stash != null ? stash : { voucherAuth: undefined, voucherId: undefined }
const { syncToken, voucherAuth, voucherId } = stash ?? {
syncToken: undefined,
voucherAuth: undefined,
voucherId: undefined
}

const { loginId, userId, loginAuth, passwordAuth } = login
if (loginAuth != null) {
return {
loginId,
loginAuth,
otp: getLoginOtp(login),
syncToken,
voucherAuth,
voucherId
}
Expand All @@ -566,6 +579,7 @@ export function makeAuthJson(
userId,
passwordAuth,
otp: getLoginOtp(login),
syncToken,
voucherAuth,
voucherId
}
Expand Down
6 changes: 5 additions & 1 deletion src/core/login/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ async function loginPasswordOnline(
request,
async reply => {
const { passwordBox, passwordKeySnrp } = reply
if (passwordBox == null || passwordKeySnrp == null) {
if (
passwordBox == null ||
passwordBox === true ||
passwordKeySnrp == null
) {
throw new Error('Missing data for online password login')
}
const passwordKey = await scrypt(ai, up, passwordKeySnrp)
Expand Down
2 changes: 1 addition & 1 deletion src/core/login/pin2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function loginPin2(
pin2Auth: makePin2Auth(pin2Key, pin)
}
return await serverLogin(ai, stashTree, stash, opts, request, async reply => {
if (reply.pin2Box == null) {
if (reply.pin2Box == null || reply.pin2Box === true) {
throw new Error('Missing data for PIN v2 login')
}
return decrypt(reply.pin2Box, pin2Key)
Expand Down
2 changes: 1 addition & 1 deletion src/core/login/recovery2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export async function loginRecovery2(
opts,
request,
async reply => {
if (reply.recovery2Box == null) {
if (reply.recovery2Box == null || reply.recovery2Box === true) {
throw new Error('Missing data for recovery v2 login')
}
return decrypt(reply.recovery2Box, recovery2Key)
Expand Down
13 changes: 9 additions & 4 deletions src/types/server-cleaners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
asBoolean,
asCodec,
asDate,
asEither,
asNumber,
asObject,
asOptional,
Expand Down Expand Up @@ -135,6 +136,7 @@ export const asLoginRequestBody: Cleaner<LoginRequestBody> = asObject({
challengeId: asOptional(asString),
deviceDescription: asOptional(asString),
otp: asOptional(asString),
syncToken: asOptional(asString),
voucherId: asOptional(asString),
voucherAuth: asOptional(asBase64),

Expand Down Expand Up @@ -269,35 +271,38 @@ export const asLobbyPayload: Cleaner<LobbyPayload> = asObject({
replies: asArray(asEdgeLobbyReply)
})

const asTrue = asValue(true)

export const asLoginPayload: Cleaner<LoginPayload> = asObject({
// Identity:
appId: asString,
created: asDate,
loginId: asBase64,
syncToken: asOptional(asString),

// Nested logins:
children: asOptional(asArray(raw => asLoginPayload(raw))),
parentBox: asOptional(asEdgeBox),

// 2-factor login:
otpKey: asOptional(asBase32),
otpKey: asOptional(asEither(asTrue, asBase32)),
otpResetDate: asOptional(asDate),
otpTimeout: asOptional(asNumber),

// Password login:
passwordAuthBox: asOptional(asEdgeBox),
passwordAuthSnrp: asOptional(asEdgeSnrp),
passwordBox: asOptional(asEdgeBox),
passwordBox: asOptional(asEither(asTrue, asEdgeBox)),
passwordKeySnrp: asOptional(asEdgeSnrp),

// PIN v2 login:
pin2Box: asOptional(asEdgeBox),
pin2Box: asOptional(asEither(asTrue, asEdgeBox)),
pin2KeyBox: asOptional(asEdgeBox),
pin2TextBox: asOptional(asEdgeBox),

// Recovery v2 login:
question2Box: asOptional(asEdgeBox),
recovery2Box: asOptional(asEdgeBox),
recovery2Box: asOptional(asEither(asTrue, asEdgeBox)),
recovery2KeyBox: asOptional(asEdgeBox),

// Secret-key login:
Expand Down
10 changes: 6 additions & 4 deletions src/types/server-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface LoginRequestBody {
challengeId?: string
deviceDescription?: string
otp?: string
syncToken?: string
voucherId?: string
voucherAuth?: Uint8Array

Expand Down Expand Up @@ -209,30 +210,31 @@ export interface LoginPayload {
appId: string
created: Date
loginId: Uint8Array
syncToken?: string

// Nested logins:
children?: LoginPayload[]
parentBox?: EdgeBox

// 2-factor login:
otpKey?: Uint8Array
otpKey?: Uint8Array | true
otpResetDate?: Date
otpTimeout?: number

// Password login:
passwordAuthBox?: EdgeBox
passwordAuthSnrp?: EdgeSnrp
passwordBox?: EdgeBox
passwordBox?: EdgeBox | true
passwordKeySnrp?: EdgeSnrp

// PIN v2 login:
pin2Box?: EdgeBox
pin2Box?: EdgeBox | true
pin2KeyBox?: EdgeBox
pin2TextBox?: EdgeBox

// Recovery v2 login:
question2Box?: EdgeBox
recovery2Box?: EdgeBox
recovery2Box?: EdgeBox | true
recovery2KeyBox?: EdgeBox

// Secret-key login:
Expand Down