diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e394413a..3eb982626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # edge-core-js +## Unreleased + +- added: Export cleaners for server types and testing data types. +- deprecated: `EdgeContext.listRecoveryQuestionChoices`. The GUI provides its own localized strings now. + ## v1.7.0 (2023-09-12) - added: Add a `ChallengeError` and related types, which will allow the login server to request CAPTCHA validation. diff --git a/src/core/context/context-api.ts b/src/core/context/context-api.ts index 0dba6ab61..67216cef2 100644 --- a/src/core/context/context-api.ts +++ b/src/core/context/context-api.ts @@ -212,10 +212,6 @@ export function makeContextApi(ai: ApiInput): EdgeContext { return await getQuestions2(ai, base58.parse(recovery2Key), username) }, - async listRecoveryQuestionChoices(): Promise { - return await listRecoveryQuestionChoices(ai) - }, - async requestEdgeLogin( opts?: EdgeAccountOptions ): Promise { @@ -271,6 +267,11 @@ export function makeContextApi(ai: ApiInput): EdgeContext { async changeLogSettings(settings: Partial): Promise { const newSettings = { ...ai.props.state.logSettings, ...settings } ai.props.dispatch({ type: 'CHANGE_LOG_SETTINGS', payload: newSettings }) + }, + + /** @deprecated The GUI provides its own localized strings now. */ + async listRecoveryQuestionChoices(): Promise { + return await listRecoveryQuestionChoices(ai) } } bridgifyObject(out) diff --git a/src/core/fake/fake-db.ts b/src/core/fake/fake-db.ts index cb2b39a7b..d2c7723b5 100644 --- a/src/core/fake/fake-db.ts +++ b/src/core/fake/fake-db.ts @@ -1,6 +1,5 @@ -import { FakeUser, LoginDump } from '../../types/fake-types' +import { EdgeLoginDump, EdgeRepoDump } from '../../types/fake-types' import { - EdgeBox, EdgeLobbyReply, EdgeLobbyRequest, LoginPayload @@ -20,14 +19,7 @@ export interface DbLobby { /** * A login object stored in the fake database. */ -export type DbLogin = Omit - -/** - * A sync repo stored in the fake database. - */ -export interface DbRepo { - [path: string]: EdgeBox -} +export type DbLogin = Omit /** * Emulates the Airbitz login server database. @@ -35,7 +27,7 @@ export interface DbRepo { export class FakeDb { lobbies: { [lobbyId: string]: DbLobby } logins: DbLogin[] - repos: { [syncKey: string]: DbRepo } + repos: { [syncKey: string]: EdgeRepoDump } constructor() { this.lobbies = {} @@ -78,25 +70,21 @@ export class FakeDb { // Dumping & restoration -------------------------------------------- - setupFakeUser(user: FakeUser): void { - const setupLogin = (dump: LoginDump): void => { - const { children, ...rest } = dump - this.insertLogin(rest) - for (const child of children) { - child.parentId = dump.loginId - setupLogin(child) - } + setupLogin(dump: EdgeLoginDump): void { + const { children, ...rest } = dump + this.insertLogin(rest) + for (const child of children) { + child.parentId = dump.loginId + this.setupLogin(child) } - setupLogin(user.server) + } - // Create fake repos: - for (const syncKey of Object.keys(user.repos)) { - this.repos[syncKey] = { ...user.repos[syncKey] } - } + setupRepo(syncKey: string, repo: EdgeRepoDump): void { + this.repos[syncKey] = repo } - dumpLogin(login: DbLogin): LoginDump { - const makeTree = (login: DbLogin): LoginDump => ({ + dumpLogin(login: DbLogin): EdgeLoginDump { + const makeTree = (login: DbLogin): EdgeLoginDump => ({ ...login, children: this.getLoginsByParent(login).map(login => makeTree(login)) }) diff --git a/src/core/fake/fake-responses.ts b/src/core/fake/fake-responses.ts index d9adc25fe..e1d093675 100644 --- a/src/core/fake/fake-responses.ts +++ b/src/core/fake/fake-responses.ts @@ -1,16 +1,14 @@ -import { uncleaner } from 'cleaners' +import { Cleaner } from 'cleaners' import { HttpHeaders, HttpResponse } from 'serverlet' -import { VoucherDump } from '../../types/fake-types' +import { EdgeVoucherDump } from '../../types/fake-types' import { - asOtpErrorPayload, - asPasswordErrorPayload + wasLoginResponseBody, + wasOtpErrorPayload, + wasPasswordErrorPayload } from '../../types/server-cleaners' import { DbLogin } from './fake-db' -const wasOtpErrorPayload = uncleaner(asOtpErrorPayload) -const wasPasswordErrorPayload = uncleaner(asPasswordErrorPayload) - interface LoginStatusCode { code: number httpStatus: number @@ -104,19 +102,37 @@ export const statusCodes = { } } +export function cleanRequest( + cleaner: Cleaner, + raw: unknown +): [T | undefined, HttpResponse] { + try { + const clean = cleaner(raw) + return [clean, { status: 200 }] + } catch (error) { + return [ + undefined, + statusResponse( + statusCodes.invalidRequest, + `Invalid request: ${String(error)}` + ) + ] + } +} + /** * Construct an HttpResponse object with a JSON body. */ export function jsonResponse( body: unknown, opts: { status?: number; headers?: HttpHeaders } = {} -): Promise { +): HttpResponse { const { status = 200, headers = {} } = opts - return Promise.resolve({ + return { status, headers: { 'content-type': 'application/json', ...headers }, body: JSON.stringify(body) - }) + } } /** @@ -125,9 +141,12 @@ export function jsonResponse( export function statusResponse( statusCode: LoginStatusCode = statusCodes.success, message: string = statusCode.message -): Promise { +): HttpResponse { const { code, httpStatus } = statusCode - const body = { status_code: code, message } + const body = wasLoginResponseBody({ + status_code: code, + message + }) return jsonResponse(body, { status: httpStatus }) } @@ -138,9 +157,13 @@ export function payloadResponse( payload: unknown, statusCode: LoginStatusCode = statusCodes.success, message: string = statusCode.message -): Promise { +): HttpResponse { const { code, httpStatus } = statusCode - const body = { status_code: code, message, results: payload } + const body = wasLoginResponseBody({ + status_code: code, + message, + results: payload + }) return jsonResponse(body, { status: httpStatus }) } @@ -151,9 +174,9 @@ export function otpErrorResponse( login: DbLogin, opts: { reason?: 'ip' | 'otp' - voucher?: VoucherDump + voucher?: EdgeVoucherDump } = {} -): Promise { +): HttpResponse { const { reason = 'otp', voucher } = opts return payloadResponse( wasOtpErrorPayload({ @@ -172,7 +195,7 @@ export function otpErrorResponse( /** * A password failure, with timeout. */ -export function passwordErrorResponse(wait: number): Promise { +export function passwordErrorResponse(wait: number): HttpResponse { return payloadResponse( wasPasswordErrorPayload({ wait_seconds: wait diff --git a/src/core/fake/fake-server.ts b/src/core/fake/fake-server.ts index 276195430..03caa0e9f 100644 --- a/src/core/fake/fake-server.ts +++ b/src/core/fake/fake-server.ts @@ -1,4 +1,4 @@ -import { asMaybe, asObject, uncleaner } from 'cleaners' +import { asMaybe, asObject } from 'cleaners' import { FetchRequest, HttpRequest, @@ -8,7 +8,11 @@ import { Serverlet } from 'serverlet' -import { VoucherDump } from '../../types/fake-types' +import { + EdgeRepoDump, + EdgeVoucherDump, + wasEdgeRepoDump +} from '../../types/fake-types' import { asChangeOtpPayload, asChangePasswordPayload, @@ -24,13 +28,13 @@ import { asEdgeBox, asEdgeLobbyReply, asEdgeLobbyRequest, - asLobbyPayload, - asLoginPayload, asLoginRequestBody, - asMessagesPayload, - asOtpResetPayload, - asRecovery2InfoPayload, - asUsernameInfoPayload + wasLobbyPayload, + wasLoginPayload, + wasMessagesPayload, + wasOtpResetPayload, + wasRecovery2InfoPayload, + wasUsernameInfoPayload } from '../../types/server-cleaners' import { LoginRequestBody, MessagesPayload } from '../../types/server-types' import { checkTotp } from '../../util/crypto/hotp' @@ -41,12 +45,12 @@ import { userIdSnrp } from '../scrypt/scrypt-selectors' import { DbLobby, DbLogin, - DbRepo, FakeDb, makeLoginPayload, makePendingVouchers } from './fake-db' import { + cleanRequest, jsonResponse, otpErrorResponse, passwordErrorResponse, @@ -55,13 +59,6 @@ import { statusResponse } from './fake-responses' -const wasLobbyPayload = uncleaner(asLobbyPayload) -const wasLoginPayload = uncleaner(asLoginPayload) -const wasMessagesPayload = uncleaner(asMessagesPayload) -const wasOtpResetPayload = uncleaner(asOtpResetPayload) -const wasRecovery2InfoPayload = uncleaner(asRecovery2InfoPayload) -const wasUsernameInfoPayload = uncleaner(asUsernameInfoPayload) - type DbRequest = HttpRequest & { readonly db: FakeDb readonly json: unknown @@ -77,7 +74,7 @@ type LobbyIdRequest = ApiRequest & { readonly lobbyId: string } type RepoRequest = DbRequest & { - readonly repo: DbRepo + readonly repo: EdgeRepoDump } // Authentication middleware: ---------------------------------------------- @@ -86,8 +83,8 @@ const withApiKey = (server: Serverlet): Serverlet => async request => { const { json } = request - const body = asMaybe(asLoginRequestBody)(json) - if (body == null) return await statusResponse(statusCodes.invalidRequest) + const [body, bodyError] = cleanRequest(asLoginRequestBody, json) + if (body == null) return bodyError return await server({ ...request, body, payload: body.data }) } @@ -121,7 +118,7 @@ const withValidOtp: ( } login.otpResetAuth = 'Super secret reset token' - const voucher: VoucherDump = { + const voucher: EdgeVoucherDump = { activates: new Date('2020-01-01T00:00:00Z'), created: new Date('2020-01-08T00:00:00Z'), deviceDescription: 'A phone', @@ -240,8 +237,10 @@ const loginRoute = withLogin2( if (login == null) { return statusResponse(statusCodes.noAccount) } - const { passwordAuthSnrp = userIdSnrp } = login - return payloadResponse(wasUsernameInfoPayload({ passwordAuthSnrp })) + const { loginId, passwordAuthSnrp = userIdSnrp } = login + return payloadResponse( + wasUsernameInfoPayload({ loginId, passwordAuthSnrp }) + ) } if (recovery2Id != null && recovery2Auth == null) { const login = db.getLoginByRecovery2Id(recovery2Id) @@ -258,20 +257,16 @@ const loginRoute = withLogin2( } ) -function createLogin( - request: ApiRequest, - login?: DbLogin -): Promise { +function createLogin(request: ApiRequest, login?: DbLogin): HttpResponse { const { db, json } = request const date = new Date() - const body = asMaybe(asLoginRequestBody)(json) - if (body == null) return statusResponse(statusCodes.invalidRequest) - const clean = asMaybe(asCreateLoginPayload)(body.data) - const secret = asMaybe(asChangeSecretPayload)(clean) - if (clean == null || secret == null) { - return statusResponse(statusCodes.invalidRequest) - } + const [body, bodyError] = cleanRequest(asLoginRequestBody, json) + if (body == null) return bodyError + const [clean, cleanError] = cleanRequest(asCreateLoginPayload, body.data) + if (clean == null) return cleanError + const [secret, secretError] = cleanRequest(asChangeSecretPayload, body.data) + if (secret == null) return secretError // Do not re-create accounts: if (db.getLoginById(clean.loginId) != null) { @@ -279,8 +274,10 @@ function createLogin( } // Set up repos: - const emptyKeys = { newSyncKeys: [], keyBoxes: [] } - const keys = asMaybe(asCreateKeysPayload, emptyKeys)(clean) + const keys = asMaybe(asCreateKeysPayload, () => ({ + newSyncKeys: [], + keyBoxes: [] + }))(body.data) for (const syncKey of keys.newSyncKeys) { db.repos[syncKey] = {} } @@ -295,11 +292,11 @@ function createLogin( vouchers: [], // Optional fields: - ...asMaybe(asChangeOtpPayload)(clean), - ...asMaybe(asChangePasswordPayload)(clean), - ...asMaybe(asChangePin2Payload)(clean), - ...asMaybe(asChangeRecovery2Payload)(clean), - ...asMaybe(asChangeUsernamePayload)(clean) + ...asMaybe(asChangeOtpPayload)(body.data), + ...asMaybe(asChangePasswordPayload)(body.data), + ...asMaybe(asChangePin2Payload)(body.data), + ...asMaybe(asChangeRecovery2Payload)(body.data), + ...asMaybe(asChangeUsernamePayload)(body.data) } // Set up the parent/child relationship: @@ -324,8 +321,8 @@ const createLoginRoute = withLogin2( const addKeysRoute = withLogin2(request => { const { db, login, payload } = request - const clean = asMaybe(asCreateKeysPayload)(payload) - if (clean == null) return statusResponse(statusCodes.invalidRequest) + const [clean, cleanError] = cleanRequest(asCreateKeysPayload, payload) + if (clean == null) return cleanError // Set up repos: for (const syncKey of clean.newSyncKeys) { @@ -338,8 +335,8 @@ const addKeysRoute = withLogin2(request => { const changeOtpRoute = withLogin2(request => { const { login, payload } = request - const clean = asMaybe(asChangeOtpPayload)(payload) - if (clean == null) return statusResponse(statusCodes.invalidRequest) + const [clean, cleanError] = cleanRequest(asChangeOtpPayload, payload) + if (clean == null) return cleanError login.otpKey = clean.otpKey login.otpTimeout = clean.otpTimeout @@ -402,8 +399,8 @@ const deletePasswordRoute = withLogin2(request => { const changePasswordRoute = withLogin2(request => { const { login, payload } = request - const clean = asMaybe(asChangePasswordPayload)(payload) - if (clean == null) return statusResponse(statusCodes.invalidRequest) + const [clean, cleanError] = cleanRequest(asChangePasswordPayload, payload) + if (clean == null) return cleanError login.passwordAuth = clean.passwordAuth login.passwordAuthBox = clean.passwordAuthBox @@ -427,8 +424,8 @@ const deletePin2Route = withLogin2(request => { const changePin2Route = withLogin2(request => { const { login, payload } = request - const clean = asMaybe(asChangePin2Payload)(payload) - if (clean == null) return statusResponse(statusCodes.invalidRequest) + const [clean, cleanError] = cleanRequest(asChangePin2Payload, payload) + if (clean == null) return cleanError login.pin2Auth = clean.pin2Auth login.pin2Box = clean.pin2Box @@ -452,8 +449,8 @@ const deleteRecovery2Route = withLogin2(request => { const changeRecovery2Route = withLogin2(request => { const { login, payload } = request - const clean = asMaybe(asChangeRecovery2Payload)(payload) - if (clean == null) return statusResponse(statusCodes.invalidRequest) + const [clean, cleanError] = cleanRequest(asChangeRecovery2Payload, payload) + if (clean == null) return cleanError login.question2Box = clean.question2Box login.recovery2Auth = clean.recovery2Auth @@ -466,8 +463,8 @@ const changeRecovery2Route = withLogin2(request => { const secretRoute = withLogin2(request => { const { db, login, payload } = request - const clean = asMaybe(asChangeSecretPayload)(payload) - if (clean == null) return statusResponse(statusCodes.invalidRequest) + const [clean, cleanError] = cleanRequest(asChangeSecretPayload, payload) + if (clean == null) return cleanError // Do a quick sanity check: if (login.loginAuth != null) { @@ -493,25 +490,22 @@ const usernameRoute = withLogin2(async request => { // Validate the payload selection: if (login.passwordAuth != null && cleanPassword == null) { - return await statusResponse( + return statusResponse( statusCodes.invalidRequest, 'Missing password payload' ) } if (login.pin2Auth != null && cleanPin2 == null) { - return await statusResponse( - statusCodes.invalidRequest, - 'Missing pin2Id payload' - ) + return statusResponse(statusCodes.invalidRequest, 'Missing pin2Id payload') } if (login.recovery2Auth != null && cleanRecovery2 == null) { - return await statusResponse( + return statusResponse( statusCodes.invalidRequest, 'Missing recovery2Id payload' ) } if (login.parentBox == null && cleanUsername == null) { - return await statusResponse( + return statusResponse( statusCodes.invalidRequest, 'Missing username payload' ) @@ -529,14 +523,11 @@ const usernameRoute = withLogin2(async request => { // Do we have a PIN? if (cleanPin2 != null) { if (login.pin2Auth == null) { - return await statusResponse( - statusCodes.invalidRequest, - 'Login lacks pin2' - ) + return statusResponse(statusCodes.invalidRequest, 'Login lacks pin2') } - const existing = await db.getLoginByPin2Id(cleanPin2.pin2Id) + const existing = db.getLoginByPin2Id(cleanPin2.pin2Id) if (existing != null) { - return await statusResponse(statusCodes.conflict) + return statusResponse(statusCodes.conflict) } login.pin2Id = cleanPin2.pin2Id } @@ -544,14 +535,11 @@ const usernameRoute = withLogin2(async request => { // Do we have recovery? if (cleanRecovery2 != null) { if (login.recovery2Auth == null) { - return await statusResponse( - statusCodes.invalidRequest, - 'Login lacks recovery2' - ) + return statusResponse(statusCodes.invalidRequest, 'Login lacks recovery2') } - const existing = await db.getLoginByRecovery2Id(cleanRecovery2.recovery2Id) + const existing = db.getLoginByRecovery2Id(cleanRecovery2.recovery2Id) if (existing != null) { - return await statusResponse(statusCodes.conflict) + return statusResponse(statusCodes.conflict) } login.recovery2Id = cleanRecovery2.recovery2Id } @@ -559,26 +547,26 @@ const usernameRoute = withLogin2(async request => { // Are we the root login? if (cleanUsername != null) { if (login.parentBox != null) { - return await statusResponse( + return statusResponse( statusCodes.invalidRequest, 'Only top-level logins can have usernames' ) } - const existing = await db.getLoginByUserId(cleanUsername.userId) + const existing = db.getLoginByUserId(cleanUsername.userId) if (existing != null) { - return await statusResponse(statusCodes.conflict) + return statusResponse(statusCodes.conflict) } login.userId = cleanUsername.userId login.userTextBox = cleanUsername.userTextBox } - return await payloadResponse(wasLoginPayload(makeLoginPayload(db, login))) + return payloadResponse(wasLoginPayload(makeLoginPayload(db, login))) }) const vouchersRoute = withLogin2(async request => { const { db, login, payload } = request - const clean = asMaybe(asChangeVouchersPayload)(payload) - if (clean == null) return await statusResponse(statusCodes.invalidRequest) + const [clean, cleanError] = cleanRequest(asChangeVouchersPayload, payload) + if (clean == null) return cleanError const { approvedVouchers = [], rejectedVouchers = [] } = clean // Let's get our tasks organized: @@ -592,9 +580,7 @@ const vouchersRoute = withLogin2(async request => { voucher.status = table[voucher.voucherId] } - return await payloadResponse( - wasLoginPayload(await makeLoginPayload(db, login)) - ) + return payloadResponse(wasLoginPayload(makeLoginPayload(db, login))) }) // lobby: ------------------------------------------------------------------ @@ -625,10 +611,10 @@ const createLobbyRoute = withLobby( request => { const { db, json, lobbyId } = request - const body = asMaybe(asLoginRequestBody)(json) - if (body == null) return statusResponse(statusCodes.invalidRequest) - const clean = asMaybe(asEdgeLobbyRequest)(body.data) - if (clean == null) return statusResponse(statusCodes.invalidRequest) + const [body, bodyError] = cleanRequest(asLoginRequestBody, json) + if (body == null) return bodyError + const [clean, cleanError] = cleanRequest(asEdgeLobbyRequest, body.data) + if (clean == null) return cleanError const { timeout = 600 } = clean const expires = new Date(Date.now() + 1000 * timeout).toISOString() @@ -641,10 +627,10 @@ const createLobbyRoute = withLobby( const updateLobbyRoute = withLobby(request => { const { json, lobby } = request - const body = asMaybe(asLoginRequestBody)(json) - if (body == null) return statusResponse(statusCodes.invalidRequest) - const clean = asMaybe(asEdgeLobbyReply)(body.data) - if (clean == null) return statusResponse(statusCodes.invalidRequest) + const [body, bodyError] = cleanRequest(asLoginRequestBody, json) + if (body == null) return bodyError + const [clean, cleanError] = cleanRequest(asEdgeLobbyReply, body.data) + if (clean == null) return cleanError lobby.replies.push(clean) return statusResponse() @@ -665,11 +651,11 @@ const deleteLobbyRoute = withLobby(request => { const messagesRoute: Serverlet = request => { const { db, json } = request - const clean = asMaybe(asLoginRequestBody)(json) - if (clean == null || clean.loginIds == null) { - return statusResponse(statusCodes.invalidRequest) - } + const [clean, cleanError] = cleanRequest(asLoginRequestBody, json) + if (clean == null) return cleanError + const { loginIds } = clean + if (loginIds == null) return statusResponse(statusCodes.invalidRequest) const out: MessagesPayload = [] for (const loginId of loginIds) { @@ -707,7 +693,7 @@ const withRepo = const storeReadRoute = withRepo(request => { const { repo } = request - return jsonResponse({ changes: repo }) + return jsonResponse({ changes: wasEdgeRepoDump(repo) }) }) const storeUpdateRoute = withRepo(request => { @@ -717,7 +703,7 @@ const storeUpdateRoute = withRepo(request => { repo[change] = changes[change] } return jsonResponse({ - changes: repo, + changes: wasEdgeRepoDump(repo), hash: '1111111111111111111111111111111111111111' }) }) diff --git a/src/core/fake/fake-world.ts b/src/core/fake/fake-world.ts index 0ee8337ef..ebc626460 100644 --- a/src/core/fake/fake-world.ts +++ b/src/core/fake/fake-world.ts @@ -1,14 +1,15 @@ -import { uncleaner } from 'cleaners' import { makeMemoryDisklet } from 'disklet' import { base16, base64 } from 'rfc4648' import { makeFetchFunction } from 'serverlet' import { bridgifyObject, close } from 'yaob' +import { fixUsername } from '../../client-side' import { - asFakeUser, - asFakeUsers, - asLoginDump, - FakeUser + asEdgeLoginDump, + asEdgeRepoDump, + EdgeRepoDump, + wasEdgeLoginDump, + wasEdgeRepoDump } from '../../types/fake-types' import { asLoginPayload } from '../../types/server-cleaners' import { @@ -24,19 +25,19 @@ import { base58 } from '../../util/encoding' import { validateServer } from '../../util/validateServer' import { LogBackend } from '../log/log' import { applyLoginPayload } from '../login/login' -import { asLoginStash } from '../login/login-stash' +import { wasLoginStash } from '../login/login-stash' import { PluginIos } from '../plugins/plugins-actions' import { makeContext } from '../root' import { makeRepoPaths, saveChanges } from '../storage/repo' import { FakeDb } from './fake-db' import { makeFakeServer } from './fake-server' -const wasLoginStash = uncleaner(asLoginStash) -const wasLoginDump = uncleaner(asLoginDump) -const wasFakeUser = uncleaner(asFakeUser) - -async function saveUser(io: EdgeIo, user: FakeUser): Promise { - const { lastLogin, loginId, loginKey, repos, server, username } = user +async function saveLogin(io: EdgeIo, user: EdgeFakeUser): Promise { + const { lastLogin, server } = user + const loginId = base64.parse(user.loginId) + const loginKey = base64.parse(user.loginKey) + const username = + user.username == null ? undefined : fixUsername(user.username) // Save the stash: const stash = applyLoginPayload( @@ -48,23 +49,26 @@ async function saveUser(io: EdgeIo, user: FakeUser): Promise { username }, loginKey, - asLoginPayload(wasLoginDump(server)) + // The correct cleaner is `asEdgeLoginDump`, + // but the format is close enough that the other cleaner kinda fits: + asLoginPayload(server) ) const path = `logins/${base58.stringify(loginId)}.json` await io.disklet .setText(path, JSON.stringify(wasLoginStash(stash))) .catch(() => {}) +} - // Save the repos: - await Promise.all( - Object.keys(repos).map(async syncKey => { - const paths = makeRepoPaths(io, base16.parse(syncKey), new Uint8Array(0)) - await saveChanges(paths.dataDisklet, user.repos[syncKey]) - await paths.baseDisklet.setText( - 'status.json', - JSON.stringify({ lastSync: 1, lastHash: null }) - ) - }) +async function saveRepo( + io: EdgeIo, + syncKey: string, + repo: EdgeRepoDump +): Promise { + const paths = makeRepoPaths(io, base16.parse(syncKey), new Uint8Array(0)) + await saveChanges(paths.dataDisklet, repo) + await paths.baseDisklet.setText( + 'status.json', + JSON.stringify({ lastSync: 1, lastHash: null }) ) } @@ -81,8 +85,12 @@ export function makeFakeWorld( const fakeServer = makeFakeServer(fakeDb) // Populate the fake database: - const cleanUsers = asFakeUsers(users) - for (const user of cleanUsers) fakeDb.setupFakeUser(user) + for (const user of users) { + fakeDb.setupLogin(asEdgeLoginDump(user.server)) + for (const syncKey of Object.keys(user.repos)) { + fakeDb.setupRepo(syncKey, asEdgeRepoDump(user.repos[syncKey])) + } + } const contexts: EdgeContext[] = [] @@ -115,9 +123,12 @@ export function makeFakeWorld( // Populate the fake disk: if (!cleanDevice) { - await Promise.all( - cleanUsers.map(async user => await saveUser(fakeIo, user)) - ) + for (const user of users) { + await saveLogin(fakeIo, user) + for (const syncKey of Object.keys(user.repos)) { + await saveRepo(fakeIo, syncKey, asEdgeRepoDump(user.repos[syncKey])) + } + } } const out = await makeContext({ io: fakeIo, nativeIo }, logBackend, { @@ -152,16 +163,18 @@ export function makeFakeWorld( ) } const repos: EdgeFakeUser['repos'] = {} - for (const syncKey of syncKeys) repos[syncKey] = fakeDb.repos[syncKey] + for (const syncKey of syncKeys) { + repos[syncKey] = wasEdgeRepoDump(fakeDb.repos[syncKey]) + } - return wasFakeUser({ + return { lastLogin: account.lastLogin, - loginId, - loginKey, + loginId: base64.stringify(loginId), + loginKey: base64.stringify(loginKey), repos, - server: fakeDb.dumpLogin(login), + server: wasEdgeLoginDump(fakeDb.dumpLogin(login)), username: account.username - }) + } } } bridgifyObject(out) diff --git a/src/core/login/create.ts b/src/core/login/create.ts index 4f5c290ad..b2e8e8abe 100644 --- a/src/core/login/create.ts +++ b/src/core/login/create.ts @@ -1,6 +1,4 @@ -import { uncleaner } from 'cleaners' - -import { asCreateLoginPayload } from '../../types/server-cleaners' +import { wasCreateLoginPayload } from '../../types/server-cleaners' import { EdgeBox } from '../../types/server-types' import { asMaybeUsernameError, @@ -19,8 +17,6 @@ import { makeUsernameKit } from './login-username' import { makePasswordKit } from './password' import { makeChangePin2Kit } from './pin2' -const wasCreateLoginPayload = uncleaner(asCreateLoginPayload) - export interface LoginCreateOpts { keyInfo?: EdgeWalletInfo password?: string diff --git a/src/core/login/keys.ts b/src/core/login/keys.ts index 701bf6d50..be6a29265 100644 --- a/src/core/login/keys.ts +++ b/src/core/login/keys.ts @@ -1,5 +1,6 @@ import { base16, base64 } from 'rfc4648' +import { wasCreateKeysPayload } from '../../types/server-cleaners' import { EdgeCreateCurrencyWalletOptions, EdgeCurrencyWallet, @@ -21,7 +22,7 @@ import { maybeFindCurrencyPluginId } from '../plugins/plugins-selectors' import { ApiInput } from '../root-pixie' -import { AppIdMap, LoginKit, LoginTree } from './login-types' +import { AppIdMap, LoginKit, LoginTree, wasEdgeWalletInfo } from './login-types' /** * Returns the first keyInfo with a matching type. @@ -82,7 +83,11 @@ export function makeKeysKit( ): LoginKit { const { io } = ai.props const keyBoxes = keyInfos.map(info => - encrypt(io, utf8.parse(JSON.stringify(info)), login.loginKey) + encrypt( + io, + utf8.parse(JSON.stringify(wasEdgeWalletInfo(info))), + login.loginKey + ) ) const newSyncKeys: string[] = [] for (const info of keyInfos) { @@ -94,7 +99,7 @@ export function makeKeysKit( return { serverPath: '/v2/login/keys', - server: { keyBoxes, newSyncKeys }, + server: wasCreateKeysPayload({ keyBoxes, newSyncKeys }), stash: { keyBoxes }, login: { keyInfos }, loginId: login.loginId diff --git a/src/core/login/lobby.ts b/src/core/login/lobby.ts index fae79f10d..bc958930c 100644 --- a/src/core/login/lobby.ts +++ b/src/core/login/lobby.ts @@ -1,11 +1,10 @@ -import { uncleaner } from 'cleaners' import elliptic from 'elliptic' import { Events, makeEvents, OnEvents } from 'yavent' import { - asEdgeLobbyReply, - asEdgeLobbyRequest, - asLobbyPayload + asLobbyPayload, + wasEdgeLobbyReply, + wasEdgeLobbyRequest } from '../../types/server-cleaners' import { EdgeLobbyReply, EdgeLobbyRequest } from '../../types/server-types' import { EdgeIo } from '../../types/types' @@ -20,9 +19,6 @@ import { loginFetch } from './login-fetch' const EC = elliptic.ec const secp256k1 = new EC('secp256k1') -const wasEdgeLobbyReply = uncleaner(asEdgeLobbyReply) -const wasEdgeLobbyRequest = uncleaner(asEdgeLobbyRequest) - type KeyPair = elliptic.ec.KeyPair interface LobbyEvents { diff --git a/src/core/login/login-fetch.ts b/src/core/login/login-fetch.ts index adf5008fc..dae8042a1 100644 --- a/src/core/login/login-fetch.ts +++ b/src/core/login/login-fetch.ts @@ -1,9 +1,9 @@ -import { asMaybe, uncleaner } from 'cleaners' +import { asMaybe } from 'cleaners' import { asChallengeErrorPayload, - asLoginRequestBody, - asLoginResponseBody + asLoginResponseBody, + wasLoginRequestBody } from '../../types/server-cleaners' import { LoginRequestBody } from '../../types/server-types' import { @@ -18,8 +18,6 @@ import { import { timeout } from '../../util/promise' import { ApiInput } from '../root-pixie' -const wasLoginRequestBody = uncleaner(asLoginRequestBody) - export function parseReply(json: unknown): unknown { const clean = asLoginResponseBody(json) diff --git a/src/core/login/login-secret.ts b/src/core/login/login-secret.ts index d6768c15f..17f7fe27a 100644 --- a/src/core/login/login-secret.ts +++ b/src/core/login/login-secret.ts @@ -1,12 +1,8 @@ -import { uncleaner } from 'cleaners' - -import { asChangeSecretPayload } from '../../types/server-cleaners' +import { wasChangeSecretPayload } from '../../types/server-cleaners' import { encrypt } from '../../util/crypto/crypto' import { ApiInput } from '../root-pixie' import { LoginKit, LoginTree } from './login-types' -const wasChangeSecretPayload = uncleaner(asChangeSecretPayload) - export function makeSecretKit( ai: ApiInput, login: Pick diff --git a/src/core/login/login-stash.ts b/src/core/login/login-stash.ts index 24f6aa591..3a4cffb8f 100644 --- a/src/core/login/login-stash.ts +++ b/src/core/login/login-stash.ts @@ -137,7 +137,7 @@ export async function saveStash( dispatch({ type: 'LOGIN_STASH_SAVED', payload: stashTree }) } -export const asUsername: Cleaner = raw => fixUsername(asString(raw)) +const asUsername: Cleaner = raw => fixUsername(asString(raw)) export const asLoginStash: Cleaner = asObject({ // Identity: @@ -191,4 +191,5 @@ export const asLoginStash: Cleaner = asObject({ rootKeyBox: asOptional(asEdgeBox), syncKeyBox: asOptional(asEdgeBox) }) -const wasLoginStash = uncleaner(asLoginStash) + +export const wasLoginStash = uncleaner(asLoginStash) diff --git a/src/core/login/login-types.ts b/src/core/login/login-types.ts index 7b9ec8853..b8d01aa96 100644 --- a/src/core/login/login-types.ts +++ b/src/core/login/login-types.ts @@ -1,7 +1,10 @@ +import { asObject, asString, uncleaner } from 'cleaners' + import { EdgePendingVoucher, EdgeWalletInfo, - EdgeWalletInfoFull + EdgeWalletInfoFull, + JsonObject } from '../../types/types' import { LoginStash } from './login-stash' @@ -71,3 +74,11 @@ export interface StashLeaf { export interface WalletInfoFullMap { [walletId: string]: EdgeWalletInfoFull } + +export const asEdgeWalletInfo = asObject({ + id: asString, + keys: (raw: any): JsonObject => raw, + type: asString +}) + +export const wasEdgeWalletInfo = uncleaner(asEdgeWalletInfo) diff --git a/src/core/login/login-username.ts b/src/core/login/login-username.ts index 79c7625c8..51d41f671 100644 --- a/src/core/login/login-username.ts +++ b/src/core/login/login-username.ts @@ -1,7 +1,5 @@ -import { uncleaner } from 'cleaners' - import { ChangeUsernameOptions } from '../../browser' -import { asChangeUsernamePayload } from '../../types/server-cleaners' +import { wasChangeUsernamePayload } from '../../types/server-cleaners' import { encrypt } from '../../util/crypto/crypto' import { utf8 } from '../../util/encoding' import { ApiInput } from '../root-pixie' @@ -12,8 +10,6 @@ import { makePasswordKit } from './password' import { makeChangePin2IdKit } from './pin2' import { makeChangeRecovery2IdKit } from './recovery2' -const wasChangeUsernamePayload = uncleaner(asChangeUsernamePayload) - export async function changeUsername( ai: ApiInput, accountId: string, diff --git a/src/core/login/login.ts b/src/core/login/login.ts index 15eee8c9d..534fd5fe9 100644 --- a/src/core/login/login.ts +++ b/src/core/login/login.ts @@ -27,7 +27,7 @@ import { loginFetch } from './login-fetch' import { makeSecretKit } from './login-secret' import { getStashById } from './login-selectors' import { LoginStash, saveStash } from './login-stash' -import { LoginKit, LoginTree } from './login-types' +import { asEdgeWalletInfo, LoginKit, LoginTree } from './login-types' import { getLoginOtp, getStashOtp } from './otp' /** @@ -277,7 +277,9 @@ function makeLoginTreeInner( } // Keys: - const keyInfos = keyBoxes.map(box => JSON.parse(decryptText(box, loginKey))) + const keyInfos = keyBoxes.map(box => + asEdgeWalletInfo(JSON.parse(decryptText(box, loginKey))) + ) login.keyInfos = mergeKeyInfos([...legacyKeys, ...keyInfos]).map(walletInfo => fixWalletInfo(walletInfo) ) diff --git a/src/core/login/otp.ts b/src/core/login/otp.ts index 111b0969e..1855926b6 100644 --- a/src/core/login/otp.ts +++ b/src/core/login/otp.ts @@ -1,9 +1,8 @@ -import { uncleaner } from 'cleaners' import { base32 } from 'rfc4648' import { - asChangeOtpPayload, - asOtpResetPayload + asOtpResetPayload, + wasChangeOtpPayload } from '../../types/server-cleaners' import { EdgeAccountOptions } from '../../types/types' import { totp } from '../../util/crypto/hotp' @@ -14,8 +13,6 @@ import { getStashById, hashUsername } from './login-selectors' import { LoginStash } from './login-stash' import { LoginKit, LoginTree } from './login-types' -const wasChangeOtpPayload = uncleaner(asChangeOtpPayload) - /** * Gets the current OTP for a logged-in account. */ diff --git a/src/core/login/password.ts b/src/core/login/password.ts index f06cfdcde..ce40ac0bb 100644 --- a/src/core/login/password.ts +++ b/src/core/login/password.ts @@ -1,6 +1,4 @@ -import { uncleaner } from 'cleaners' - -import { asChangePasswordPayload } from '../../types/server-cleaners' +import { wasChangePasswordPayload } from '../../types/server-cleaners' import { EdgeAccountOptions } from '../../types/types' import { decrypt, encrypt } from '../../util/crypto/crypto' import { ApiInput } from '../root-pixie' @@ -10,7 +8,6 @@ import { hashUsername } from './login-selectors' import { LoginStash, saveStash } from './login-stash' import { LoginKit, LoginTree } from './login-types' -const wasChangePasswordPayload = uncleaner(asChangePasswordPayload) const passwordAuthSnrp = userIdSnrp function makeHashInput(username: string, password: string): string { diff --git a/src/core/login/pin2.ts b/src/core/login/pin2.ts index 3ebc256bd..0c8eecf4a 100644 --- a/src/core/login/pin2.ts +++ b/src/core/login/pin2.ts @@ -1,8 +1,6 @@ -import { uncleaner } from 'cleaners' - import { - asChangePin2IdPayload, - asChangePin2Payload + wasChangePin2IdPayload, + wasChangePin2Payload } from '../../types/server-cleaners' import { LoginRequestBody } from '../../types/server-types' import { ChangePinOptions, EdgeAccountOptions } from '../../types/types' @@ -17,9 +15,6 @@ import { LoginStash } from './login-stash' import { LoginKit, LoginTree } from './login-types' import { getLoginOtp } from './otp' -const wasChangePin2IdPayload = uncleaner(asChangePin2IdPayload) -const wasChangePin2Payload = uncleaner(asChangePin2Payload) - function makePin2Id( pin2Key: Uint8Array, username: string | undefined diff --git a/src/core/login/recovery2.ts b/src/core/login/recovery2.ts index c80317f12..5888b9afe 100644 --- a/src/core/login/recovery2.ts +++ b/src/core/login/recovery2.ts @@ -1,10 +1,10 @@ -import { uncleaner } from 'cleaners' +import { asArray, asString, uncleaner } from 'cleaners' import { - asChangeRecovery2IdPayload, - asChangeRecovery2Payload, asQuestionChoicesPayload, - asRecovery2InfoPayload + asRecovery2InfoPayload, + wasChangeRecovery2IdPayload, + wasChangeRecovery2Payload } from '../../types/server-cleaners' import { EdgeAccountOptions, @@ -19,9 +19,6 @@ import { loginFetch } from './login-fetch' import { LoginStash } from './login-stash' import { LoginKit, LoginTree } from './login-types' -const wasChangeRecovery2IdPayload = uncleaner(asChangeRecovery2IdPayload) -const wasChangeRecovery2Payload = uncleaner(asChangeRecovery2Payload) - function makeRecovery2Id( recovery2Key: Uint8Array, username: string @@ -94,7 +91,7 @@ export async function getQuestions2( } // Decrypt the questions: - return JSON.parse(decryptText(question2Box, recovery2Key)) + return asQuestions(JSON.parse(decryptText(question2Box, recovery2Key))) } export async function changeRecovery( @@ -175,7 +172,7 @@ export function makeRecovery2Kit( const { loginId, loginKey, recovery2Key = io.random(32) } = login const question2Box = encrypt( io, - utf8.parse(JSON.stringify(questions)), + utf8.parse(JSON.stringify(wasQuestions(questions))), recovery2Key ) const recovery2Box = encrypt(io, loginKey, recovery2Key) @@ -207,3 +204,6 @@ export async function listRecoveryQuestionChoices( await loginFetch(ai, 'POST', '/v1/questions', {}) ) } + +const asQuestions = asArray(asString) +const wasQuestions = uncleaner(asQuestions) diff --git a/src/core/login/vouchers.ts b/src/core/login/vouchers.ts index 8331b5fa6..07aaa343e 100644 --- a/src/core/login/vouchers.ts +++ b/src/core/login/vouchers.ts @@ -1,8 +1,6 @@ -import { uncleaner } from 'cleaners' - import { - asChangeVouchersPayload, - asLoginPayload + asLoginPayload, + wasChangeVouchersPayload } from '../../types/server-cleaners' import { ChangeVouchersPayload } from '../../types/server-types' import { ApiInput } from '../root-pixie' @@ -12,8 +10,6 @@ import { getStashById } from './login-selectors' import { saveStash } from './login-stash' import { LoginTree } from './login-types' -const wasChangeVouchersPayload = uncleaner(asChangeVouchersPayload) - /** * Approves or rejects vouchers on the server. */ diff --git a/src/core/storage/encrypt-disklet.ts b/src/core/storage/encrypt-disklet.ts index 32d96c67d..e3266c290 100644 --- a/src/core/storage/encrypt-disklet.ts +++ b/src/core/storage/encrypt-disklet.ts @@ -1,7 +1,7 @@ import { Disklet, DiskletListing } from 'disklet' import { bridgifyObject } from 'yaob' -import { asEdgeBox } from '../../types/server-cleaners' +import { asEdgeBox, wasEdgeBox } from '../../types/server-cleaners' import { EdgeIo } from '../../types/types' import { decrypt, decryptText, encrypt } from '../../util/crypto/crypto' import { utf8 } from '../../util/encoding' @@ -35,7 +35,7 @@ export function encryptDisklet( setData(path: string, data: ArrayLike): Promise { return disklet.setText( path, - JSON.stringify(encrypt(io, Uint8Array.from(data), dataKey)) + JSON.stringify(wasEdgeBox(encrypt(io, Uint8Array.from(data), dataKey))) ) }, diff --git a/src/core/storage/repo.ts b/src/core/storage/repo.ts index c978cefca..3c531d35e 100644 --- a/src/core/storage/repo.ts +++ b/src/core/storage/repo.ts @@ -1,7 +1,10 @@ import { Disklet, mergeDisklets, navigateDisklet } from 'disklet' +import type { EdgeBox as SyncEdgeBox } from 'edge-sync-client' import { SyncClient } from 'edge-sync-client' import { base16, base64 } from 'rfc4648' +import { asEdgeBox, wasEdgeBox } from '../../types/server-cleaners' +import { EdgeBox } from '../../types/server-types' import { EdgeIo } from '../../types/types' import { sha256 } from '../../util/crypto/hashes' import { base58 } from '../../util/encoding' @@ -10,8 +13,12 @@ import { StorageWalletPaths, StorageWalletStatus } from './storage-reducer' const CHANGESET_MAX_ENTRIES = 100 +interface RepoChanges { + [path: string]: EdgeBox | null +} + export interface SyncResult { - changes: { [path: string]: any } + changes: RepoChanges status: StorageWalletStatus } @@ -71,13 +78,13 @@ export function loadRepoStatus( */ export async function saveChanges( disklet: Disklet, - changes: { [path: string]: any } + changes: RepoChanges ): Promise { await Promise.all( Object.keys(changes).map(path => { - const json = changes[path] - return json != null - ? disklet.setText(path, JSON.stringify(json)) + const box = changes[path] + return box != null + ? disklet.setText(path, JSON.stringify(wasEdgeBox(box))) : disklet.delete(path) }) ) @@ -95,12 +102,12 @@ export async function syncRepo( const ourChanges: Array<{ path: string - json: any + box: EdgeBox }> = await deepListWithLimit(changesDisklet).then(paths => { return Promise.all( paths.map(async path => ({ path, - json: JSON.parse(await changesDisklet.getText(path)) + box: asEdgeBox(JSON.parse(await changesDisklet.getText(path))) })) ) }) @@ -116,15 +123,20 @@ export async function syncRepo( } // Write local changes to the repo. - const changes: { [name: string]: any } = {} + const changes: { [name: string]: SyncEdgeBox } = {} for (const change of ourChanges) { - changes[change.path] = change.json + changes[change.path] = wasEdgeBox(change.box) } return syncClient.updateRepo(syncKeyEncoded, status.lastHash, { changes }) })() // Make the request: - const { changes = {}, hash } = reply + const { hash } = reply + const changes: RepoChanges = {} + for (const path of Object.keys(reply.changes ?? {})) { + const json = reply.changes[path] + changes[path] = json == null ? null : asEdgeBox(json) + } // Save the incoming changes into our `data` folder: await saveChanges(dataDisklet, changes) diff --git a/src/react-native.tsx b/src/react-native.tsx index abe88f049..643884d78 100644 --- a/src/react-native.tsx +++ b/src/react-native.tsx @@ -1,3 +1,4 @@ +import { asObject, asString } from 'cleaners' import { makeReactNativeDisklet } from 'disklet' import * as React from 'react' import { base64 } from 'rfc4648' @@ -128,6 +129,12 @@ function bridgifyLogBackend(backend: LogBackend): LogBackend { return bridgifyObject(backend) } +/** Just the parts of LoginStash that `fetchLoginMessages` needs. */ +const asUsernameStash = asObject({ + loginId: asString, + username: asString +}) + /** * Fetches any login-related messages for all the users on this device. */ @@ -146,8 +153,7 @@ export async function fetchLoginMessages( ) for (const text of files) { try { - const { username, loginId } = JSON.parse(text) - if (loginId == null || username == null) continue + const { username, loginId } = asUsernameStash(JSON.parse(text)) loginMap[loginId] = username } catch (error: unknown) {} } diff --git a/src/types/fake-types.ts b/src/types/fake-types.ts index e4beb7171..7b4c29ed8 100644 --- a/src/types/fake-types.ts +++ b/src/types/fake-types.ts @@ -6,10 +6,10 @@ import { asObject, asOptional, asString, - asValue + asValue, + uncleaner } from 'cleaners' -import { asUsername } from '../core/login/login-stash' import { asBase32, asBase64, @@ -19,7 +19,11 @@ import { } from './server-cleaners' import type { EdgeBox, EdgeSnrp } from './server-types' -export interface VoucherDump { +export interface EdgeRepoDump { + [key: string]: EdgeBox +} + +export interface EdgeVoucherDump { // Identity: loginId: Uint8Array voucherAuth: Uint8Array @@ -36,7 +40,7 @@ export interface VoucherDump { deviceDescription: string | undefined } -export interface LoginDump { +export interface EdgeLoginDump { // Identity: appId: string created: Date @@ -82,12 +86,12 @@ export interface LoginDump { userTextBox?: EdgeBox // Resources: - children: LoginDump[] + children: EdgeLoginDump[] keyBoxes: EdgeBox[] mnemonicBox?: EdgeBox rootKeyBox?: EdgeBox syncKeyBox?: EdgeBox - vouchers: VoucherDump[] + vouchers: EdgeVoucherDump[] // Obsolete: pinBox?: EdgeBox @@ -95,16 +99,9 @@ export interface LoginDump { pinKeyBox?: EdgeBox } -export interface FakeUser { - lastLogin?: Date - loginId: Uint8Array - loginKey: Uint8Array - repos: { [repo: string]: { [path: string]: EdgeBox } } - server: LoginDump - username?: string -} +export const asEdgeRepoDump: Cleaner = asObject(asEdgeBox) -export const asVoucherDump: Cleaner = asObject({ +export const asEdgeVoucherDump: Cleaner = asObject({ // Identity: loginId: asBase64, voucherAuth: asBase64, @@ -121,15 +118,15 @@ export const asVoucherDump: Cleaner = asObject({ deviceDescription: asOptional(asString) }) -export const asLoginDump: Cleaner = asObject({ +export const asEdgeLoginDump: Cleaner = asObject({ // Identity: appId: asString, - created: raw => (raw == null ? new Date() : asDate(raw)), + created: asOptional(asDate, () => new Date()), loginId: asBase64, // Nested logins: children: asOptional( - asArray(raw => asLoginDump(raw)), + asArray(raw => asEdgeLoginDump(raw)), () => [] ), parentBox: asOptional(asEdgeBox), @@ -175,7 +172,7 @@ export const asLoginDump: Cleaner = asObject({ mnemonicBox: asOptional(asEdgeBox), rootKeyBox: asOptional(asEdgeBox), syncKeyBox: asOptional(asEdgeBox), - vouchers: asOptional(asArray(asVoucherDump), () => []), + vouchers: asOptional(asArray(asEdgeVoucherDump), () => []), // Obsolete: pinBox: asOptional(asEdgeBox), @@ -183,18 +180,5 @@ export const asLoginDump: Cleaner = asObject({ pinKeyBox: asOptional(asEdgeBox) }) -export const asFakeUser: Cleaner = asObject({ - lastLogin: asOptional(asDateObject), - loginId: asBase64, - loginKey: asBase64, - repos: asObject(asObject(asEdgeBox)), - server: asLoginDump, - username: asOptional(asUsername) -}) - -export const asFakeUsers = asArray(asFakeUser) - -function asDateObject(raw: unknown): Date { - if (raw instanceof Date) return raw - throw new TypeError('Expecting a Date') -} +export const wasEdgeLoginDump = uncleaner(asEdgeLoginDump) +export const wasEdgeRepoDump = uncleaner(asEdgeRepoDump) diff --git a/src/types/server-cleaners.ts b/src/types/server-cleaners.ts index 4a8bfa675..4bc52e367 100644 --- a/src/types/server-cleaners.ts +++ b/src/types/server-cleaners.ts @@ -9,7 +9,8 @@ import { asOptional, asString, asUnknown, - asValue + asValue, + uncleaner } from 'cleaners' import { base16, base32, base64 } from 'rfc4648' @@ -81,6 +82,7 @@ export const asEdgePendingVoucher: Cleaner = asObject({ deviceDescription: asOptional(asString) }) +/** @deprecated The GUI provides its own localized strings now. */ const asEdgeRecoveryQuestionChoice: Cleaner = asObject({ min_length: asNumber, @@ -94,8 +96,8 @@ const asEdgeRecoveryQuestionChoice: Cleaner = export const asEdgeBox: Cleaner = asObject({ encryptionType: asNumber, - data_base64: asString, - iv_hex: asString + data_base64: asBase64, + iv_hex: asBase16 }) export const asEdgeSnrp: Cleaner = asObject({ @@ -171,7 +173,7 @@ export const asLoginResponseBody: Cleaner = asObject({ // The response payload: results: asOptional(asUnknown), - // What of response is this (success or failure)?: + // What type of response is this (success or failure)?: status_code: asNumber, message: asString }) @@ -249,7 +251,7 @@ export const asCreateLoginPayload: Cleaner = asObject({ appId: asString, loginId: asBase64, parentBox: asOptional(asEdgeBox) -}).withRest +}) // --------------------------------------------------------------------- // response payloads @@ -342,6 +344,7 @@ export const asPasswordErrorPayload: Cleaner = asObject({ wait_seconds: asOptional(asNumber) }) +/** @deprecated The GUI provides its own localized strings now. */ export const asQuestionChoicesPayload: Cleaner = asArray(asEdgeRecoveryQuestionChoice) @@ -350,6 +353,8 @@ export const asRecovery2InfoPayload: Cleaner = asObject({ }) export const asUsernameInfoPayload: Cleaner = asObject({ + loginId: asBase64, + // Password login: passwordAuthSnrp: asOptional(asEdgeSnrp), @@ -358,3 +363,73 @@ export const asUsernameInfoPayload: Cleaner = asObject({ questionKeySnrp: asOptional(asEdgeSnrp), recoveryAuthSnrp: asOptional(asEdgeSnrp) }) + +// --------------------------------------------------------------------- +// uncleaners +// --------------------------------------------------------------------- + +// Common types: +export const wasEdgeBox = uncleaner(asEdgeBox) +export const wasEdgeLobbyReply = uncleaner(asEdgeLobbyReply) +export const wasEdgeLobbyRequest = + uncleaner(asEdgeLobbyRequest) + +// Top-level request / response bodies: +export const wasLoginRequestBody = + uncleaner(asLoginRequestBody) +export const wasLoginResponseBody = + uncleaner(asLoginResponseBody) + +// Request payloads: +export const wasChangeOtpPayload = + uncleaner(asChangeOtpPayload) +export const wasChangePasswordPayload = uncleaner( + asChangePasswordPayload +) +export const wasChangePin2IdPayload = uncleaner( + asChangePin2IdPayload +) +export const wasChangePin2Payload = + uncleaner(asChangePin2Payload) +export const wasChangeRecovery2IdPayload = uncleaner( + asChangeRecovery2IdPayload +) +export const wasChangeRecovery2Payload = uncleaner( + asChangeRecovery2Payload +) +export const wasChangeSecretPayload = uncleaner( + asChangeSecretPayload +) +export const wasChangeUsernamePayload = uncleaner( + asChangeUsernamePayload +) +export const wasChangeVouchersPayload = uncleaner( + asChangeVouchersPayload +) +export const wasCreateKeysPayload = + uncleaner(asCreateKeysPayload) +export const wasCreateLoginPayload = + uncleaner(asCreateLoginPayload) + +// Response payloads: +export const wasChallengeErrorPayload = uncleaner( + asChallengeErrorPayload +) +export const wasLobbyPayload = uncleaner(asLobbyPayload) +export const wasLoginPayload = uncleaner(asLoginPayload) +export const wasMessagesPayload = uncleaner(asMessagesPayload) +export const wasOtpErrorPayload = uncleaner(asOtpErrorPayload) +export const wasOtpResetPayload = uncleaner(asOtpResetPayload) +export const wasPasswordErrorPayload = uncleaner( + asPasswordErrorPayload +) +/** @deprecated The GUI provides its own localized strings now. */ +export const wasQuestionChoicesPayload = uncleaner( + asQuestionChoicesPayload +) +export const wasRecovery2InfoPayload = uncleaner( + asRecovery2InfoPayload +) +export const wasUsernameInfoPayload = uncleaner( + asUsernameInfoPayload +) diff --git a/src/types/server-types.ts b/src/types/server-types.ts index 6ca4128ed..a6fe055a9 100644 --- a/src/types/server-types.ts +++ b/src/types/server-types.ts @@ -9,8 +9,8 @@ import type { EdgePendingVoucher, EdgeRecoveryQuestionChoice } from './types' */ export interface EdgeBox { encryptionType: number - data_base64: string - iv_hex: string + data_base64: Uint8Array + iv_hex: Uint8Array } /** @@ -300,6 +300,7 @@ export interface PasswordErrorPayload { /** * A list of recovery questions the user can pick from. + * @deprecated The GUI provides its own localized strings now. */ export type QuestionChoicesPayload = EdgeRecoveryQuestionChoice[] @@ -314,6 +315,8 @@ export interface Recovery2InfoPayload { * Returned when fetching the password hashing options for an account. */ export interface UsernameInfoPayload { + loginId: Uint8Array + // Password login: passwordAuthSnrp?: EdgeSnrp diff --git a/src/types/types.ts b/src/types/types.ts index 7e176f52c..4f1fa0009 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -8,6 +8,9 @@ import type { import type { Subscriber } from 'yaob' export * from './error' +export * from './fake-types' +export * from './server-cleaners' +export * from './server-types' // --------------------------------------------------------------------- // helper types @@ -1600,6 +1603,7 @@ export interface EdgeContextOptions { skipBlockHeight?: boolean } +/** @deprecated The GUI provides its own localized strings now. */ export interface EdgeRecoveryQuestionChoice { category: 'address' | 'must' | 'numeric' | 'recovery2' | 'string' min_length: number @@ -1737,6 +1741,7 @@ export interface EdgeContext { recovery2Key: string, username: string ) => Promise + /** @deprecated The GUI provides its own localized strings now. */ readonly listRecoveryQuestionChoices: () => Promise< EdgeRecoveryQuestionChoice[] > @@ -1802,12 +1807,14 @@ export interface EdgeFakeContextOptions { * on the fake unit-testing server. */ export interface EdgeFakeUser { - username: string + username?: string lastLogin?: Date loginId: string // base64 loginKey: string // base64 - repos: { [repo: string]: { [path: string]: any /* asEdgeBox */ } } - server: any // asLoginDump + repos: { + [syncKey: string]: unknown // Cleaned with asEdgeRepoDump + } + server: unknown // Cleaned with asEdgeLoginDump } export interface EdgeFakeWorld { diff --git a/src/util/crypto/crypto.ts b/src/util/crypto/crypto.ts index d889c74c1..42cab8a43 100644 --- a/src/util/crypto/crypto.ts +++ b/src/util/crypto/crypto.ts @@ -1,5 +1,4 @@ import aesjs from 'aes-js' -import { base16, base64 } from 'rfc4648' import { EdgeBox } from '../../types/server-types' import { EdgeIo } from '../../types/types' @@ -30,8 +29,8 @@ export function decrypt(box: EdgeBox, key: Uint8Array): Uint8Array { if (box.encryptionType !== 0) { throw new Error('Unknown encryption type') } - const iv = base16.parse(box.iv_hex) - const ciphertext = base64.parse(box.data_base64) + const iv = box.iv_hex + const ciphertext = box.data_base64 // Decrypt: const cipher = new AesCbc(key, iv) @@ -115,8 +114,8 @@ export function encrypt( const ciphertext = cipher.encrypt(raw) return { encryptionType: 0, - iv_hex: base16.stringify(iv), - data_base64: base64.stringify(ciphertext) + iv_hex: iv, + data_base64: ciphertext } } diff --git a/test/core/context/context.test.ts b/test/core/context/context.test.ts index 9e82b77fa..20ec0c401 100644 --- a/test/core/context/context.test.ts +++ b/test/core/context/context.test.ts @@ -93,17 +93,18 @@ describe('context', function () { // Do the dump: const dump = await world.dumpFakeUser(account) + // Get rid of extra `undefined` fields: + const server = JSON.parse(JSON.stringify(dump.server)) + // The PIN login upgrades the account, so the dump will have extra stuff: - expect(dump.server.loginAuthBox != null).equals(true) - expect(dump.server.loginAuth != null).equals(true) - dump.server.loginAuthBox = undefined - dump.server.loginAuth = undefined + expect(server.loginAuthBox != null).equals(true) + expect(server.loginAuth != null).equals(true) + delete server.loginAuthBox + delete server.loginAuth - // Get rid of extra `undefined` fields: - dump.server = JSON.parse(JSON.stringify(dump.server)) + expect({ ...dump, server }).deep.equals(fakeUserDump) // require('fs').writeFileSync('./fake-user.json', JSON.stringify(dump)) - expect(dump).deep.equals(fakeUserDump) }) }) diff --git a/test/util/crypto/crypto.test.ts b/test/util/crypto/crypto.test.ts index 05a302c55..6b6ef1391 100644 --- a/test/util/crypto/crypto.test.ts +++ b/test/util/crypto/crypto.test.ts @@ -3,6 +3,7 @@ import { describe, it } from 'mocha' import { base16 } from 'rfc4648' import { makeFakeIo } from '../../../src/index' +import { asEdgeBox } from '../../../src/types/server-cleaners' import { decrypt, decryptText, encrypt } from '../../../src/util/crypto/crypto' import { utf8 } from '../../../src/util/encoding' @@ -11,12 +12,12 @@ describe('encryption', function () { const key = base16.parse( '002688cc350a5333a87fa622eacec626c3d1c0ebf9f3793de3885fa254d7e393' ) - const box = { + const box = asEdgeBox({ data_base64: 'X08Snnou2PrMW21ZNyJo5C8StDjTNgMtuEoAJL5bJ6LDPdZGQLhjaUMetOknaPYnmfBCHNQ3ApqmE922Hkp30vdxzXBloopfPLJKdYwQxURYNbiL4TvNakP7i0bnTlIsR7bj1q/65ZyJOW1HyOKV/tmXCf56Fhe3Hcmb/ebsBF72FZr3jX5pkSBO+angK15IlCIiem1kPi6QmzyFtMB11i0GTjSS67tLrWkGIqAmik+bGqy7WtQgfMRxQNNOxePPSHHp09431Ogrc9egY3txnBN2FKnfEM/0Wa/zLWKCVQXCGhmrTx1tmf4HouNDOnnCgkRWJYs8FJdrDP8NZy4Fkzs7FoH7RIaUiOvosNKMil1CBknKremP6ohK7SMLGoOHpv+bCgTXcAeB3P4Slx3iy+RywTSLb3yh+HDo6bwt+vhujP0RkUamI5523bwz3/7vLO8BzyF6WX0By2s4gvMdFQ==', encryptionType: 0, iv_hex: '96a4cd52670c13df9712fdc1b564d44b' - } + }) expect(decrypt(box, key)).deep.equals(utf8.parse('payload')) })