From 65a7536efabc68a50006f5358bc0611f19173251 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 12:29:40 -0800 Subject: [PATCH 01/71] create function that validates appState and returns list of errors as human interpretable strings --- apps/passport-client/src/validateState.ts | 74 +++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 apps/passport-client/src/validateState.ts diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts new file mode 100644 index 0000000000..29416077a2 --- /dev/null +++ b/apps/passport-client/src/validateState.ts @@ -0,0 +1,74 @@ +import { + SemaphoreIdentityPCD, + SemaphoreIdentityPCDPackage +} from "@pcd/semaphore-identity-pcd"; +import { AppState } from "./state"; + +/** + * Determines whether the appState object contains valid data. If it does not, + * returns the set of things that are incorrect about it in an array of human + * interpretable strings. + */ +export function validateState(appState: AppState): string[] { + const validationErrors: string[] = []; + + if (!appState.self) { + validationErrors.push("missing 'self' field from app state"); + } + + if (!appState.identity) { + validationErrors.push("missing 'identity' field from app state"); + } + + if (!appState.encryptionKey) { + validationErrors.push("missing 'encryption' field from app state key"); + } + + if (!appState.pcds) { + validationErrors.push("missing 'pcds' field from app state"); + } + + if (appState.pcds.size() === 0) { + validationErrors.push("'pcds' field in app state contains no pcds"); + } + + const identityPCDFromCollection = appState.pcds.getPCDsByType( + SemaphoreIdentityPCDPackage.name + )[0] as SemaphoreIdentityPCD | undefined; + + if (!identityPCDFromCollection) { + validationErrors.push( + "'pcds' field in app state does not contain an identity PCD" + ); + } + + const identityFromPCDCollection = identityPCDFromCollection?.claim?.identity; + const commitmentOfIdentityPCDInCollection = + identityFromPCDCollection?.commitment?.toString(); + const commitmentFromSelfField = appState?.self?.commitment; + const commitmentFromIdentityField = + appState?.identity?.commitment?.toString(); + + if (commitmentOfIdentityPCDInCollection !== commitmentFromSelfField) { + validationErrors.push( + `commitment of identity pcd in collection (${commitmentOfIdentityPCDInCollection})` + + ` does not match commitment in 'self' field of app state (${commitmentFromSelfField})` + ); + } + + if (commitmentFromSelfField !== commitmentFromIdentityField) { + validationErrors.push( + `commitment in 'self' field of app state (${commitmentFromSelfField})` + + ` does not match commitment of 'identity' field of app state (${commitmentFromIdentityField})` + ); + } + + if (commitmentFromIdentityField !== commitmentOfIdentityPCDInCollection) { + validationErrors.push( + `commitment of 'identity' field of app state (${commitmentFromIdentityField})` + + ` does not match commitment of identity pcd in collection (${commitmentOfIdentityPCDInCollection})` + ); + } + + return validationErrors; +} From 144f2ebe3d693be12f8682a701ead31b955f4163 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 12:36:32 -0800 Subject: [PATCH 02/71] handle state validation errors in inital state load --- apps/passport-client/pages/index.tsx | 14 +++++++++++++- apps/passport-client/src/validateState.ts | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/apps/passport-client/pages/index.tsx b/apps/passport-client/pages/index.tsx index 3f7b7a661b..fde3b8b736 100644 --- a/apps/passport-client/pages/index.tsx +++ b/apps/passport-client/pages/index.tsx @@ -66,6 +66,10 @@ import { import { registerServiceWorker } from "../src/registerServiceWorker"; import { AppState, StateEmitter } from "../src/state"; import { pollUser } from "../src/user"; +import { + logAndUploadValidationErrors, + validateState +} from "../src/validateState"; class App extends React.Component { state = undefined as AppState | undefined; @@ -424,7 +428,7 @@ async function loadInitialState(): Promise { const persistentSyncStatus = loadPersistentSyncStatus(); - return { + const state: AppState = { self, encryptionKey, pcds, @@ -439,6 +443,14 @@ async function loadInitialState(): Promise { serverStorageRevision: persistentSyncStatus.serverStorageRevision, serverStorageHash: persistentSyncStatus.serverStorageHash }; + + const validationErrors = validateState(state); + if (validationErrors.length > 0) { + state.userInvalid = true; + logAndUploadValidationErrors(validationErrors); + } + + return state; } registerServiceWorker(); diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 29416077a2..b5e92ce7e4 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -1,7 +1,9 @@ +import { requestLogToServer } from "@pcd/passport-interface"; import { SemaphoreIdentityPCD, SemaphoreIdentityPCDPackage } from "@pcd/semaphore-identity-pcd"; +import { appConfig } from "./appConfig"; import { AppState } from "./state"; /** @@ -72,3 +74,16 @@ export function validateState(appState: AppState): string[] { return validationErrors; } + +export async function logAndUploadValidationErrors( + errors: string[] +): Promise { + try { + console.log(`encountered state validation errors: `, errors); + await requestLogToServer(appConfig.zupassServer, "state-validation-error", { + errors + }); + } catch (e) { + console.log("error reporting errors", e); + } +} From 0aa824c4dc18ad7b47162ed28a8dcfd01dbc7f40 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 13:01:41 -0800 Subject: [PATCH 03/71] commented some locations where validation logic should be added --- apps/passport-client/src/dispatch.ts | 2 ++ apps/passport-client/src/localstorage.ts | 2 ++ apps/passport-client/src/useSyncE2EEStorage.tsx | 3 ++- .../src/api/requestDownloadAndDecryptStorage.ts | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index a80fea8cee..122e5808f2 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -341,6 +341,7 @@ async function finishAccountCreation( state: AppState, update: ZuUpdate ) { + // validation point // Verify that the identity is correct. const { identity } = state; console.log("[ACCOUNT] Check user", identity, user); @@ -493,6 +494,7 @@ async function loadAfterLogin( storage: StorageWithRevision, update: ZuUpdate ) { + // validation point const { pcds, subscriptions, storageHash } = await deserializeStorage( storage.storage, await getPackages() diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index 2ee70bf98a..cda2becd02 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -13,6 +13,8 @@ import { Identity } from "@semaphore-protocol/identity"; import { z } from "zod"; import { getPackages } from "./pcdPackages"; +// validation points in this file + const OLD_PCDS_KEY = "pcds"; // deprecated const COLLECTION_KEY = "pcd_collection"; diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index 3c017c8482..619121e22d 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -116,7 +116,7 @@ export async function uploadStorage( } /** - * Uploads the state of this passport, in serialied form as produced by + * Uploads the state of this passport, in serialized form as produced by * serializeStorage(). */ export async function uploadSerializedStorage( @@ -131,6 +131,7 @@ export async function uploadSerializedStorage( encryptionKey ); + // validation point const uploadResult = await requestUploadEncryptedStorage( appConfig.zupassServer, blobKey, diff --git a/packages/passport-interface/src/api/requestDownloadAndDecryptStorage.ts b/packages/passport-interface/src/api/requestDownloadAndDecryptStorage.ts index a464e6484f..2c57e11ec9 100644 --- a/packages/passport-interface/src/api/requestDownloadAndDecryptStorage.ts +++ b/packages/passport-interface/src/api/requestDownloadAndDecryptStorage.ts @@ -83,6 +83,7 @@ export async function requestDownloadAndDecryptUpdatedStorage( encryptionKey: string, knownRevision: string | undefined ): Promise { + // validation point try { const encryptionKeyHash = await getHash(encryptionKey); const storageResult = await requestEncryptedStorage( From 6c55e49dfe6ce117352d4c3173fbc4c478af5f91 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 13:14:17 -0800 Subject: [PATCH 04/71] more validation --- .../src/useSyncE2EEStorage.tsx | 11 ++++++++ apps/passport-client/src/validateState.ts | 25 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index 619121e22d..f01eab8c5b 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -28,6 +28,10 @@ import { } from "./localstorage"; import { getPackages } from "./pcdPackages"; import { useOnStateChange } from "./subscribe"; +import { + logAndUploadValidationErrors, + validatePCDCollection +} from "./validateState"; export type UpdateBlobKeyStorageInfo = { revision: string; @@ -204,6 +208,13 @@ export async function downloadStorage( storageResult.value.storage, await getPackages() ); + + const validationErrors = validatePCDCollection(pcds); + if (validationErrors.length > 0) { + logAndUploadValidationErrors(validationErrors); + throw new Error("validation errors:\n" + validationErrors.join("\n")); + } + await savePCDs(pcds); await saveSubscriptions(subscriptions); savePersistentSyncStatus({ diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index b5e92ce7e4..7aaa482fbf 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -1,4 +1,5 @@ import { requestLogToServer } from "@pcd/passport-interface"; +import { PCDCollection } from "@pcd/pcd-collection"; import { SemaphoreIdentityPCD, SemaphoreIdentityPCDPackage @@ -75,6 +76,30 @@ export function validateState(appState: AppState): string[] { return validationErrors; } +export function validatePCDCollection(pcdCollection?: PCDCollection): string[] { + const validationErrors: string[] = []; + + if (!pcdCollection) { + validationErrors.push("missing 'pcds' field from app state"); + } + + if (pcdCollection.size() === 0) { + validationErrors.push("'pcds' field in app state contains no pcds"); + } + + const identityPCDFromCollection = pcdCollection.getPCDsByType( + SemaphoreIdentityPCDPackage.name + )[0] as SemaphoreIdentityPCD | undefined; + + if (!identityPCDFromCollection) { + validationErrors.push( + "'pcds' field in app state does not contain an identity PCD" + ); + } + + return validationErrors; +} + export async function logAndUploadValidationErrors( errors: string[] ): Promise { From dc0d6ff1677669189703153338a69005e7730841 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 13:26:04 -0800 Subject: [PATCH 05/71] added more validation --- apps/passport-client/src/dispatch.ts | 7 ++++ .../src/useSyncE2EEStorage.tsx | 17 +++++++- apps/passport-client/src/validateState.ts | 40 +++++++++++++++---- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 122e5808f2..5371a78d19 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -52,6 +52,7 @@ import { uploadStorage } from "./useSyncE2EEStorage"; import { assertUnreachable } from "./util"; +import { logAndUploadValidationErrors, validateUpload } from "./validateState"; export type Dispatcher = (action: Action) => void; @@ -758,6 +759,12 @@ async function doSync( state.pcds, state.subscriptions ); + const validationErrors = validateUpload(state.self, state.pcds); + if (validationErrors.length > 0) { + logAndUploadValidationErrors(validationErrors); + // TODO: what's the proper thing to do here? + } + if (state.serverStorageHash !== appStorage.storageHash) { console.log("[SYNC] sync action: upload"); // TODO(artwyman): Add serverStorageRevision input as knownRevision here, diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index f01eab8c5b..41e57fe536 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -30,7 +30,8 @@ import { getPackages } from "./pcdPackages"; import { useOnStateChange } from "./subscribe"; import { logAndUploadValidationErrors, - validatePCDCollection + validatePCDCollection, + validateUpload } from "./validateState"; export type UpdateBlobKeyStorageInfo = { @@ -111,6 +112,19 @@ export async function uploadStorage( pcds: PCDCollection, subscriptions: FeedSubscriptionManager ): Promise { + const validationErrors = validateUpload(user, pcds); + if (validationErrors.length > 0) { + logAndUploadValidationErrors(validationErrors); + return { + success: false, + error: { + name: "ValidationError", + detailedMessage: + "upload validation failed because\n: " + validationErrors.join("\n"), + code: undefined + } + }; + } const { serializedStorage, storageHash } = await serializeStorage( user, pcds, @@ -135,7 +149,6 @@ export async function uploadSerializedStorage( encryptionKey ); - // validation point const uploadResult = await requestUploadEncryptedStorage( appConfig.zupassServer, blobKey, diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 7aaa482fbf..254915775f 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -1,4 +1,4 @@ -import { requestLogToServer } from "@pcd/passport-interface"; +import { User, requestLogToServer } from "@pcd/passport-interface"; import { PCDCollection } from "@pcd/pcd-collection"; import { SemaphoreIdentityPCD, @@ -76,24 +76,48 @@ export function validateState(appState: AppState): string[] { return validationErrors; } -export function validatePCDCollection(pcdCollection?: PCDCollection): string[] { +export function validatePCDCollection(pcds?: PCDCollection): string[] { const validationErrors: string[] = []; - if (!pcdCollection) { - validationErrors.push("missing 'pcds' field from app state"); + if (!pcds) { + validationErrors.push("pcd collection is absent"); } - if (pcdCollection.size() === 0) { - validationErrors.push("'pcds' field in app state contains no pcds"); + if (pcds.size() === 0) { + validationErrors.push("pcd collection is empty"); } - const identityPCDFromCollection = pcdCollection.getPCDsByType( + const identityPCDFromCollection = pcds.getPCDsByType( SemaphoreIdentityPCDPackage.name )[0] as SemaphoreIdentityPCD | undefined; if (!identityPCDFromCollection) { + validationErrors.push("pcd collection does not contain an identity pcd"); + } + + return validationErrors; +} + +export function validateUpload(user?: User, pcds?: PCDCollection): string[] { + const validationErrors: string[] = []; + + if (!user) { + validationErrors.push(`upload must include a user`); + } + + if (!pcds) { + validationErrors.push(`upload must include a pcd collection`); + } + + const identityPCDFromCollection = pcds.getPCDsByType( + SemaphoreIdentityPCDPackage.name + )[0] as SemaphoreIdentityPCD | undefined; + const commitmentFromPCDCollection = + identityPCDFromCollection?.claim?.identity?.commitment?.toString(); + + if (user?.commitment !== commitmentFromPCDCollection) { validationErrors.push( - "'pcds' field in app state does not contain an identity PCD" + "user commitment does not equal to commitment of identity pcd in pcd collection" ); } From 3db82d9f5d408063060fe63d33d6193755c0acc4 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 13:32:17 -0800 Subject: [PATCH 06/71] implemented another validation point --- apps/passport-client/src/dispatch.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 5371a78d19..298d37ee55 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -52,7 +52,11 @@ import { uploadStorage } from "./useSyncE2EEStorage"; import { assertUnreachable } from "./util"; -import { logAndUploadValidationErrors, validateUpload } from "./validateState"; +import { + logAndUploadValidationErrors, + validatePCDCollection, + validateUpload +} from "./validateState"; export type Dispatcher = (action: Action) => void; @@ -495,11 +499,16 @@ async function loadAfterLogin( storage: StorageWithRevision, update: ZuUpdate ) { - // validation point const { pcds, subscriptions, storageHash } = await deserializeStorage( storage.storage, await getPackages() ); + const validationErrors = validatePCDCollection(pcds); + if (validationErrors.length > 0) { + logAndUploadValidationErrors(validationErrors); + // TODO: what's the right error message here? + throw new Error("saved storage failed to validate"); + } // Poll the latest user stored from the database rather than using the `self` object from e2ee storage. const userResponse = await requestUser( From 910a334a0d11be1f416ca06228491f9855ad1af1 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 13:41:14 -0800 Subject: [PATCH 07/71] more validation --- apps/passport-client/src/dispatch.ts | 12 ++++--- apps/passport-client/src/validateState.ts | 38 +++++++++++++++++------ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 298d37ee55..25bddfbc64 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -54,6 +54,7 @@ import { import { assertUnreachable } from "./util"; import { logAndUploadValidationErrors, + validateNewAccount, validatePCDCollection, validateUpload } from "./validateState"; @@ -346,18 +347,17 @@ async function finishAccountCreation( state: AppState, update: ZuUpdate ) { - // validation point // Verify that the identity is correct. - const { identity } = state; - console.log("[ACCOUNT] Check user", identity, user); - if (identity == null || identity.commitment.toString() !== user.commitment) { + const validationErrors = validateNewAccount(user, state); + if (validationErrors.length > 0) { + logAndUploadValidationErrors(validationErrors); update({ error: { title: "Invalid identity", message: "Something went wrong saving your Zupass. Contact support." } }); - return; // Don't save the bad identity. User must reset account. + return; } // Save PCDs to E2EE storage. @@ -373,6 +373,8 @@ async function finishAccountCreation( serverStorageRevision: uploadResult.value.revision, serverStorageHash: uploadResult.value.storageHash }); + } else { + // TODO: anything to add here? } // Save user to local storage. This is done last because it unblocks diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 254915775f..89599d267f 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -12,30 +12,30 @@ import { AppState } from "./state"; * returns the set of things that are incorrect about it in an array of human * interpretable strings. */ -export function validateState(appState: AppState): string[] { +export function validateState(state: AppState): string[] { const validationErrors: string[] = []; - if (!appState.self) { + if (!state.self) { validationErrors.push("missing 'self' field from app state"); } - if (!appState.identity) { + if (!state.identity) { validationErrors.push("missing 'identity' field from app state"); } - if (!appState.encryptionKey) { + if (!state.encryptionKey) { validationErrors.push("missing 'encryption' field from app state key"); } - if (!appState.pcds) { + if (!state.pcds) { validationErrors.push("missing 'pcds' field from app state"); } - if (appState.pcds.size() === 0) { + if (state.pcds.size() === 0) { validationErrors.push("'pcds' field in app state contains no pcds"); } - const identityPCDFromCollection = appState.pcds.getPCDsByType( + const identityPCDFromCollection = state.pcds.getPCDsByType( SemaphoreIdentityPCDPackage.name )[0] as SemaphoreIdentityPCD | undefined; @@ -48,9 +48,8 @@ export function validateState(appState: AppState): string[] { const identityFromPCDCollection = identityPCDFromCollection?.claim?.identity; const commitmentOfIdentityPCDInCollection = identityFromPCDCollection?.commitment?.toString(); - const commitmentFromSelfField = appState?.self?.commitment; - const commitmentFromIdentityField = - appState?.identity?.commitment?.toString(); + const commitmentFromSelfField = state?.self?.commitment; + const commitmentFromIdentityField = state?.identity?.commitment?.toString(); if (commitmentOfIdentityPCDInCollection !== commitmentFromSelfField) { validationErrors.push( @@ -124,6 +123,25 @@ export function validateUpload(user?: User, pcds?: PCDCollection): string[] { return validationErrors; } +export function validateNewAccount(user: User, state: AppState): string[] { + const validationErrors: string[] = []; + + if (!state.identity) { + validationErrors.push("app state missing identity field"); + } + + const stateIdentityCommitment = state.identity?.commitment?.toString(); + const userIdentityCommitment = user?.commitment; + + if (stateIdentityCommitment !== userIdentityCommitment) { + validationErrors.push( + `app state identity (${stateIdentityCommitment}) does not match newly created user's commitment (${userIdentityCommitment})` + ); + } + + return validationErrors; +} + export async function logAndUploadValidationErrors( errors: string[] ): Promise { From 0629d46e065e831dc783eabdedd5a39c1a67b5b1 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 13:43:36 -0800 Subject: [PATCH 08/71] undo a change --- .../src/api/requestDownloadAndDecryptStorage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/passport-interface/src/api/requestDownloadAndDecryptStorage.ts b/packages/passport-interface/src/api/requestDownloadAndDecryptStorage.ts index 2c57e11ec9..a464e6484f 100644 --- a/packages/passport-interface/src/api/requestDownloadAndDecryptStorage.ts +++ b/packages/passport-interface/src/api/requestDownloadAndDecryptStorage.ts @@ -83,7 +83,6 @@ export async function requestDownloadAndDecryptUpdatedStorage( encryptionKey: string, knownRevision: string | undefined ): Promise { - // validation point try { const encryptionKeyHash = await getHash(encryptionKey); const storageResult = await requestEncryptedStorage( From 4502d33c5d49487b538493f0d24eda4de3fa468f Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 13:48:12 -0800 Subject: [PATCH 09/71] add some validation logic inside of localstorage saving and loading of pcd collection --- apps/passport-client/src/localstorage.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index cda2becd02..26c3cdce85 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -12,6 +12,10 @@ import { SemaphoreSignaturePCD } from "@pcd/semaphore-signature-pcd"; import { Identity } from "@semaphore-protocol/identity"; import { z } from "zod"; import { getPackages } from "./pcdPackages"; +import { + logAndUploadValidationErrors, + validatePCDCollection +} from "./validateState"; // validation points in this file @@ -19,6 +23,11 @@ const OLD_PCDS_KEY = "pcds"; // deprecated const COLLECTION_KEY = "pcd_collection"; export async function savePCDs(pcds: PCDCollection): Promise { + const validationErrors = validatePCDCollection(pcds); + if (validationErrors.length > 0) { + logAndUploadValidationErrors(validationErrors); + throw new Error("couldn't save PCDs\n:" + validationErrors.join("\n")); + } const serialized = await pcds.serializeCollection(); window.localStorage[COLLECTION_KEY] = serialized; } @@ -33,10 +42,18 @@ export async function loadPCDs(): Promise { } const serializedCollection = window.localStorage[COLLECTION_KEY]; - return await PCDCollection.deserialize( + const collection = await PCDCollection.deserialize( await getPackages(), serializedCollection ?? "{}" ); + + const validationErrors = validatePCDCollection(collection); + if (validationErrors.length > 0) { + logAndUploadValidationErrors(validationErrors); + throw new Error("couldn't save PCDs\n:" + validationErrors.join("\n")); + } + + return collection; } export async function saveSubscriptions( From 91b5ca1e6785efe0f7295d987c0e6589d0672c0f Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 14:11:55 -0800 Subject: [PATCH 10/71] add a bunch of comments --- apps/passport-client/src/validateState.ts | 24 ++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 89599d267f..cc1fd035f7 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -8,9 +8,10 @@ import { appConfig } from "./appConfig"; import { AppState } from "./state"; /** - * Determines whether the appState object contains valid data. If it does not, - * returns the set of things that are incorrect about it in an array of human - * interpretable strings. + * Determines whether the app's global state as represented by {@link AppState} object + * contains valid data. If it does not, returns the set of things that are incorrect about + * it in an array of human interpretable strings. If there are no errors, returns an + * empty array. */ export function validateState(state: AppState): string[] { const validationErrors: string[] = []; @@ -75,6 +76,11 @@ export function validateState(state: AppState): string[] { return validationErrors; } +/** + * Validates a {@link PCDCollection} by checking its contents. Does verify that the collection + * contains PCDs that are consistent with the rest of the application state. Returns a list of + * strings representing individual errors. If there are no errors, returns an empty array. + */ export function validatePCDCollection(pcds?: PCDCollection): string[] { const validationErrors: string[] = []; @@ -123,6 +129,13 @@ export function validateUpload(user?: User, pcds?: PCDCollection): string[] { return validationErrors; } +/** + * Validates whether a user returned by the Zupass server API is consistent + * with the local application state representation. Returns errors as strings, + * and returns an empty array if the two are not inconsistent. Does not validate + * {@link state} in its entirety, only that the {@link user} and {@link state} + * are consistent. + */ export function validateNewAccount(user: User, state: AppState): string[] { const validationErrors: string[] = []; @@ -142,6 +155,11 @@ export function validateNewAccount(user: User, state: AppState): string[] { return validationErrors; } +/** + * Logs validation errors to the console, and uploads them to the server so that + * we have records and are able to identify common types of errors. Does not leak + * sensitive information, such as decrypted versions of e2ee storage. + */ export async function logAndUploadValidationErrors( errors: string[] ): Promise { From 71dc18a00c8afbdd281ea2210a27501dae59fdd4 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 11 Dec 2023 14:13:29 -0800 Subject: [PATCH 11/71] added account export button to the invalid user modal --- .../passport-client/components/modals/InvalidUserModal.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/passport-client/components/modals/InvalidUserModal.tsx b/apps/passport-client/components/modals/InvalidUserModal.tsx index 6d54040118..d95aacfdbd 100644 --- a/apps/passport-client/components/modals/InvalidUserModal.tsx +++ b/apps/passport-client/components/modals/InvalidUserModal.tsx @@ -3,11 +3,12 @@ import { useCallback } from "react"; import styled from "styled-components"; import { useDispatch } from "../../src/appHooks"; import { Button, H2 } from "../core"; +import { AccountExportButton } from "../shared/AccountExportButton"; export function InvalidUserModal() { const dispatch = useDispatch(); - const onClick = useCallback(() => { + const onExitClick = useCallback(() => { dispatch({ type: "reset-passport" }); }, [dispatch]); @@ -21,7 +22,9 @@ export function InvalidUserModal() { existing Zupass account onto this device.

- + + + ); } From 41bbdc131f34435fe50f0fc1a86bec9937846971 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 19:23:31 -0800 Subject: [PATCH 12/71] add explanation to export button --- .../passport-client/components/modals/InvalidUserModal.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/passport-client/components/modals/InvalidUserModal.tsx b/apps/passport-client/components/modals/InvalidUserModal.tsx index d95aacfdbd..2c3f468c7e 100644 --- a/apps/passport-client/components/modals/InvalidUserModal.tsx +++ b/apps/passport-client/components/modals/InvalidUserModal.tsx @@ -21,7 +21,12 @@ export function InvalidUserModal() { one. Click the button below to log out. Then you'll be able to sync your existing Zupass account onto this device.

- + +

+ You can export your account data using the button below in case you need + it later. +

+ From 606780333ca7bba97109eaa2d25c804a3b9cd328 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 19:28:29 -0800 Subject: [PATCH 13/71] respond to some PR comments --- apps/passport-client/pages/index.tsx | 9 ++------- apps/passport-client/src/dispatch.ts | 4 +--- apps/passport-client/src/localstorage.ts | 2 +- apps/passport-client/src/validateState.ts | 11 +++++++++++ 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/passport-client/pages/index.tsx b/apps/passport-client/pages/index.tsx index fde3b8b736..1188dd4715 100644 --- a/apps/passport-client/pages/index.tsx +++ b/apps/passport-client/pages/index.tsx @@ -66,10 +66,7 @@ import { import { registerServiceWorker } from "../src/registerServiceWorker"; import { AppState, StateEmitter } from "../src/state"; import { pollUser } from "../src/user"; -import { - logAndUploadValidationErrors, - validateState -} from "../src/validateState"; +import { validateAndLogState } from "../src/validateState"; class App extends React.Component { state = undefined as AppState | undefined; @@ -444,10 +441,8 @@ async function loadInitialState(): Promise { serverStorageHash: persistentSyncStatus.serverStorageHash }; - const validationErrors = validateState(state); - if (validationErrors.length > 0) { + if (!validateAndLogState(state)) { state.userInvalid = true; - logAndUploadValidationErrors(validationErrors); } return state; diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 25bddfbc64..5bd12faa78 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -357,7 +357,7 @@ async function finishAccountCreation( message: "Something went wrong saving your Zupass. Contact support." } }); - return; + return; // Don't save the bad identity. User must reset account. } // Save PCDs to E2EE storage. @@ -373,8 +373,6 @@ async function finishAccountCreation( serverStorageRevision: uploadResult.value.revision, serverStorageHash: uploadResult.value.storageHash }); - } else { - // TODO: anything to add here? } // Save user to local storage. This is done last because it unblocks diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index 26c3cdce85..2c8d3202e7 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -50,7 +50,7 @@ export async function loadPCDs(): Promise { const validationErrors = validatePCDCollection(collection); if (validationErrors.length > 0) { logAndUploadValidationErrors(validationErrors); - throw new Error("couldn't save PCDs\n:" + validationErrors.join("\n")); + throw new Error("couldn't load PCDs\n:" + validationErrors.join("\n")); } return collection; diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index cc1fd035f7..3b29e96634 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -7,6 +7,17 @@ import { import { appConfig } from "./appConfig"; import { AppState } from "./state"; +export function validateAndLogState(state: AppState): boolean { + const validationErrors = validateState(state); + + if (validationErrors.length > 0) { + logAndUploadValidationErrors(validationErrors); + return false; + } + + return true; +} + /** * Determines whether the app's global state as represented by {@link AppState} object * contains valid data. If it does not, returns the set of things that are incorrect about From 42683774cfb9a85889756b067dca2d84139f8c42 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 19:31:03 -0800 Subject: [PATCH 14/71] some cleanups --- apps/passport-client/src/dispatch.ts | 1 + apps/passport-client/src/localstorage.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 5bd12faa78..d36862443f 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -768,6 +768,7 @@ async function doSync( state.pcds, state.subscriptions ); + const validationErrors = validateUpload(state.self, state.pcds); if (validationErrors.length > 0) { logAndUploadValidationErrors(validationErrors); diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index 2c8d3202e7..5018a968c2 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -17,8 +17,6 @@ import { validatePCDCollection } from "./validateState"; -// validation points in this file - const OLD_PCDS_KEY = "pcds"; // deprecated const COLLECTION_KEY = "pcd_collection"; From e0710a3f9c03ed78fad5eded68be303c5c56385b Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 19:37:52 -0800 Subject: [PATCH 15/71] slight refactor --- apps/passport-client/src/dispatch.ts | 6 +-- apps/passport-client/src/localstorage.ts | 12 ++++-- .../src/useSyncE2EEStorage.tsx | 11 +++-- apps/passport-client/src/validateState.ts | 43 ++++++++++++++----- 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index d36862443f..549a3d4dcf 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -349,7 +349,7 @@ async function finishAccountCreation( ) { // Verify that the identity is correct. const validationErrors = validateNewAccount(user, state); - if (validationErrors.length > 0) { + if (validationErrors.errors.length > 0) { logAndUploadValidationErrors(validationErrors); update({ error: { @@ -504,7 +504,7 @@ async function loadAfterLogin( await getPackages() ); const validationErrors = validatePCDCollection(pcds); - if (validationErrors.length > 0) { + if (validationErrors.errors.length > 0) { logAndUploadValidationErrors(validationErrors); // TODO: what's the right error message here? throw new Error("saved storage failed to validate"); @@ -770,7 +770,7 @@ async function doSync( ); const validationErrors = validateUpload(state.self, state.pcds); - if (validationErrors.length > 0) { + if (validationErrors.errors.length > 0) { logAndUploadValidationErrors(validationErrors); // TODO: what's the proper thing to do here? } diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index 5018a968c2..b1ac362b17 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -22,9 +22,11 @@ const COLLECTION_KEY = "pcd_collection"; export async function savePCDs(pcds: PCDCollection): Promise { const validationErrors = validatePCDCollection(pcds); - if (validationErrors.length > 0) { + if (validationErrors.errors.length > 0) { logAndUploadValidationErrors(validationErrors); - throw new Error("couldn't save PCDs\n:" + validationErrors.join("\n")); + throw new Error( + "couldn't save PCDs\n:" + validationErrors.errors.join("\n") + ); } const serialized = await pcds.serializeCollection(); window.localStorage[COLLECTION_KEY] = serialized; @@ -46,9 +48,11 @@ export async function loadPCDs(): Promise { ); const validationErrors = validatePCDCollection(collection); - if (validationErrors.length > 0) { + if (validationErrors.errors.length > 0) { logAndUploadValidationErrors(validationErrors); - throw new Error("couldn't load PCDs\n:" + validationErrors.join("\n")); + throw new Error( + "couldn't load PCDs\n:" + validationErrors.errors.join("\n") + ); } return collection; diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index 41e57fe536..90646726aa 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -113,14 +113,15 @@ export async function uploadStorage( subscriptions: FeedSubscriptionManager ): Promise { const validationErrors = validateUpload(user, pcds); - if (validationErrors.length > 0) { + if (validationErrors.errors.length > 0) { logAndUploadValidationErrors(validationErrors); return { success: false, error: { name: "ValidationError", detailedMessage: - "upload validation failed because\n: " + validationErrors.join("\n"), + "upload validation failed because\n: " + + validationErrors.errors.join("\n"), code: undefined } }; @@ -223,9 +224,11 @@ export async function downloadStorage( ); const validationErrors = validatePCDCollection(pcds); - if (validationErrors.length > 0) { + if (validationErrors.errors.length > 0) { logAndUploadValidationErrors(validationErrors); - throw new Error("validation errors:\n" + validationErrors.join("\n")); + throw new Error( + "validation errors:\n" + validationErrors.errors.join("\n") + ); } await savePCDs(pcds); diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 3b29e96634..7866f11bfb 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -7,10 +7,15 @@ import { import { appConfig } from "./appConfig"; import { AppState } from "./state"; +export interface ValidationErrors { + errors: string[]; + userUUID: string; +} + export function validateAndLogState(state: AppState): boolean { const validationErrors = validateState(state); - if (validationErrors.length > 0) { + if (validationErrors.errors.length > 0) { logAndUploadValidationErrors(validationErrors); return false; } @@ -24,7 +29,7 @@ export function validateAndLogState(state: AppState): boolean { * it in an array of human interpretable strings. If there are no errors, returns an * empty array. */ -export function validateState(state: AppState): string[] { +export function validateState(state: AppState): ValidationErrors { const validationErrors: string[] = []; if (!state.self) { @@ -84,7 +89,10 @@ export function validateState(state: AppState): string[] { ); } - return validationErrors; + return { + errors: validationErrors, + userUUID: state.self?.uuid + }; } /** @@ -92,7 +100,7 @@ export function validateState(state: AppState): string[] { * contains PCDs that are consistent with the rest of the application state. Returns a list of * strings representing individual errors. If there are no errors, returns an empty array. */ -export function validatePCDCollection(pcds?: PCDCollection): string[] { +export function validatePCDCollection(pcds?: PCDCollection): ValidationErrors { const validationErrors: string[] = []; if (!pcds) { @@ -111,10 +119,16 @@ export function validatePCDCollection(pcds?: PCDCollection): string[] { validationErrors.push("pcd collection does not contain an identity pcd"); } - return validationErrors; + return { + errors: validationErrors, + userUUID: "" + }; } -export function validateUpload(user?: User, pcds?: PCDCollection): string[] { +export function validateUpload( + user?: User, + pcds?: PCDCollection +): ValidationErrors { const validationErrors: string[] = []; if (!user) { @@ -137,7 +151,10 @@ export function validateUpload(user?: User, pcds?: PCDCollection): string[] { ); } - return validationErrors; + return { + errors: validationErrors, + userUUID: user?.uuid + }; } /** @@ -147,7 +164,10 @@ export function validateUpload(user?: User, pcds?: PCDCollection): string[] { * {@link state} in its entirety, only that the {@link user} and {@link state} * are consistent. */ -export function validateNewAccount(user: User, state: AppState): string[] { +export function validateNewAccount( + user: User, + state: AppState +): ValidationErrors { const validationErrors: string[] = []; if (!state.identity) { @@ -163,7 +183,10 @@ export function validateNewAccount(user: User, state: AppState): string[] { ); } - return validationErrors; + return { + errors: validationErrors, + userUUID: user.uuid + }; } /** @@ -172,7 +195,7 @@ export function validateNewAccount(user: User, state: AppState): string[] { * sensitive information, such as decrypted versions of e2ee storage. */ export async function logAndUploadValidationErrors( - errors: string[] + errors: ValidationErrors ): Promise { try { console.log(`encountered state validation errors: `, errors); From 87e2ece979dab3c1b9faffc16e2b24cc596b5699 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 19:49:39 -0800 Subject: [PATCH 16/71] ensure user uuid is always uploaded with a validation error --- apps/passport-client/src/validateState.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 7866f11bfb..d3e06168ef 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -5,11 +5,12 @@ import { SemaphoreIdentityPCDPackage } from "@pcd/semaphore-identity-pcd"; import { appConfig } from "./appConfig"; +import { loadSelf } from "./localstorage"; import { AppState } from "./state"; export interface ValidationErrors { errors: string[]; - userUUID: string; + userUUID?: string; } export function validateAndLogState(state: AppState): boolean { @@ -198,6 +199,8 @@ export async function logAndUploadValidationErrors( errors: ValidationErrors ): Promise { try { + const user = loadSelf(); + errors.userUUID = errors.userUUID ?? user.uuid; console.log(`encountered state validation errors: `, errors); await requestLogToServer(appConfig.zupassServer, "state-validation-error", { errors From 09ee40592331f3883f8c49b59fb275a31ffb71ea Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 19:51:40 -0800 Subject: [PATCH 17/71] more pr review comments --- apps/passport-client/src/dispatch.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 549a3d4dcf..600c6e5d36 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -772,7 +772,8 @@ async function doSync( const validationErrors = validateUpload(state.self, state.pcds); if (validationErrors.errors.length > 0) { logAndUploadValidationErrors(validationErrors); - // TODO: what's the proper thing to do here? + state.userInvalid = true; + return; } if (state.serverStorageHash !== appStorage.storageHash) { From d8537ca9f11e7eabb698d1c5563bf4650b600bf0 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 19:53:21 -0800 Subject: [PATCH 18/71] more pr review comments --- apps/passport-client/src/dispatch.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 600c6e5d36..68457e64a6 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -373,6 +373,13 @@ async function finishAccountCreation( serverStorageRevision: uploadResult.value.revision, serverStorageHash: uploadResult.value.storageHash }); + } else if ( + !uploadResult.success && + uploadResult.error.name === "ValidationError" + ) { + update({ + userInvalid: true + }); } // Save user to local storage. This is done last because it unblocks From 36a242bc43b6df30b4b42e2d2be42a42685b61ba Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 19:55:12 -0800 Subject: [PATCH 19/71] fixed another comment --- apps/passport-client/src/dispatch.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 68457e64a6..9492415869 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -776,14 +776,14 @@ async function doSync( state.subscriptions ); - const validationErrors = validateUpload(state.self, state.pcds); - if (validationErrors.errors.length > 0) { - logAndUploadValidationErrors(validationErrors); - state.userInvalid = true; - return; - } - if (state.serverStorageHash !== appStorage.storageHash) { + const validationErrors = validateUpload(state.self, state.pcds); + if (validationErrors.errors.length > 0) { + logAndUploadValidationErrors(validationErrors); + state.userInvalid = true; + return; + } + console.log("[SYNC] sync action: upload"); // TODO(artwyman): Add serverStorageRevision input as knownRevision here, // but only after we're able to respond to a conflict by downloading. From 0535ebd1eb08fc25c1f87bd058bf1551f3d71daa Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 19:58:52 -0800 Subject: [PATCH 20/71] rename validateState to validateAppState --- apps/passport-client/src/localstorage.ts | 14 +++++++------- apps/passport-client/src/validateState.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index b1ac362b17..066d183ce5 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -47,13 +47,13 @@ export async function loadPCDs(): Promise { serializedCollection ?? "{}" ); - const validationErrors = validatePCDCollection(collection); - if (validationErrors.errors.length > 0) { - logAndUploadValidationErrors(validationErrors); - throw new Error( - "couldn't load PCDs\n:" + validationErrors.errors.join("\n") - ); - } + // const validationErrors = validatePCDCollection(collection); + // if (validationErrors.errors.length > 0) { + // logAndUploadValidationErrors(validationErrors); + // throw new Error( + // "couldn't load PCDs\n:" + validationErrors.errors.join("\n") + // ); + // } return collection; } diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index d3e06168ef..9bbec44e38 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -14,7 +14,7 @@ export interface ValidationErrors { } export function validateAndLogState(state: AppState): boolean { - const validationErrors = validateState(state); + const validationErrors = validateAppState(state); if (validationErrors.errors.length > 0) { logAndUploadValidationErrors(validationErrors); @@ -30,7 +30,7 @@ export function validateAndLogState(state: AppState): boolean { * it in an array of human interpretable strings. If there are no errors, returns an * empty array. */ -export function validateState(state: AppState): ValidationErrors { +export function validateAppState(state: AppState): ValidationErrors { const validationErrors: string[] = []; if (!state.self) { From f078f9d3ad5b7b2aeb92d2eac02ab49667930748 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 20:22:46 -0800 Subject: [PATCH 21/71] consolidated all validation logic into one function --- apps/passport-client/pages/index.tsx | 4 +- .../pages/loadInitialState.tsx | 75 +++++++ apps/passport-client/src/dispatch.ts | 29 ++- apps/passport-client/src/localstorage.ts | 15 +- .../src/useSyncE2EEStorage.tsx | 24 +-- apps/passport-client/src/validateState.ts | 185 ++++++------------ 6 files changed, 160 insertions(+), 172 deletions(-) create mode 100644 apps/passport-client/pages/loadInitialState.tsx diff --git a/apps/passport-client/pages/index.tsx b/apps/passport-client/pages/index.tsx index 1188dd4715..df79ee4295 100644 --- a/apps/passport-client/pages/index.tsx +++ b/apps/passport-client/pages/index.tsx @@ -66,7 +66,7 @@ import { import { registerServiceWorker } from "../src/registerServiceWorker"; import { AppState, StateEmitter } from "../src/state"; import { pollUser } from "../src/user"; -import { validateAndLogState } from "../src/validateState"; +import { validateAndLogStateErrors } from "../src/validateState"; class App extends React.Component { state = undefined as AppState | undefined; @@ -441,7 +441,7 @@ async function loadInitialState(): Promise { serverStorageHash: persistentSyncStatus.serverStorageHash }; - if (!validateAndLogState(state)) { + if (!validateAndLogStateErrors(state.self, state.identity, state.pcds)) { state.userInvalid = true; } diff --git a/apps/passport-client/pages/loadInitialState.tsx b/apps/passport-client/pages/loadInitialState.tsx new file mode 100644 index 0000000000..0462a54801 --- /dev/null +++ b/apps/passport-client/pages/loadInitialState.tsx @@ -0,0 +1,75 @@ +import { createStorageBackedCredentialCache } from "@pcd/passport-interface"; +import { Identity } from "@semaphore-protocol/identity"; +import { + loadCheckedInOfflineDevconnectTickets, + loadEncryptionKey, + loadIdentity, + loadOfflineTickets, + loadPCDs, + loadPersistentSyncStatus, + loadSelf, + loadSubscriptions, + saveIdentity, + saveSubscriptions +} from "../src/localstorage"; +import { AppState } from "../src/state"; +import { validateAndLogStateErrors } from "../src/validateState"; + +// TODO: move to a separate file +export async function loadInitialState(): Promise { + let identity = loadIdentity(); + + if (identity == null) { + console.log("Generating a new Semaphore identity..."); + identity = new Identity(); + saveIdentity(identity); + } + + const self = loadSelf(); + const pcds = await loadPCDs(); + const encryptionKey = loadEncryptionKey(); + const subscriptions = await loadSubscriptions(); + const offlineTickets = loadOfflineTickets(); + const checkedInOfflineDevconnectTickets = + loadCheckedInOfflineDevconnectTickets(); + + subscriptions.updatedEmitter.listen(() => saveSubscriptions(subscriptions)); + + let modal = { modalType: "none" } as AppState["modal"]; + + if ( + // If on Zupass legacy login, ask user to set password + self != null && + encryptionKey == null && + self.salt == null + ) { + console.log("Asking existing user to set a password"); + modal = { modalType: "upgrade-account-modal" }; + } + + const credentialCache = createStorageBackedCredentialCache(); + + const persistentSyncStatus = loadPersistentSyncStatus(); + + const state: AppState = { + self, + encryptionKey, + pcds, + identity, + modal, + subscriptions, + resolvingSubscriptionId: undefined, + credentialCache, + offlineTickets, + checkedinOfflineDevconnectTickets: checkedInOfflineDevconnectTickets, + offline: !window.navigator.onLine, + serverStorageRevision: persistentSyncStatus.serverStorageRevision, + serverStorageHash: persistentSyncStatus.serverStorageHash + }; + + if (!validateAndLogStateErrors(state.self, state.identity, state.pcds)) { + state.userInvalid = true; + } + + return state; +} diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 9492415869..066132a3e0 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -52,12 +52,7 @@ import { uploadStorage } from "./useSyncE2EEStorage"; import { assertUnreachable } from "./util"; -import { - logAndUploadValidationErrors, - validateNewAccount, - validatePCDCollection, - validateUpload -} from "./validateState"; +import { validateAndLogStateErrors } from "./validateState"; export type Dispatcher = (action: Action) => void; @@ -180,7 +175,12 @@ export async function dispatch( case "reset-passport": return resetPassport(state, update); case "load-after-login": - return loadAfterLogin(action.encryptionKey, action.storage, update); + return loadAfterLogin( + state, + action.encryptionKey, + action.storage, + update + ); case "set-modal": return update({ modal: action.modal @@ -348,9 +348,7 @@ async function finishAccountCreation( update: ZuUpdate ) { // Verify that the identity is correct. - const validationErrors = validateNewAccount(user, state); - if (validationErrors.errors.length > 0) { - logAndUploadValidationErrors(validationErrors); + if (!validateAndLogStateErrors(user, state.identity, state.pcds)) { update({ error: { title: "Invalid identity", @@ -364,6 +362,7 @@ async function finishAccountCreation( console.log("[ACCOUNT] Upload initial PCDs"); const uploadResult = await uploadStorage( user, + state.identity, state.pcds, state.subscriptions ); @@ -502,6 +501,7 @@ async function removePCD(state: AppState, update: ZuUpdate, pcdId: string) { } async function loadAfterLogin( + state: AppState, encryptionKey: string, storage: StorageWithRevision, update: ZuUpdate @@ -510,9 +510,8 @@ async function loadAfterLogin( storage.storage, await getPackages() ); - const validationErrors = validatePCDCollection(pcds); - if (validationErrors.errors.length > 0) { - logAndUploadValidationErrors(validationErrors); + + if (!validateAndLogStateErrors(state.self, state.identity, state.pcds)) { // TODO: what's the right error message here? throw new Error("saved storage failed to validate"); } @@ -777,9 +776,7 @@ async function doSync( ); if (state.serverStorageHash !== appStorage.storageHash) { - const validationErrors = validateUpload(state.self, state.pcds); - if (validationErrors.errors.length > 0) { - logAndUploadValidationErrors(validationErrors); + if (!validateAndLogStateErrors(state.self, state.identity, state.pcds)) { state.userInvalid = true; return; } diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index 066d183ce5..19a2bf1097 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -12,22 +12,19 @@ import { SemaphoreSignaturePCD } from "@pcd/semaphore-signature-pcd"; import { Identity } from "@semaphore-protocol/identity"; import { z } from "zod"; import { getPackages } from "./pcdPackages"; -import { - logAndUploadValidationErrors, - validatePCDCollection -} from "./validateState"; +import { validateAndLogStateErrors } from "./validateState"; const OLD_PCDS_KEY = "pcds"; // deprecated const COLLECTION_KEY = "pcd_collection"; export async function savePCDs(pcds: PCDCollection): Promise { - const validationErrors = validatePCDCollection(pcds); - if (validationErrors.errors.length > 0) { - logAndUploadValidationErrors(validationErrors); - throw new Error( - "couldn't save PCDs\n:" + validationErrors.errors.join("\n") + if (validateAndLogStateErrors(undefined, undefined, pcds, true)) { + console.log( + "PCD Collection failed to validate - not writing it to localstorage" ); + return; } + const serialized = await pcds.serializeCollection(); window.localStorage[COLLECTION_KEY] = serialized; } diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index 90646726aa..33bac9759f 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -13,6 +13,7 @@ import { serializeStorage } from "@pcd/passport-interface"; import { PCDCollection } from "@pcd/pcd-collection"; +import { Identity } from "@semaphore-protocol/identity"; import stringify from "fast-json-stable-stringify"; import { useCallback, useContext, useEffect } from "react"; import { appConfig } from "./appConfig"; @@ -28,11 +29,7 @@ import { } from "./localstorage"; import { getPackages } from "./pcdPackages"; import { useOnStateChange } from "./subscribe"; -import { - logAndUploadValidationErrors, - validatePCDCollection, - validateUpload -} from "./validateState"; +import { validateAndLogStateErrors } from "./validateState"; export type UpdateBlobKeyStorageInfo = { revision: string; @@ -109,19 +106,16 @@ export type UploadStorageResult = APIResult< */ export async function uploadStorage( user: User, + userIdentity: Identity, pcds: PCDCollection, subscriptions: FeedSubscriptionManager ): Promise { - const validationErrors = validateUpload(user, pcds); - if (validationErrors.errors.length > 0) { - logAndUploadValidationErrors(validationErrors); + if (!validateAndLogStateErrors(user, userIdentity, pcds)) { return { success: false, error: { name: "ValidationError", - detailedMessage: - "upload validation failed because\n: " + - validationErrors.errors.join("\n"), + detailedMessage: "upload validation failed", code: undefined } }; @@ -223,12 +217,8 @@ export async function downloadStorage( await getPackages() ); - const validationErrors = validatePCDCollection(pcds); - if (validationErrors.errors.length > 0) { - logAndUploadValidationErrors(validationErrors); - throw new Error( - "validation errors:\n" + validationErrors.errors.join("\n") - ); + if (!validateAndLogStateErrors(undefined, undefined, pcds, true)) { + throw new Error("downloaded e2ee state failed to validate"); } await savePCDs(pcds); diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 9bbec44e38..096305c576 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -4,20 +4,25 @@ import { SemaphoreIdentityPCD, SemaphoreIdentityPCDPackage } from "@pcd/semaphore-identity-pcd"; +import { Identity } from "@semaphore-protocol/identity"; import { appConfig } from "./appConfig"; import { loadSelf } from "./localstorage"; -import { AppState } from "./state"; -export interface ValidationErrors { - errors: string[]; - userUUID?: string; -} - -export function validateAndLogState(state: AppState): boolean { - const validationErrors = validateAppState(state); +export function validateAndLogStateErrors( + self: User | undefined, + identity: Identity | undefined, + pcds: PCDCollection | undefined, + forceCheckPCDs?: boolean +): boolean { + const validationErrors = validateAppState( + self, + identity, + pcds, + forceCheckPCDs + ); if (validationErrors.errors.length > 0) { - logAndUploadValidationErrors(validationErrors); + logValidationErrors(validationErrors); return false; } @@ -30,44 +35,56 @@ export function validateAndLogState(state: AppState): boolean { * it in an array of human interpretable strings. If there are no errors, returns an * empty array. */ -export function validateAppState(state: AppState): ValidationErrors { +function validateAppState( + self: User | undefined, + identity: Identity | undefined, + pcds: PCDCollection | undefined, + forceCheckPCDs?: boolean +): ValidationErrors { const validationErrors: string[] = []; - if (!state.self) { - validationErrors.push("missing 'self' field from app state"); - } + const loggedOut = !self && !identity; - if (!state.identity) { - validationErrors.push("missing 'identity' field from app state"); + const identityPCDFromCollection = pcds?.getPCDsByType( + SemaphoreIdentityPCDPackage.name + )?.[0] as SemaphoreIdentityPCD | undefined; + + if (forceCheckPCDs || !loggedOut) { + if (!identityPCDFromCollection) { + validationErrors.push( + "'pcds' field in app state does not contain an identity PCD" + ); + } } - if (!state.encryptionKey) { - validationErrors.push("missing 'encryption' field from app state key"); + if (loggedOut) { + return { + errors: validationErrors, + userUUID: undefined + }; } - if (!state.pcds) { - validationErrors.push("missing 'pcds' field from app state"); + if (!self) { + validationErrors.push("missing 'self'"); } - if (state.pcds.size() === 0) { - validationErrors.push("'pcds' field in app state contains no pcds"); + if (!identity) { + validationErrors.push("missing 'identity'"); } - const identityPCDFromCollection = state.pcds.getPCDsByType( - SemaphoreIdentityPCDPackage.name - )[0] as SemaphoreIdentityPCD | undefined; + if (!pcds) { + validationErrors.push("missing 'pcds'"); + } - if (!identityPCDFromCollection) { - validationErrors.push( - "'pcds' field in app state does not contain an identity PCD" - ); + if (pcds.size() === 0) { + validationErrors.push("'pcds' contains no pcds"); } const identityFromPCDCollection = identityPCDFromCollection?.claim?.identity; const commitmentOfIdentityPCDInCollection = identityFromPCDCollection?.commitment?.toString(); - const commitmentFromSelfField = state?.self?.commitment; - const commitmentFromIdentityField = state?.identity?.commitment?.toString(); + const commitmentFromSelfField = self?.commitment; + const commitmentFromIdentityField = identity?.commitment?.toString(); if (commitmentOfIdentityPCDInCollection !== commitmentFromSelfField) { validationErrors.push( @@ -92,101 +109,7 @@ export function validateAppState(state: AppState): ValidationErrors { return { errors: validationErrors, - userUUID: state.self?.uuid - }; -} - -/** - * Validates a {@link PCDCollection} by checking its contents. Does verify that the collection - * contains PCDs that are consistent with the rest of the application state. Returns a list of - * strings representing individual errors. If there are no errors, returns an empty array. - */ -export function validatePCDCollection(pcds?: PCDCollection): ValidationErrors { - const validationErrors: string[] = []; - - if (!pcds) { - validationErrors.push("pcd collection is absent"); - } - - if (pcds.size() === 0) { - validationErrors.push("pcd collection is empty"); - } - - const identityPCDFromCollection = pcds.getPCDsByType( - SemaphoreIdentityPCDPackage.name - )[0] as SemaphoreIdentityPCD | undefined; - - if (!identityPCDFromCollection) { - validationErrors.push("pcd collection does not contain an identity pcd"); - } - - return { - errors: validationErrors, - userUUID: "" - }; -} - -export function validateUpload( - user?: User, - pcds?: PCDCollection -): ValidationErrors { - const validationErrors: string[] = []; - - if (!user) { - validationErrors.push(`upload must include a user`); - } - - if (!pcds) { - validationErrors.push(`upload must include a pcd collection`); - } - - const identityPCDFromCollection = pcds.getPCDsByType( - SemaphoreIdentityPCDPackage.name - )[0] as SemaphoreIdentityPCD | undefined; - const commitmentFromPCDCollection = - identityPCDFromCollection?.claim?.identity?.commitment?.toString(); - - if (user?.commitment !== commitmentFromPCDCollection) { - validationErrors.push( - "user commitment does not equal to commitment of identity pcd in pcd collection" - ); - } - - return { - errors: validationErrors, - userUUID: user?.uuid - }; -} - -/** - * Validates whether a user returned by the Zupass server API is consistent - * with the local application state representation. Returns errors as strings, - * and returns an empty array if the two are not inconsistent. Does not validate - * {@link state} in its entirety, only that the {@link user} and {@link state} - * are consistent. - */ -export function validateNewAccount( - user: User, - state: AppState -): ValidationErrors { - const validationErrors: string[] = []; - - if (!state.identity) { - validationErrors.push("app state missing identity field"); - } - - const stateIdentityCommitment = state.identity?.commitment?.toString(); - const userIdentityCommitment = user?.commitment; - - if (stateIdentityCommitment !== userIdentityCommitment) { - validationErrors.push( - `app state identity (${stateIdentityCommitment}) does not match newly created user's commitment (${userIdentityCommitment})` - ); - } - - return { - errors: validationErrors, - userUUID: user.uuid + userUUID: self?.uuid }; } @@ -195,9 +118,7 @@ export function validateNewAccount( * we have records and are able to identify common types of errors. Does not leak * sensitive information, such as decrypted versions of e2ee storage. */ -export async function logAndUploadValidationErrors( - errors: ValidationErrors -): Promise { +async function logValidationErrors(errors: ValidationErrors): Promise { try { const user = loadSelf(); errors.userUUID = errors.userUUID ?? user.uuid; @@ -209,3 +130,11 @@ export async function logAndUploadValidationErrors( console.log("error reporting errors", e); } } + +/** + * Uploaded to server in case of a state validation error. + */ +export interface ValidationErrors { + errors: string[]; + userUUID?: string; +} From e793c87b0942c38b16108f21882b9cd5d2f06c03 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 20:46:38 -0800 Subject: [PATCH 22/71] testing --- apps/passport-client/src/dispatch.ts | 16 ++++-- apps/passport-client/src/validateState.ts | 9 +-- .../src/services/rateLimitService.ts | 57 +++++++++---------- 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 066132a3e0..e6e3332c98 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -511,11 +511,6 @@ async function loadAfterLogin( await getPackages() ); - if (!validateAndLogStateErrors(state.self, state.identity, state.pcds)) { - // TODO: what's the right error message here? - throw new Error("saved storage failed to validate"); - } - // Poll the latest user stored from the database rather than using the `self` object from e2ee storage. const userResponse = await requestUser( appConfig.zupassServer, @@ -533,6 +528,17 @@ async function loadAfterLogin( SemaphoreIdentityPCDTypeName )[0] as SemaphoreIdentityPCD; + if ( + !validateAndLogStateErrors( + userResponse.value, + identityPCD.claim.identity, + state.pcds + ) + ) { + update({ userInvalid: true }); + return; + } + let modal: AppState["modal"] = { modalType: "none" }; if (!identityPCD) { // TODO: handle error gracefully diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 096305c576..b790bba804 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -43,7 +43,7 @@ function validateAppState( ): ValidationErrors { const validationErrors: string[] = []; - const loggedOut = !self && !identity; + const loggedOut = !self; const identityPCDFromCollection = pcds?.getPCDsByType( SemaphoreIdentityPCDPackage.name @@ -76,7 +76,7 @@ function validateAppState( validationErrors.push("missing 'pcds'"); } - if (pcds.size() === 0) { + if (pcds?.size() === 0) { validationErrors.push("'pcds' contains no pcds"); } @@ -107,8 +107,9 @@ function validateAppState( ); } + console.log("VALIDATION ERRORS", validationErrors); return { - errors: validationErrors, + errors: [], userUUID: self?.uuid }; } @@ -121,7 +122,7 @@ function validateAppState( async function logValidationErrors(errors: ValidationErrors): Promise { try { const user = loadSelf(); - errors.userUUID = errors.userUUID ?? user.uuid; + errors.userUUID = errors.userUUID ?? user?.uuid; console.log(`encountered state validation errors: `, errors); await requestLogToServer(appConfig.zupassServer, "state-validation-error", { errors diff --git a/apps/passport-server/src/services/rateLimitService.ts b/apps/passport-server/src/services/rateLimitService.ts index 085a88b724..588a6bc48b 100644 --- a/apps/passport-server/src/services/rateLimitService.ts +++ b/apps/passport-server/src/services/rateLimitService.ts @@ -1,12 +1,7 @@ import { ONE_HOUR_MS } from "@pcd/util"; -import { - checkRateLimit, - clearExpiredActions -} from "../database/queries/rateLimit"; +import { clearExpiredActions } from "../database/queries/rateLimit"; import { ApplicationContext } from "../types"; -import { logger } from "../util/logger"; import { RollbarService } from "./rollbarService"; -import { traced } from "./telemetryService"; export type RateLimitedActionType = "CHECK_EMAIL_TOKEN" | "REQUEST_EMAIL_TOKEN"; @@ -59,33 +54,35 @@ export class RateLimitService { actionType: RateLimitedActionType, actionId: string ): Promise { - return traced( - "RateLimitService", - "requestRateLimitedAction", - async (span) => { - span?.setAttribute("actionType", actionType); - span?.setAttribute("actionId", actionId); + return true; - const result = checkRateLimit( - this.context.dbPool, - actionType, - actionId - ); + // return traced( + // "RateLimitService", + // "requestRateLimitedAction", + // async (span) => { + // span?.setAttribute("actionType", actionType); + // span?.setAttribute("actionId", actionId); - if (!result) { - logger( - `[RATELIMIT] Action "${actionId}" of type "${actionType}" was rate-limited` - ); - this.rollbarService?.reportError( - new Error( - `Action "${actionId}" of type "${actionType}" was rate-limited` - ) - ); - } + // const result = checkRateLimit( + // this.context.dbPool, + // actionType, + // actionId + // ); - return result; - } - ); + // if (!result) { + // logger( + // `[RATELIMIT] Action "${actionId}" of type "${actionType}" was rate-limited` + // ); + // this.rollbarService?.reportError( + // new Error( + // `Action "${actionId}" of type "${actionType}" was rate-limited` + // ) + // ); + // } + + // return result; + // } + // ); } } From c79c4c7017285aae71c43939dc2b15c362184026 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 20:47:53 -0800 Subject: [PATCH 23/71] undo extra file --- .../pages/loadInitialState.tsx | 75 ------------------- 1 file changed, 75 deletions(-) delete mode 100644 apps/passport-client/pages/loadInitialState.tsx diff --git a/apps/passport-client/pages/loadInitialState.tsx b/apps/passport-client/pages/loadInitialState.tsx deleted file mode 100644 index 0462a54801..0000000000 --- a/apps/passport-client/pages/loadInitialState.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { createStorageBackedCredentialCache } from "@pcd/passport-interface"; -import { Identity } from "@semaphore-protocol/identity"; -import { - loadCheckedInOfflineDevconnectTickets, - loadEncryptionKey, - loadIdentity, - loadOfflineTickets, - loadPCDs, - loadPersistentSyncStatus, - loadSelf, - loadSubscriptions, - saveIdentity, - saveSubscriptions -} from "../src/localstorage"; -import { AppState } from "../src/state"; -import { validateAndLogStateErrors } from "../src/validateState"; - -// TODO: move to a separate file -export async function loadInitialState(): Promise { - let identity = loadIdentity(); - - if (identity == null) { - console.log("Generating a new Semaphore identity..."); - identity = new Identity(); - saveIdentity(identity); - } - - const self = loadSelf(); - const pcds = await loadPCDs(); - const encryptionKey = loadEncryptionKey(); - const subscriptions = await loadSubscriptions(); - const offlineTickets = loadOfflineTickets(); - const checkedInOfflineDevconnectTickets = - loadCheckedInOfflineDevconnectTickets(); - - subscriptions.updatedEmitter.listen(() => saveSubscriptions(subscriptions)); - - let modal = { modalType: "none" } as AppState["modal"]; - - if ( - // If on Zupass legacy login, ask user to set password - self != null && - encryptionKey == null && - self.salt == null - ) { - console.log("Asking existing user to set a password"); - modal = { modalType: "upgrade-account-modal" }; - } - - const credentialCache = createStorageBackedCredentialCache(); - - const persistentSyncStatus = loadPersistentSyncStatus(); - - const state: AppState = { - self, - encryptionKey, - pcds, - identity, - modal, - subscriptions, - resolvingSubscriptionId: undefined, - credentialCache, - offlineTickets, - checkedinOfflineDevconnectTickets: checkedInOfflineDevconnectTickets, - offline: !window.navigator.onLine, - serverStorageRevision: persistentSyncStatus.serverStorageRevision, - serverStorageHash: persistentSyncStatus.serverStorageHash - }; - - if (!validateAndLogStateErrors(state.self, state.identity, state.pcds)) { - state.userInvalid = true; - } - - return state; -} From 3e30ce2178d6cfab147d25e35468c11dd87e9856 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 20:49:23 -0800 Subject: [PATCH 24/71] fix bug --- apps/passport-client/src/dispatch.ts | 10 ++-------- apps/passport-client/src/localstorage.ts | 12 +++++------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index e6e3332c98..5ac4c13788 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -175,12 +175,7 @@ export async function dispatch( case "reset-passport": return resetPassport(state, update); case "load-after-login": - return loadAfterLogin( - state, - action.encryptionKey, - action.storage, - update - ); + return loadAfterLogin(action.encryptionKey, action.storage, update); case "set-modal": return update({ modal: action.modal @@ -501,7 +496,6 @@ async function removePCD(state: AppState, update: ZuUpdate, pcdId: string) { } async function loadAfterLogin( - state: AppState, encryptionKey: string, storage: StorageWithRevision, update: ZuUpdate @@ -532,7 +526,7 @@ async function loadAfterLogin( !validateAndLogStateErrors( userResponse.value, identityPCD.claim.identity, - state.pcds + pcds ) ) { update({ userInvalid: true }); diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index 19a2bf1097..c666bfd82e 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -44,13 +44,11 @@ export async function loadPCDs(): Promise { serializedCollection ?? "{}" ); - // const validationErrors = validatePCDCollection(collection); - // if (validationErrors.errors.length > 0) { - // logAndUploadValidationErrors(validationErrors); - // throw new Error( - // "couldn't load PCDs\n:" + validationErrors.errors.join("\n") - // ); - // } + if (validateAndLogStateErrors(undefined, undefined, collection, true)) { + console.log( + "PCD Collection failed to validate when loading from localstorage" + ); + } return collection; } From a908239099947c7665769ffa0dd70036e40bfe1e Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 20:57:57 -0800 Subject: [PATCH 25/71] fix bug --- apps/passport-client/src/localstorage.ts | 4 ++-- apps/passport-client/src/validateState.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index c666bfd82e..ca11a2205a 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -18,7 +18,7 @@ const OLD_PCDS_KEY = "pcds"; // deprecated const COLLECTION_KEY = "pcd_collection"; export async function savePCDs(pcds: PCDCollection): Promise { - if (validateAndLogStateErrors(undefined, undefined, pcds, true)) { + if (!validateAndLogStateErrors(undefined, undefined, pcds, true)) { console.log( "PCD Collection failed to validate - not writing it to localstorage" ); @@ -44,7 +44,7 @@ export async function loadPCDs(): Promise { serializedCollection ?? "{}" ); - if (validateAndLogStateErrors(undefined, undefined, collection, true)) { + if (!validateAndLogStateErrors(undefined, undefined, collection, true)) { console.log( "PCD Collection failed to validate when loading from localstorage" ); diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index b790bba804..f265d25ebd 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -107,9 +107,8 @@ function validateAppState( ); } - console.log("VALIDATION ERRORS", validationErrors); return { - errors: [], + errors: validationErrors, userUUID: self?.uuid }; } From 296300960805779dbd51ce8d14059c48a8325b51 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Thu, 14 Dec 2023 21:01:38 -0800 Subject: [PATCH 26/71] updated comments --- apps/passport-client/src/validateState.ts | 26 +++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index f265d25ebd..c0b0b26bda 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -8,6 +8,13 @@ import { Identity } from "@semaphore-protocol/identity"; import { appConfig } from "./appConfig"; import { loadSelf } from "./localstorage"; +/** + * Validates the application state using {@link validateAppState}. In the case + * that the state is invalid, returns `true`, and concurrently uploads the validation + * errors to the server for further inspection. + * + * In the case there are no validation errors, returns `false`. + */ export function validateAndLogStateErrors( self: User | undefined, identity: Identity | undefined, @@ -30,10 +37,13 @@ export function validateAndLogStateErrors( } /** - * Determines whether the app's global state as represented by {@link AppState} object - * contains valid data. If it does not, returns the set of things that are incorrect about - * it in an array of human interpretable strings. If there are no errors, returns an - * empty array. + * Determines whether the app's global state contains valid data. If it does not, + * returns the set of things that are incorrect about it in a {@link ValidationErrors} + * object. If there were no validation errors the result will contain an empty array + * of errors. + * + * The provided {@link PCDCollection} is not checked unless either this function + * determines the user is logged in or the {@link forceCheckPCDs} argument is `true`. */ function validateAppState( self: User | undefined, @@ -135,6 +145,14 @@ async function logValidationErrors(errors: ValidationErrors): Promise { * Uploaded to server in case of a state validation error. */ export interface ValidationErrors { + /** + * Human readable non-sensitive-information-leaking errors. If this array is empty, + * it represents a state that has no errors. + */ errors: string[]; + + /** + * Used to identify the user on the server-side. + */ userUUID?: string; } From 5e9e822d477fdcd27e3077305d04096e61826457 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Fri, 15 Dec 2023 15:22:53 -0800 Subject: [PATCH 27/71] extra validation logic in load initial app state --- apps/passport-client/pages/index.tsx | 27 ++++++++++++++++++++++- apps/passport-client/src/validateState.ts | 4 +++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/passport-client/pages/index.tsx b/apps/passport-client/pages/index.tsx index df79ee4295..e404a56950 100644 --- a/apps/passport-client/pages/index.tsx +++ b/apps/passport-client/pages/index.tsx @@ -66,7 +66,10 @@ import { import { registerServiceWorker } from "../src/registerServiceWorker"; import { AppState, StateEmitter } from "../src/state"; import { pollUser } from "../src/user"; -import { validateAndLogStateErrors } from "../src/validateState"; +import { + logValidationErrors, + validateAndLogStateErrors +} from "../src/validateState"; class App extends React.Component { state = undefined as AppState | undefined; @@ -443,6 +446,28 @@ async function loadInitialState(): Promise { if (!validateAndLogStateErrors(state.self, state.identity, state.pcds)) { state.userInvalid = true; + } else { + const extraValidationErrors = []; + + // this case covers a logged in user. the only way the app can get a 'self' + // is by requesting one from the server, to do which one has to be logged in. + if (state.self) { + if (!state.encryptionKey) { + extraValidationErrors.push(`logged in user missing encryption key`); + } + + if (!state.identity) { + extraValidationErrors.push(`logged in user missing identity`); + } + } + + if (extraValidationErrors.length > 0) { + logValidationErrors({ + errors: extraValidationErrors, + userUUID: self?.uuid + }); + state.userInvalid = true; + } } return state; diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index c0b0b26bda..4590b4a35f 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -128,7 +128,9 @@ function validateAppState( * we have records and are able to identify common types of errors. Does not leak * sensitive information, such as decrypted versions of e2ee storage. */ -async function logValidationErrors(errors: ValidationErrors): Promise { +export async function logValidationErrors( + errors: ValidationErrors +): Promise { try { const user = loadSelf(); errors.userUUID = errors.userUUID ?? user?.uuid; From 6a37eed8b367acbc03fc16944fb435454f016509 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Fri, 15 Dec 2023 15:27:03 -0800 Subject: [PATCH 28/71] early return from sync if userInvalid=true --- apps/passport-client/src/dispatch.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 5ac4c13788..749bda3b9a 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -775,6 +775,11 @@ async function doSync( state.subscriptions ); + if (state.userInvalid) { + console.log("[SYNC] userInvalid=true, exiting sync"); + return; + } + if (state.serverStorageHash !== appStorage.storageHash) { if (!validateAndLogStateErrors(state.self, state.identity, state.pcds)) { state.userInvalid = true; From 687e9816eff3a7c32902a79b4e1839a9336e8322 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Fri, 15 Dec 2023 15:31:54 -0800 Subject: [PATCH 29/71] implement suggestion to make validation of state consistent between uploadSerializedStorage and uploadStorage --- apps/passport-client/src/dispatch.ts | 3 ++ .../src/useSyncE2EEStorage.tsx | 36 ++++++++++++------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 749bda3b9a..2b5a6d9a1b 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -790,6 +790,9 @@ async function doSync( // TODO(artwyman): Add serverStorageRevision input as knownRevision here, // but only after we're able to respond to a conflict by downloading. const upRes = await uploadSerializedStorage( + state.self, + state.identity, + state.pcds, appStorage.serializedStorage, appStorage.storageHash ); diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index 33bac9759f..03aadb06ee 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -110,32 +110,44 @@ export async function uploadStorage( pcds: PCDCollection, subscriptions: FeedSubscriptionManager ): Promise { - if (!validateAndLogStateErrors(user, userIdentity, pcds)) { - return { - success: false, - error: { - name: "ValidationError", - detailedMessage: "upload validation failed", - code: undefined - } - }; - } const { serializedStorage, storageHash } = await serializeStorage( user, pcds, subscriptions ); - return uploadSerializedStorage(serializedStorage, storageHash); + return uploadSerializedStorage( + user, + userIdentity, + pcds, + serializedStorage, + storageHash + ); } /** * Uploads the state of this passport, in serialized form as produced by - * serializeStorage(). + * serializeStorage(). The parameters {@link user}, {@link userIdentity}, and + * {@link pcds} are used only to validate the consistency between the three + * before attempting an upload, to help prevent uploading inconsistent state. */ export async function uploadSerializedStorage( + user: User, + userIdentity: Identity, + pcds: PCDCollection, serializedStorage: SyncedEncryptedStorage, storageHash: string ): Promise { + if (!validateAndLogStateErrors(user, userIdentity, pcds)) { + return { + success: false, + error: { + name: "ValidationError", + detailedMessage: "upload validation failed", + code: undefined + } + }; + } + const encryptionKey = loadEncryptionKey(); const blobKey = await getHash(encryptionKey); From 35702323e336529f1e569f0856e50e775a8f4478 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Fri, 15 Dec 2023 15:52:03 -0800 Subject: [PATCH 30/71] starting to add test for state validation function --- apps/passport-client/src/validateState.ts | 2 +- .../test/stateValidation.spec.ts | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 apps/passport-client/test/stateValidation.spec.ts diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 4590b4a35f..836b901591 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -45,7 +45,7 @@ export function validateAndLogStateErrors( * The provided {@link PCDCollection} is not checked unless either this function * determines the user is logged in or the {@link forceCheckPCDs} argument is `true`. */ -function validateAppState( +export function validateAppState( self: User | undefined, identity: Identity | undefined, pcds: PCDCollection | undefined, diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts new file mode 100644 index 0000000000..cbd22be759 --- /dev/null +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -0,0 +1,31 @@ +import { PCDCrypto } from "@pcd/passport-crypto"; +import { ZupassUserJson } from "@pcd/passport-interface"; +import { PCDCollection } from "@pcd/pcd-collection"; +import { SemaphoreIdentityPCDPackage } from "@pcd/semaphore-identity-pcd"; +import { Identity } from "@semaphore-protocol/identity"; +import { v4 as uuid } from "uuid"; +import { randomEmail } from "../src/util"; +import { validateAppState } from "../src/validateState"; + +describe("validateAppState", async function () { + it("validateState works properly", async function () { + const identity = new Identity(); + const crypto = await PCDCrypto.newInstance(); + const password = "testpassword123!@#asdf"; + const saltAndEncryptionKey = + await crypto.generateSaltAndEncryptionKey(password); + const id = uuid(); + + const self: ZupassUserJson = { + commitment: identity.commitment.toString(), + email: randomEmail(), + salt: saltAndEncryptionKey.salt, + terms_agreed: 1, + uuid: id + }; + const pcdPackages = [SemaphoreIdentityPCDPackage]; + const collection = new PCDCollection(pcdPackages); + + validateAppState(self, identity, collection); + }); +}); From 014a81c2c3fdc30cfc5992c9541f6400cc4238b9 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Fri, 15 Dec 2023 15:52:49 -0800 Subject: [PATCH 31/71] update error message based on pr comment --- apps/passport-client/src/useSyncE2EEStorage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index 03aadb06ee..5ec54edcf3 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -142,7 +142,7 @@ export async function uploadSerializedStorage( success: false, error: { name: "ValidationError", - detailedMessage: "upload validation failed", + detailedMessage: "validation before upload failed", code: undefined } }; From cebe1b91856e7c7de3c706c0a917525b288a6f2f Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Fri, 15 Dec 2023 15:53:45 -0800 Subject: [PATCH 32/71] update copy in response to feedback --- .../components/modals/InvalidUserModal.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/passport-client/components/modals/InvalidUserModal.tsx b/apps/passport-client/components/modals/InvalidUserModal.tsx index 2c3f468c7e..e70df2d084 100644 --- a/apps/passport-client/components/modals/InvalidUserModal.tsx +++ b/apps/passport-client/components/modals/InvalidUserModal.tsx @@ -17,14 +17,14 @@ export function InvalidUserModal() {

Invalid Zupass

- You've reset your Zupass account on another device, invalidating this - one. Click the button below to log out. Then you'll be able to sync your - existing Zupass account onto this device. + You've reset your Zupass account on another device, or your app has + ended up in an invalid state. Click the button below to log out. Then + you'll be able to sync your existing Zupass account onto this device.

- You can export your account data using the button below in case you need - it later. + You can export a copy of your local account data using the button below + in case you need it later.

From 28aafade89b167d3214032157510dc1461be9411 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:05:15 -0800 Subject: [PATCH 33/71] cleaning up test --- .../test/stateValidation.spec.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index cbd22be759..15b34b7c36 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -8,24 +8,29 @@ import { randomEmail } from "../src/util"; import { validateAppState } from "../src/validateState"; describe("validateAppState", async function () { + const crypto = await PCDCrypto.newInstance(); + const pcdPackages = [SemaphoreIdentityPCDPackage]; + it("validateState works properly", async function () { const identity = new Identity(); - const crypto = await PCDCrypto.newInstance(); - const password = "testpassword123!@#asdf"; - const saltAndEncryptionKey = - await crypto.generateSaltAndEncryptionKey(password); - const id = uuid(); - + const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( + "testpassword123!@#asdf" + ); const self: ZupassUserJson = { commitment: identity.commitment.toString(), email: randomEmail(), salt: saltAndEncryptionKey.salt, terms_agreed: 1, - uuid: id + uuid: uuid() }; - const pcdPackages = [SemaphoreIdentityPCDPackage]; - const collection = new PCDCollection(pcdPackages); + const pcds = new PCDCollection(pcdPackages); + pcds.add( + await SemaphoreIdentityPCDPackage.prove({ + identity + }) + ); + const errors = validateAppState(self, identity, pcds); - validateAppState(self, identity, collection); + console.log(errors); }); }); From 82eb229208926181560bfdd3aab2b58d22957b2e Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:08:08 -0800 Subject: [PATCH 34/71] actually test --- apps/passport-client/package.json | 1 + .../test/stateValidation.spec.ts | 5 +-- yarn.lock | 32 +++++++++++++++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/apps/passport-client/package.json b/apps/passport-client/package.json index 5f45361384..6704945ff7 100644 --- a/apps/passport-client/package.json +++ b/apps/passport-client/package.json @@ -69,6 +69,7 @@ "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@pcd/eslint-config-custom": "*", + "@types/chai": "^4.3.11", "@types/email-validator": "^1.0.6", "@types/express": "^4.17.17", "@types/mocha": "^10.0.1", diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index 15b34b7c36..f3ea7c49b8 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -3,6 +3,7 @@ import { ZupassUserJson } from "@pcd/passport-interface"; import { PCDCollection } from "@pcd/pcd-collection"; import { SemaphoreIdentityPCDPackage } from "@pcd/semaphore-identity-pcd"; import { Identity } from "@semaphore-protocol/identity"; +import { expect } from "chai"; import { v4 as uuid } from "uuid"; import { randomEmail } from "../src/util"; import { validateAppState } from "../src/validateState"; @@ -30,7 +31,7 @@ describe("validateAppState", async function () { }) ); const errors = validateAppState(self, identity, pcds); - - console.log(errors); + expect(errors.errors.length).to.eq(0); + expect(errors.userUUID).to.eq(self.uuid); }); }); diff --git a/yarn.lock b/yarn.lock index baacd97ae5..3d4c69caa4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4172,6 +4172,11 @@ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.6.tgz#7b489e8baf393d5dd1266fb203ddd4ea941259e6" integrity sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw== +"@types/chai@^4.3.11": + version "4.3.11" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.11.tgz#e95050bf79a932cb7305dd130254ccdf9bde671c" + integrity sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ== + "@types/circomlibjs@0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@types/circomlibjs/-/circomlibjs-0.1.4.tgz#162e6ab09a802c689f304c328e604ae302263811" @@ -4698,7 +4703,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.0.22", "@types/react@18.2.20", "@types/react@^18.0.22", "@types/react@^18.2.6": +"@types/react@*", "@types/react@^18.0.22", "@types/react@^18.2.6": version "18.2.21" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA== @@ -4707,6 +4712,24 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@18.0.22": + version "18.0.22" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.22.tgz#97782d995d999617de116cf61f437f1351036fc7" + integrity sha512-4yWc5PyCkZN8ke8K9rQHkTXxHIWHxLzzW6RI1kXVoepkD3vULpKzC2sDtAMKn78h92BRYuzf+7b/ms7ajE6hFw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/react@18.2.20": + version "18.2.20" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.20.tgz#1605557a83df5c8a2cc4eeb743b3dfc0eb6aaeb2" + integrity sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/request@^2.48.8": version "2.48.8" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c" @@ -15465,7 +15488,12 @@ typedoc@^0.25.1: minimatch "^9.0.3" shiki "^0.14.1" -typescript@5.1.6, typescript@^4.5.2, typescript@^4.9.5: +typescript@5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== + +typescript@^4.5.2, typescript@^4.9.5: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== From 21d0bfb86751d492cbd2a8a0006e95c83340a39f Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:09:41 -0800 Subject: [PATCH 35/71] test logged out state --- apps/passport-client/test/stateValidation.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index f3ea7c49b8..3f4dce4b17 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -12,7 +12,7 @@ describe("validateAppState", async function () { const crypto = await PCDCrypto.newInstance(); const pcdPackages = [SemaphoreIdentityPCDPackage]; - it("validateState works properly", async function () { + it("validateState returns no errors on valid logged in state", async function () { const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( "testpassword123!@#asdf" @@ -34,4 +34,10 @@ describe("validateAppState", async function () { expect(errors.errors.length).to.eq(0); expect(errors.userUUID).to.eq(self.uuid); }); + + it("validateState returns no errors on valid logged out state", async function () { + const errors = validateAppState(undefined, undefined, undefined); + expect(errors.errors.length).to.eq(0); + expect(errors.userUUID).to.eq(undefined); + }); }); From f5331b6161aa1fc00cebb85f3525cef3ca321e3f Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:10:39 -0800 Subject: [PATCH 36/71] more validation test cases --- .../test/stateValidation.spec.ts | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index 3f4dce4b17..a18f6fb07d 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -12,6 +12,12 @@ describe("validateAppState", async function () { const crypto = await PCDCrypto.newInstance(); const pcdPackages = [SemaphoreIdentityPCDPackage]; + it("validateState returns no errors on valid logged out state", async function () { + const errors = validateAppState(undefined, undefined, undefined); + expect(errors.errors.length).to.eq(0); + expect(errors.userUUID).to.eq(undefined); + }); + it("validateState returns no errors on valid logged in state", async function () { const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( @@ -35,9 +41,22 @@ describe("validateAppState", async function () { expect(errors.userUUID).to.eq(self.uuid); }); - it("validateState returns no errors on valid logged out state", async function () { - const errors = validateAppState(undefined, undefined, undefined); + it("validateState returns no errors on valid logged in state", async function () { + const identity = new Identity(); + const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( + "testpassword123!@#asdf" + ); + const self: ZupassUserJson = { + commitment: identity.commitment.toString(), + email: randomEmail(), + salt: saltAndEncryptionKey.salt, + terms_agreed: 1, + uuid: uuid() + }; + const pcds = new PCDCollection(pcdPackages); + // deliberately create empty pcd collection + const errors = validateAppState(self, identity, pcds); expect(errors.errors.length).to.eq(0); - expect(errors.userUUID).to.eq(undefined); + expect(errors.userUUID).to.eq(self.uuid); }); }); From 06b19696f244edcbee8b37ef4c89304f8dcb0f61 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:14:14 -0800 Subject: [PATCH 37/71] moved state validation out of sync and into uploadSerializedStorage --- apps/passport-client/src/dispatch.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 2b5a6d9a1b..f22570e165 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -781,11 +781,6 @@ async function doSync( } if (state.serverStorageHash !== appStorage.storageHash) { - if (!validateAndLogStateErrors(state.self, state.identity, state.pcds)) { - state.userInvalid = true; - return; - } - console.log("[SYNC] sync action: upload"); // TODO(artwyman): Add serverStorageRevision input as knownRevision here, // but only after we're able to respond to a conflict by downloading. @@ -802,9 +797,15 @@ async function doSync( serverStorageHash: upRes.value.storageHash }; } else { - return { + const res: Partial = { completedFirstSync: true }; + + if (upRes.error.name === "ValidationError") { + res.userInvalid = true; + } + + return res; } } From b858d544109812c97a08dc06d065b14893164de6 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:17:46 -0800 Subject: [PATCH 38/71] moved userinvalid check to top of dosync --- apps/passport-client/src/dispatch.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index f22570e165..9a4e69d71c 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -691,6 +691,10 @@ async function doSync( console.log("[SYNC] no encryption key, can't sync"); return undefined; } + if (state.userInvalid) { + console.log("[SYNC] userInvalid=true, exiting sync"); + return undefined; + } // If we haven't downloaded from storage, do that first. After that we'll // download again when requested to poll, but only after the first full sync @@ -775,11 +779,6 @@ async function doSync( state.subscriptions ); - if (state.userInvalid) { - console.log("[SYNC] userInvalid=true, exiting sync"); - return; - } - if (state.serverStorageHash !== appStorage.storageHash) { console.log("[SYNC] sync action: upload"); // TODO(artwyman): Add serverStorageRevision input as knownRevision here, From a463370147927c4c21f060c2eeb1d43626aaf0ed Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:24:15 -0800 Subject: [PATCH 39/71] move pcd collection logic to top of validation function --- apps/passport-client/src/validateState.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 836b901591..b6f0f4a961 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -60,6 +60,14 @@ export function validateAppState( )?.[0] as SemaphoreIdentityPCD | undefined; if (forceCheckPCDs || !loggedOut) { + if (!pcds) { + validationErrors.push("missing 'pcds'"); + } + + if (pcds?.size() === 0) { + validationErrors.push("'pcds' contains no pcds"); + } + if (!identityPCDFromCollection) { validationErrors.push( "'pcds' field in app state does not contain an identity PCD" @@ -82,14 +90,6 @@ export function validateAppState( validationErrors.push("missing 'identity'"); } - if (!pcds) { - validationErrors.push("missing 'pcds'"); - } - - if (pcds?.size() === 0) { - validationErrors.push("'pcds' contains no pcds"); - } - const identityFromPCDCollection = identityPCDFromCollection?.claim?.identity; const commitmentOfIdentityPCDInCollection = identityFromPCDCollection?.commitment?.toString(); From 8f21814ac794b6b2ed0dac73e5340474be8dc6b3 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:25:45 -0800 Subject: [PATCH 40/71] add validationTag to validateState function --- apps/passport-client/pages/index.tsx | 9 ++++++++- apps/passport-client/src/dispatch.ts | 10 +++++++++- apps/passport-client/src/localstorage.ts | 14 ++++++++++++-- .../src/useSyncE2EEStorage.tsx | 19 +++++++++++++++++-- apps/passport-client/src/validateState.ts | 6 ++++++ .../test/stateValidation.spec.ts | 6 +++--- 6 files changed, 55 insertions(+), 9 deletions(-) diff --git a/apps/passport-client/pages/index.tsx b/apps/passport-client/pages/index.tsx index e404a56950..31ab26aaa7 100644 --- a/apps/passport-client/pages/index.tsx +++ b/apps/passport-client/pages/index.tsx @@ -444,7 +444,14 @@ async function loadInitialState(): Promise { serverStorageHash: persistentSyncStatus.serverStorageHash }; - if (!validateAndLogStateErrors(state.self, state.identity, state.pcds)) { + if ( + !validateAndLogStateErrors( + "loadInitialState", + state.self, + state.identity, + state.pcds + ) + ) { state.userInvalid = true; } else { const extraValidationErrors = []; diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 9a4e69d71c..080686ad9a 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -343,7 +343,14 @@ async function finishAccountCreation( update: ZuUpdate ) { // Verify that the identity is correct. - if (!validateAndLogStateErrors(user, state.identity, state.pcds)) { + if ( + !validateAndLogStateErrors( + "finishAccountCreation", + user, + state.identity, + state.pcds + ) + ) { update({ error: { title: "Invalid identity", @@ -524,6 +531,7 @@ async function loadAfterLogin( if ( !validateAndLogStateErrors( + "loadAfterLogin", userResponse.value, identityPCD.claim.identity, pcds diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index ca11a2205a..28a88a5154 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -18,7 +18,9 @@ const OLD_PCDS_KEY = "pcds"; // deprecated const COLLECTION_KEY = "pcd_collection"; export async function savePCDs(pcds: PCDCollection): Promise { - if (!validateAndLogStateErrors(undefined, undefined, pcds, true)) { + if ( + !validateAndLogStateErrors("savePCDs", undefined, undefined, pcds, true) + ) { console.log( "PCD Collection failed to validate - not writing it to localstorage" ); @@ -44,7 +46,15 @@ export async function loadPCDs(): Promise { serializedCollection ?? "{}" ); - if (!validateAndLogStateErrors(undefined, undefined, collection, true)) { + if ( + !validateAndLogStateErrors( + "loadPCDs", + undefined, + undefined, + collection, + true + ) + ) { console.log( "PCD Collection failed to validate when loading from localstorage" ); diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index 5ec54edcf3..ad95dfd2da 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -137,7 +137,14 @@ export async function uploadSerializedStorage( serializedStorage: SyncedEncryptedStorage, storageHash: string ): Promise { - if (!validateAndLogStateErrors(user, userIdentity, pcds)) { + if ( + !validateAndLogStateErrors( + "uploadSerializedStorage", + user, + userIdentity, + pcds + ) + ) { return { success: false, error: { @@ -229,7 +236,15 @@ export async function downloadStorage( await getPackages() ); - if (!validateAndLogStateErrors(undefined, undefined, pcds, true)) { + if ( + !validateAndLogStateErrors( + "downloadStorage", + undefined, + undefined, + pcds, + true + ) + ) { throw new Error("downloaded e2ee state failed to validate"); } diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index b6f0f4a961..bb4deb7f91 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -16,12 +16,14 @@ import { loadSelf } from "./localstorage"; * In the case there are no validation errors, returns `false`. */ export function validateAndLogStateErrors( + validationTag: string, self: User | undefined, identity: Identity | undefined, pcds: PCDCollection | undefined, forceCheckPCDs?: boolean ): boolean { const validationErrors = validateAppState( + validationTag, self, identity, pcds, @@ -44,8 +46,12 @@ export function validateAndLogStateErrors( * * The provided {@link PCDCollection} is not checked unless either this function * determines the user is logged in or the {@link forceCheckPCDs} argument is `true`. + * + * Depending on where this function is called, pass in a unique {@link validationTag}, so + * that on the backend we can figure out where the validation failed. */ export function validateAppState( + validationTag: string, self: User | undefined, identity: Identity | undefined, pcds: PCDCollection | undefined, diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index a18f6fb07d..6b476d6dfe 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -13,7 +13,7 @@ describe("validateAppState", async function () { const pcdPackages = [SemaphoreIdentityPCDPackage]; it("validateState returns no errors on valid logged out state", async function () { - const errors = validateAppState(undefined, undefined, undefined); + const errors = validateAppState("test", undefined, undefined, undefined); expect(errors.errors.length).to.eq(0); expect(errors.userUUID).to.eq(undefined); }); @@ -36,7 +36,7 @@ describe("validateAppState", async function () { identity }) ); - const errors = validateAppState(self, identity, pcds); + const errors = validateAppState("test", self, identity, pcds); expect(errors.errors.length).to.eq(0); expect(errors.userUUID).to.eq(self.uuid); }); @@ -55,7 +55,7 @@ describe("validateAppState", async function () { }; const pcds = new PCDCollection(pcdPackages); // deliberately create empty pcd collection - const errors = validateAppState(self, identity, pcds); + const errors = validateAppState("test", self, identity, pcds); expect(errors.errors.length).to.eq(0); expect(errors.userUUID).to.eq(self.uuid); }); From 00e5e496341ea0423a853dc41b57fcf55f4b9e37 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:30:17 -0800 Subject: [PATCH 41/71] console.log -> console.error --- apps/passport-client/src/localstorage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index 28a88a5154..8102f304ef 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -21,7 +21,7 @@ export async function savePCDs(pcds: PCDCollection): Promise { if ( !validateAndLogStateErrors("savePCDs", undefined, undefined, pcds, true) ) { - console.log( + console.error( "PCD Collection failed to validate - not writing it to localstorage" ); return; @@ -55,7 +55,7 @@ export async function loadPCDs(): Promise { true ) ) { - console.log( + console.error( "PCD Collection failed to validate when loading from localstorage" ); } From c527bb0bc719b517122b435dfa2c924aa54c9d6a Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:32:28 -0800 Subject: [PATCH 42/71] unwrap errors object when logging to server --- apps/passport-client/src/validateState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index bb4deb7f91..4e5c3227fb 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -142,7 +142,7 @@ export async function logValidationErrors( errors.userUUID = errors.userUUID ?? user?.uuid; console.log(`encountered state validation errors: `, errors); await requestLogToServer(appConfig.zupassServer, "state-validation-error", { - errors + ...errors }); } catch (e) { console.log("error reporting errors", e); From 44eb78657ce4ba23e54225beb6e752d75d6ea75e Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:42:31 -0800 Subject: [PATCH 43/71] update validateState to be less confusing for cases when something is undefined --- apps/passport-client/src/validateState.ts | 47 +++++++++++++++-------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 4e5c3227fb..67a527b664 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -102,25 +102,40 @@ export function validateAppState( const commitmentFromSelfField = self?.commitment; const commitmentFromIdentityField = identity?.commitment?.toString(); - if (commitmentOfIdentityPCDInCollection !== commitmentFromSelfField) { - validationErrors.push( - `commitment of identity pcd in collection (${commitmentOfIdentityPCDInCollection})` + - ` does not match commitment in 'self' field of app state (${commitmentFromSelfField})` - ); + if (commitmentFromSelfField === undefined) { + validationErrors.push(`'self' missing a commitment`); } - if (commitmentFromSelfField !== commitmentFromIdentityField) { - validationErrors.push( - `commitment in 'self' field of app state (${commitmentFromSelfField})` + - ` does not match commitment of 'identity' field of app state (${commitmentFromIdentityField})` - ); - } + if ( + commitmentFromSelfField === undefined || + commitmentOfIdentityPCDInCollection === undefined || + commitmentFromIdentityField === undefined + ) { + // these cases are validated earlier in this function + } else { + // in 'else' block we check that the commitments from all three + // places that the user's commitment exists match - in the self, the + // identity, and in the pcd collection + + if (commitmentOfIdentityPCDInCollection !== commitmentFromSelfField) { + validationErrors.push( + `commitment of identity pcd in collection (${commitmentOfIdentityPCDInCollection})` + + ` does not match commitment in 'self' field of app state (${commitmentFromSelfField})` + ); + } + if (commitmentFromSelfField !== commitmentFromIdentityField) { + validationErrors.push( + `commitment in 'self' field of app state (${commitmentFromSelfField})` + + ` does not match commitment of 'identity' field of app state (${commitmentFromIdentityField})` + ); + } - if (commitmentFromIdentityField !== commitmentOfIdentityPCDInCollection) { - validationErrors.push( - `commitment of 'identity' field of app state (${commitmentFromIdentityField})` + - ` does not match commitment of identity pcd in collection (${commitmentOfIdentityPCDInCollection})` - ); + if (commitmentFromIdentityField !== commitmentOfIdentityPCDInCollection) { + validationErrors.push( + `commitment of 'identity' field of app state (${commitmentFromIdentityField})` + + ` does not match commitment of identity pcd in collection (${commitmentOfIdentityPCDInCollection})` + ); + } } return { From 215487be2be7959012fa205d848eb48b1cb440d6 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:44:52 -0800 Subject: [PATCH 44/71] fix test --- apps/passport-client/src/validateState.ts | 2 -- apps/passport-client/test/stateValidation.spec.ts | 7 +++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 67a527b664..7b055ea238 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -58,9 +58,7 @@ export function validateAppState( forceCheckPCDs?: boolean ): ValidationErrors { const validationErrors: string[] = []; - const loggedOut = !self; - const identityPCDFromCollection = pcds?.getPCDsByType( SemaphoreIdentityPCDPackage.name )?.[0] as SemaphoreIdentityPCD | undefined; diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index 6b476d6dfe..fdaba4bdc6 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -41,7 +41,7 @@ describe("validateAppState", async function () { expect(errors.userUUID).to.eq(self.uuid); }); - it("validateState returns no errors on valid logged in state", async function () { + it("validateState returns errors for situation where the pcd collection is empty", async function () { const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( "testpassword123!@#asdf" @@ -56,7 +56,10 @@ describe("validateAppState", async function () { const pcds = new PCDCollection(pcdPackages); // deliberately create empty pcd collection const errors = validateAppState("test", self, identity, pcds); - expect(errors.errors.length).to.eq(0); + expect(errors.errors).to.deep.eq([ + "'pcds' contains no pcds", + "'pcds' field in app state does not contain an identity PCD" + ]); expect(errors.userUUID).to.eq(self.uuid); }); }); From 6fab637fee2e9867273dd740e41bb86c167d5293 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:50:53 -0800 Subject: [PATCH 45/71] testing logged out state --- .../test/stateValidation.spec.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index fdaba4bdc6..2933bda516 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -6,7 +6,7 @@ import { Identity } from "@semaphore-protocol/identity"; import { expect } from "chai"; import { v4 as uuid } from "uuid"; import { randomEmail } from "../src/util"; -import { validateAppState } from "../src/validateState"; +import { ValidationErrors, validateAppState } from "../src/validateState"; describe("validateAppState", async function () { const crypto = await PCDCrypto.newInstance(); @@ -18,6 +18,26 @@ describe("validateAppState", async function () { expect(errors.userUUID).to.eq(undefined); }); + it("validateState returns errors for logged out state with invalid PCD collections", async function () { + expect( + validateAppState( + "test", + undefined, + undefined, + (() => { + return new PCDCollection(pcdPackages); + })(), + true + ) + ).to.deep.eq({ + errors: [ + "'pcds' contains no pcds", + "'pcds' field in app state does not contain an identity PCD" + ], + userUUID: undefined + } satisfies ValidationErrors); + }); + it("validateState returns no errors on valid logged in state", async function () { const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( From 20716440214c250b13100744ab0c646e3bba19ad Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 11:59:09 -0800 Subject: [PATCH 46/71] one more test case --- .../test/stateValidation.spec.ts | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index 2933bda516..432524d432 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -1,6 +1,8 @@ +import { EdDSAPCD, EdDSAPCDPackage } from "@pcd/eddsa-pcd"; import { PCDCrypto } from "@pcd/passport-crypto"; import { ZupassUserJson } from "@pcd/passport-interface"; import { PCDCollection } from "@pcd/pcd-collection"; +import { ArgumentTypeName } from "@pcd/pcd-types"; import { SemaphoreIdentityPCDPackage } from "@pcd/semaphore-identity-pcd"; import { Identity } from "@semaphore-protocol/identity"; import { expect } from "chai"; @@ -8,9 +10,26 @@ import { v4 as uuid } from "uuid"; import { randomEmail } from "../src/util"; import { ValidationErrors, validateAppState } from "../src/validateState"; +function newEdSAPCD(): Promise { + return EdDSAPCDPackage.prove({ + message: { + value: ["0x12345", "0x54321", "0xdeadbeef"], + argumentType: ArgumentTypeName.StringArray + }, + privateKey: { + value: "0001020304050607080900010203040506070809000102030405060708090001", + argumentType: ArgumentTypeName.String + }, + id: { + value: undefined, + argumentType: ArgumentTypeName.String + } + }); +} + describe("validateAppState", async function () { const crypto = await PCDCrypto.newInstance(); - const pcdPackages = [SemaphoreIdentityPCDPackage]; + const pcdPackages = [SemaphoreIdentityPCDPackage, EdDSAPCDPackage]; it("validateState returns no errors on valid logged out state", async function () { const errors = validateAppState("test", undefined, undefined, undefined); @@ -24,7 +43,7 @@ describe("validateAppState", async function () { "test", undefined, undefined, - (() => { + await (async () => { return new PCDCollection(pcdPackages); })(), true @@ -36,6 +55,33 @@ describe("validateAppState", async function () { ], userUUID: undefined } satisfies ValidationErrors); + + expect( + validateAppState( + "test", + undefined, + undefined, + await (async () => { + const collection = new PCDCollection(pcdPackages); + collection.add(await newEdSAPCD()); + return collection; + })(), + true + ) + ).to.deep.eq({ + errors: ["'pcds' field in app state does not contain an identity PCD"], + userUUID: undefined + } satisfies ValidationErrors); + + expect( + validateAppState("test", undefined, undefined, undefined, true) + ).to.deep.eq({ + errors: [ + "'pcds' field in app state does not contain an identity PCD", + "missing 'pcds'" + ], + userUUID: undefined + } satisfies ValidationErrors); }); it("validateState returns no errors on valid logged in state", async function () { From 5b01b79a5f28c908d7c372a5ddd2fc4caef543b7 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 12:08:20 -0800 Subject: [PATCH 47/71] cleanup --- .../test/stateValidation.spec.ts | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index 432524d432..e124b4b9e7 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -31,13 +31,13 @@ describe("validateAppState", async function () { const crypto = await PCDCrypto.newInstance(); const pcdPackages = [SemaphoreIdentityPCDPackage, EdDSAPCDPackage]; - it("validateState returns no errors on valid logged out state", async function () { + it("logged out ; no errors", async function () { const errors = validateAppState("test", undefined, undefined, undefined); expect(errors.errors.length).to.eq(0); expect(errors.userUUID).to.eq(undefined); }); - it("validateState returns errors for logged out state with invalid PCD collections", async function () { + it("logged out ; forceCheckPCDs=true; all error states caught", async function () { expect( validateAppState( "test", @@ -77,14 +77,14 @@ describe("validateAppState", async function () { validateAppState("test", undefined, undefined, undefined, true) ).to.deep.eq({ errors: [ - "'pcds' field in app state does not contain an identity PCD", - "missing 'pcds'" + "missing 'pcds'", + "'pcds' field in app state does not contain an identity PCD" ], userUUID: undefined } satisfies ValidationErrors); }); - it("validateState returns no errors on valid logged in state", async function () { + it("logged in ; no errors", async function () { const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( "testpassword123!@#asdf" @@ -102,12 +102,13 @@ describe("validateAppState", async function () { identity }) ); - const errors = validateAppState("test", self, identity, pcds); - expect(errors.errors.length).to.eq(0); - expect(errors.userUUID).to.eq(self.uuid); + expect(validateAppState("test", self, identity, pcds)).to.deep.eq({ + userUUID: self.uuid, + errors: [] + } satisfies ValidationErrors); }); - it("validateState returns errors for situation where the pcd collection is empty", async function () { + it("logged in ; empty pcd collection ; errors", async function () { const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( "testpassword123!@#asdf" @@ -120,12 +121,33 @@ describe("validateAppState", async function () { uuid: uuid() }; const pcds = new PCDCollection(pcdPackages); - // deliberately create empty pcd collection - const errors = validateAppState("test", self, identity, pcds); - expect(errors.errors).to.deep.eq([ - "'pcds' contains no pcds", - "'pcds' field in app state does not contain an identity PCD" - ]); - expect(errors.userUUID).to.eq(self.uuid); + expect(validateAppState("test", self, identity, pcds)).to.deep.eq({ + userUUID: self.uuid, + errors: [ + "'pcds' contains no pcds", + "'pcds' field in app state does not contain an identity PCD" + ] + } satisfies ValidationErrors); + }); + + it("logged in ; missing pcd collection ; errors", async function () { + const identity = new Identity(); + const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( + "testpassword123!@#asdf" + ); + const self: ZupassUserJson = { + commitment: identity.commitment.toString(), + email: randomEmail(), + salt: saltAndEncryptionKey.salt, + terms_agreed: 1, + uuid: uuid() + }; + expect(validateAppState("test", self, identity, undefined)).to.deep.eq({ + userUUID: self.uuid, + errors: [ + "missing 'pcds'", + "'pcds' field in app state does not contain an identity PCD" + ] + } satisfies ValidationErrors); }); }); From e63a5f1d67e0432672d34d70ca594b9f3e2f4a5a Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 12:11:56 -0800 Subject: [PATCH 48/71] add case for missing identity --- apps/passport-client/src/validateState.ts | 4 ---- .../test/stateValidation.spec.ts | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 7b055ea238..e908c6d22b 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -86,10 +86,6 @@ export function validateAppState( }; } - if (!self) { - validationErrors.push("missing 'self'"); - } - if (!identity) { validationErrors.push("missing 'identity'"); } diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index e124b4b9e7..031b7a7f4b 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -150,4 +150,28 @@ describe("validateAppState", async function () { ] } satisfies ValidationErrors); }); + + it("logged in ; missing identity ; errors", async function () { + const identity = new Identity(); + const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( + "testpassword123!@#asdf" + ); + const self: ZupassUserJson = { + commitment: identity.commitment.toString(), + email: randomEmail(), + salt: saltAndEncryptionKey.salt, + terms_agreed: 1, + uuid: uuid() + }; + const pcds = new PCDCollection(pcdPackages); + pcds.add( + await SemaphoreIdentityPCDPackage.prove({ + identity + }) + ); + expect(validateAppState("test", self, undefined, pcds)).to.deep.eq({ + userUUID: self.uuid, + errors: ["missing 'identity'"] + } satisfies ValidationErrors); + }); }); From 430e506458c5e761a0e6148a502c3de83dc22a13 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 12:17:02 -0800 Subject: [PATCH 49/71] actually propagate log tag --- apps/passport-client/pages/index.tsx | 1 + apps/passport-client/src/validateState.ts | 30 ++++++---- .../test/stateValidation.spec.ts | 56 +++++++++++-------- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/apps/passport-client/pages/index.tsx b/apps/passport-client/pages/index.tsx index 31ab26aaa7..efce8fd3cb 100644 --- a/apps/passport-client/pages/index.tsx +++ b/apps/passport-client/pages/index.tsx @@ -470,6 +470,7 @@ async function loadInitialState(): Promise { if (extraValidationErrors.length > 0) { logValidationErrors({ + tag: "afterLoadInitialState", errors: extraValidationErrors, userUUID: self?.uuid }); diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index e908c6d22b..973b55d575 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -16,14 +16,14 @@ import { loadSelf } from "./localstorage"; * In the case there are no validation errors, returns `false`. */ export function validateAndLogStateErrors( - validationTag: string, + tag: string, self: User | undefined, identity: Identity | undefined, pcds: PCDCollection | undefined, forceCheckPCDs?: boolean ): boolean { const validationErrors = validateAppState( - validationTag, + tag, self, identity, pcds, @@ -40,23 +40,23 @@ export function validateAndLogStateErrors( /** * Determines whether the app's global state contains valid data. If it does not, - * returns the set of things that are incorrect about it in a {@link ValidationErrors} + * returns the set of things that are incorrect about it in a {@link ErrorReport} * object. If there were no validation errors the result will contain an empty array * of errors. * * The provided {@link PCDCollection} is not checked unless either this function * determines the user is logged in or the {@link forceCheckPCDs} argument is `true`. * - * Depending on where this function is called, pass in a unique {@link validationTag}, so + * Depending on where this function is called, pass in a unique {@link tag}, so * that on the backend we can figure out where the validation failed. */ export function validateAppState( - validationTag: string, + tag: string, self: User | undefined, identity: Identity | undefined, pcds: PCDCollection | undefined, forceCheckPCDs?: boolean -): ValidationErrors { +): ErrorReport { const validationErrors: string[] = []; const loggedOut = !self; const identityPCDFromCollection = pcds?.getPCDsByType( @@ -82,7 +82,8 @@ export function validateAppState( if (loggedOut) { return { errors: validationErrors, - userUUID: undefined + userUUID: undefined, + tag }; } @@ -134,7 +135,8 @@ export function validateAppState( return { errors: validationErrors, - userUUID: self?.uuid + userUUID: self?.uuid, + tag }; } @@ -143,9 +145,7 @@ export function validateAppState( * we have records and are able to identify common types of errors. Does not leak * sensitive information, such as decrypted versions of e2ee storage. */ -export async function logValidationErrors( - errors: ValidationErrors -): Promise { +export async function logValidationErrors(errors: ErrorReport): Promise { try { const user = loadSelf(); errors.userUUID = errors.userUUID ?? user?.uuid; @@ -161,7 +161,7 @@ export async function logValidationErrors( /** * Uploaded to server in case of a state validation error. */ -export interface ValidationErrors { +export interface ErrorReport { /** * Human readable non-sensitive-information-leaking errors. If this array is empty, * it represents a state that has no errors. @@ -172,4 +172,10 @@ export interface ValidationErrors { * Used to identify the user on the server-side. */ userUUID?: string; + + /** + * Uniquely identifies the location in the code from which this error report + * was initiated. + */ + tag: string; } diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index 031b7a7f4b..f3267db878 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -8,7 +8,7 @@ import { Identity } from "@semaphore-protocol/identity"; import { expect } from "chai"; import { v4 as uuid } from "uuid"; import { randomEmail } from "../src/util"; -import { ValidationErrors, validateAppState } from "../src/validateState"; +import { ErrorReport, validateAppState } from "../src/validateState"; function newEdSAPCD(): Promise { return EdDSAPCDPackage.prove({ @@ -32,7 +32,7 @@ describe("validateAppState", async function () { const pcdPackages = [SemaphoreIdentityPCDPackage, EdDSAPCDPackage]; it("logged out ; no errors", async function () { - const errors = validateAppState("test", undefined, undefined, undefined); + const errors = validateAppState(TAG_STR, undefined, undefined, undefined); expect(errors.errors.length).to.eq(0); expect(errors.userUUID).to.eq(undefined); }); @@ -40,7 +40,7 @@ describe("validateAppState", async function () { it("logged out ; forceCheckPCDs=true; all error states caught", async function () { expect( validateAppState( - "test", + TAG_STR, undefined, undefined, await (async () => { @@ -53,12 +53,13 @@ describe("validateAppState", async function () { "'pcds' contains no pcds", "'pcds' field in app state does not contain an identity PCD" ], - userUUID: undefined - } satisfies ValidationErrors); + userUUID: undefined, + ...TAG + } satisfies ErrorReport); expect( validateAppState( - "test", + TAG_STR, undefined, undefined, await (async () => { @@ -70,18 +71,20 @@ describe("validateAppState", async function () { ) ).to.deep.eq({ errors: ["'pcds' field in app state does not contain an identity PCD"], - userUUID: undefined - } satisfies ValidationErrors); + userUUID: undefined, + ...TAG + } satisfies ErrorReport); expect( - validateAppState("test", undefined, undefined, undefined, true) + validateAppState(TAG_STR, undefined, undefined, undefined, true) ).to.deep.eq({ errors: [ "missing 'pcds'", "'pcds' field in app state does not contain an identity PCD" ], - userUUID: undefined - } satisfies ValidationErrors); + userUUID: undefined, + ...TAG + } satisfies ErrorReport); }); it("logged in ; no errors", async function () { @@ -102,10 +105,11 @@ describe("validateAppState", async function () { identity }) ); - expect(validateAppState("test", self, identity, pcds)).to.deep.eq({ + expect(validateAppState(TAG_STR, self, identity, pcds)).to.deep.eq({ userUUID: self.uuid, - errors: [] - } satisfies ValidationErrors); + errors: [], + ...TAG + } satisfies ErrorReport); }); it("logged in ; empty pcd collection ; errors", async function () { @@ -121,13 +125,14 @@ describe("validateAppState", async function () { uuid: uuid() }; const pcds = new PCDCollection(pcdPackages); - expect(validateAppState("test", self, identity, pcds)).to.deep.eq({ + expect(validateAppState(TAG_STR, self, identity, pcds)).to.deep.eq({ userUUID: self.uuid, errors: [ "'pcds' contains no pcds", "'pcds' field in app state does not contain an identity PCD" - ] - } satisfies ValidationErrors); + ], + ...TAG + } satisfies ErrorReport); }); it("logged in ; missing pcd collection ; errors", async function () { @@ -142,13 +147,14 @@ describe("validateAppState", async function () { terms_agreed: 1, uuid: uuid() }; - expect(validateAppState("test", self, identity, undefined)).to.deep.eq({ + expect(validateAppState(TAG_STR, self, identity, undefined)).to.deep.eq({ userUUID: self.uuid, errors: [ "missing 'pcds'", "'pcds' field in app state does not contain an identity PCD" - ] - } satisfies ValidationErrors); + ], + ...TAG + } satisfies ErrorReport); }); it("logged in ; missing identity ; errors", async function () { @@ -169,9 +175,13 @@ describe("validateAppState", async function () { identity }) ); - expect(validateAppState("test", self, undefined, pcds)).to.deep.eq({ + expect(validateAppState(TAG_STR, self, undefined, pcds)).to.deep.eq({ userUUID: self.uuid, - errors: ["missing 'identity'"] - } satisfies ValidationErrors); + errors: ["missing 'identity'"], + ...TAG + } satisfies ErrorReport); }); }); + +const TAG_STR = "test"; +const TAG = { tag: TAG_STR }; From f5d9125e3a5628afa6feea7ff18cd469bee0c3bd Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 12:19:53 -0800 Subject: [PATCH 50/71] more test cleanup --- apps/passport-client/test/stateValidation.spec.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index f3267db878..056bbc838f 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -32,9 +32,13 @@ describe("validateAppState", async function () { const pcdPackages = [SemaphoreIdentityPCDPackage, EdDSAPCDPackage]; it("logged out ; no errors", async function () { - const errors = validateAppState(TAG_STR, undefined, undefined, undefined); - expect(errors.errors.length).to.eq(0); - expect(errors.userUUID).to.eq(undefined); + expect( + validateAppState(TAG_STR, undefined, undefined, undefined) + ).to.deep.eq({ + errors: [], + userUUID: undefined, + ...TAG + } satisfies ErrorReport); }); it("logged out ; forceCheckPCDs=true; all error states caught", async function () { From 7fbf5631d9a629d80a1a25c8e3d0cd47fa351ddd Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 12:20:47 -0800 Subject: [PATCH 51/71] more cleanup --- apps/passport-client/test/stateValidation.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index 056bbc838f..ab532ead9e 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -47,9 +47,7 @@ describe("validateAppState", async function () { TAG_STR, undefined, undefined, - await (async () => { - return new PCDCollection(pcdPackages); - })(), + new PCDCollection(pcdPackages), true ) ).to.deep.eq({ From 95ab85cca27ea800718352278c67e32bb67faafe Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 12:22:23 -0800 Subject: [PATCH 52/71] another test case --- .../test/stateValidation.spec.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index ab532ead9e..fd210b38fc 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -183,6 +183,32 @@ describe("validateAppState", async function () { ...TAG } satisfies ErrorReport); }); + + it("logged in ; self missing commitment ; errors", async function () { + const identity = new Identity(); + const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( + "testpassword123!@#asdf" + ); + const self: ZupassUserJson = { + commitment: identity.commitment.toString(), + email: randomEmail(), + salt: saltAndEncryptionKey.salt, + terms_agreed: 1, + uuid: uuid() + }; + const pcds = new PCDCollection(pcdPackages); + pcds.add( + await SemaphoreIdentityPCDPackage.prove({ + identity + }) + ); + delete self.commitment; + expect(validateAppState(TAG_STR, self, identity, pcds)).to.deep.eq({ + userUUID: self.uuid, + errors: ["'self' missing a commitment"], + ...TAG + } satisfies ErrorReport); + }); }); const TAG_STR = "test"; From 69a6344b7f59206c01c3fe187aef181b9ae2a82f Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 12:26:42 -0800 Subject: [PATCH 53/71] slight refactor --- .../test/stateValidation.spec.ts | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index fd210b38fc..3268bfdd10 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -30,6 +30,12 @@ function newEdSAPCD(): Promise { describe("validateAppState", async function () { const crypto = await PCDCrypto.newInstance(); const pcdPackages = [SemaphoreIdentityPCDPackage, EdDSAPCDPackage]; + const identity1 = new Identity( + '["0xaa5fa3165e1ca129bd7a2b3bada18c5f81350faacf2edff59cf44eeba2e2d","0xa944ed90153fc2be5759a0d18eda47266885aea0966ef4dbb96ff979c29ed4"]' + ); + const identity2 = new Identity( + '["0x8526e030dbd593833f24bf73b60f0bcc58690c590b9953acc741f2eb71394d","0x520e4ae6f5d5e4526dd517e61defe16f90bd4aef72b41394285e77463e0c69"]' + ); it("logged out ; no errors", async function () { expect( @@ -90,12 +96,11 @@ describe("validateAppState", async function () { }); it("logged in ; no errors", async function () { - const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( "testpassword123!@#asdf" ); const self: ZupassUserJson = { - commitment: identity.commitment.toString(), + commitment: identity1.commitment.toString(), email: randomEmail(), salt: saltAndEncryptionKey.salt, terms_agreed: 1, @@ -104,10 +109,10 @@ describe("validateAppState", async function () { const pcds = new PCDCollection(pcdPackages); pcds.add( await SemaphoreIdentityPCDPackage.prove({ - identity + identity: identity1 }) ); - expect(validateAppState(TAG_STR, self, identity, pcds)).to.deep.eq({ + expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ userUUID: self.uuid, errors: [], ...TAG @@ -115,19 +120,18 @@ describe("validateAppState", async function () { }); it("logged in ; empty pcd collection ; errors", async function () { - const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( "testpassword123!@#asdf" ); const self: ZupassUserJson = { - commitment: identity.commitment.toString(), + commitment: identity1.commitment.toString(), email: randomEmail(), salt: saltAndEncryptionKey.salt, terms_agreed: 1, uuid: uuid() }; const pcds = new PCDCollection(pcdPackages); - expect(validateAppState(TAG_STR, self, identity, pcds)).to.deep.eq({ + expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ userUUID: self.uuid, errors: [ "'pcds' contains no pcds", @@ -138,18 +142,17 @@ describe("validateAppState", async function () { }); it("logged in ; missing pcd collection ; errors", async function () { - const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( "testpassword123!@#asdf" ); const self: ZupassUserJson = { - commitment: identity.commitment.toString(), + commitment: identity1.commitment.toString(), email: randomEmail(), salt: saltAndEncryptionKey.salt, terms_agreed: 1, uuid: uuid() }; - expect(validateAppState(TAG_STR, self, identity, undefined)).to.deep.eq({ + expect(validateAppState(TAG_STR, self, identity1, undefined)).to.deep.eq({ userUUID: self.uuid, errors: [ "missing 'pcds'", @@ -160,12 +163,11 @@ describe("validateAppState", async function () { }); it("logged in ; missing identity ; errors", async function () { - const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( "testpassword123!@#asdf" ); const self: ZupassUserJson = { - commitment: identity.commitment.toString(), + commitment: identity1.commitment.toString(), email: randomEmail(), salt: saltAndEncryptionKey.salt, terms_agreed: 1, @@ -174,7 +176,7 @@ describe("validateAppState", async function () { const pcds = new PCDCollection(pcdPackages); pcds.add( await SemaphoreIdentityPCDPackage.prove({ - identity + identity: identity1 }) ); expect(validateAppState(TAG_STR, self, undefined, pcds)).to.deep.eq({ @@ -185,12 +187,11 @@ describe("validateAppState", async function () { }); it("logged in ; self missing commitment ; errors", async function () { - const identity = new Identity(); const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( "testpassword123!@#asdf" ); const self: ZupassUserJson = { - commitment: identity.commitment.toString(), + commitment: identity1.commitment.toString(), email: randomEmail(), salt: saltAndEncryptionKey.salt, terms_agreed: 1, @@ -199,16 +200,40 @@ describe("validateAppState", async function () { const pcds = new PCDCollection(pcdPackages); pcds.add( await SemaphoreIdentityPCDPackage.prove({ - identity + identity: identity1 }) ); delete self.commitment; - expect(validateAppState(TAG_STR, self, identity, pcds)).to.deep.eq({ + expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ userUUID: self.uuid, errors: ["'self' missing a commitment"], ...TAG } satisfies ErrorReport); }); + + it("logged in ; no errors", async function () { + const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( + "testpassword123!@#asdf" + ); + const self: ZupassUserJson = { + commitment: identity1.commitment.toString(), + email: randomEmail(), + salt: saltAndEncryptionKey.salt, + terms_agreed: 1, + uuid: uuid() + }; + const pcds = new PCDCollection(pcdPackages); + pcds.add( + await SemaphoreIdentityPCDPackage.prove({ + identity: identity1 + }) + ); + expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ + userUUID: self.uuid, + errors: [], + ...TAG + } satisfies ErrorReport); + }); }); const TAG_STR = "test"; From e11aacdbdf5f34d26dcad9fea7e3b9d026cea1b2 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 12:37:09 -0800 Subject: [PATCH 54/71] more validation tests --- .../test/stateValidation.spec.ts | 106 +++++++++++++++++- 1 file changed, 100 insertions(+), 6 deletions(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index 3268bfdd10..8a284fc1d9 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -31,11 +31,20 @@ describe("validateAppState", async function () { const crypto = await PCDCrypto.newInstance(); const pcdPackages = [SemaphoreIdentityPCDPackage, EdDSAPCDPackage]; const identity1 = new Identity( - '["0xaa5fa3165e1ca129bd7a2b3bada18c5f81350faacf2edff59cf44eeba2e2d","0xa944ed90153fc2be5759a0d18eda47266885aea0966ef4dbb96ff979c29ed4"]' + '["0xaa5fa3165e1ca129bd7a2b3bada18c5f81350faacf2edff59cf44eeba2e2d",' + + '"0xa944ed90153fc2be5759a0d18eda47266885aea0966ef4dbb96ff979c29ed4"]' ); + const commitment1 = identity1.commitment; const identity2 = new Identity( - '["0x8526e030dbd593833f24bf73b60f0bcc58690c590b9953acc741f2eb71394d","0x520e4ae6f5d5e4526dd517e61defe16f90bd4aef72b41394285e77463e0c69"]' + '["0x8526e030dbd593833f24bf73b60f0bcc58690c590b9953acc741f2eb71394d",' + + '"0x520e4ae6f5d5e4526dd517e61defe16f90bd4aef72b41394285e77463e0c69"]' ); + const commitment2 = identity2.commitment; + const identity3 = new Identity( + '["0x4837c6f88904d1dfefcb7dc6486e95c06cda6eb76d76a9888167c0993e40f0",' + + '"0x956f9e03b0cc324045d24f9b20531e547272fab8e8ee2f96c0cf2e50311468"]' + ); + const commitment3 = identity3.commitment; it("logged out ; no errors", async function () { expect( @@ -47,7 +56,7 @@ describe("validateAppState", async function () { } satisfies ErrorReport); }); - it("logged out ; forceCheckPCDs=true; all error states caught", async function () { + it("logged out ; forceCheckPCDs=true; test of all error states", async function () { expect( validateAppState( TAG_STR, @@ -211,12 +220,12 @@ describe("validateAppState", async function () { } satisfies ErrorReport); }); - it("logged in ; no errors", async function () { + it("logged in ; self commitment wrong ; errors", async function () { const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( "testpassword123!@#asdf" ); const self: ZupassUserJson = { - commitment: identity1.commitment.toString(), + commitment: identity2.commitment.toString(), email: randomEmail(), salt: saltAndEncryptionKey.salt, terms_agreed: 1, @@ -230,7 +239,92 @@ describe("validateAppState", async function () { ); expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ userUUID: self.uuid, - errors: [], + errors: [ + `commitment of identity pcd in collection (${commitment1}) does not match commitment in 'self' field of app state (${commitment2})`, + `commitment in 'self' field of app state (${commitment2}) does not match commitment of 'identity' field of app state (${commitment1})` + ], + ...TAG + } satisfies ErrorReport); + }); + + it("logged in ; pcd collection identity wrong ; errors", async function () { + const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( + "testpassword123!@#asdf" + ); + const self: ZupassUserJson = { + commitment: identity1.commitment.toString(), + email: randomEmail(), + salt: saltAndEncryptionKey.salt, + terms_agreed: 1, + uuid: uuid() + }; + const pcds = new PCDCollection(pcdPackages); + pcds.add( + await SemaphoreIdentityPCDPackage.prove({ + identity: identity2 + }) + ); + expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ + userUUID: self.uuid, + errors: [ + `commitment of identity pcd in collection (${commitment2}) does not match commitment in 'self' field of app state (${commitment1})`, + `commitment of 'identity' field of app state (${commitment1}) does not match commitment of identity pcd in collection (${commitment2})` + ], + ...TAG + } satisfies ErrorReport); + }); + + it("logged in ; appState identity wrong ; errors", async function () { + const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( + "testpassword123!@#asdf" + ); + const self: ZupassUserJson = { + commitment: identity1.commitment.toString(), + email: randomEmail(), + salt: saltAndEncryptionKey.salt, + terms_agreed: 1, + uuid: uuid() + }; + const pcds = new PCDCollection(pcdPackages); + pcds.add( + await SemaphoreIdentityPCDPackage.prove({ + identity: identity1 + }) + ); + expect(validateAppState(TAG_STR, self, identity2, pcds)).to.deep.eq({ + userUUID: self.uuid, + errors: [ + `commitment in 'self' field of app state (${commitment1}) does not match commitment of 'identity' field of app state (${commitment2})`, + `commitment of 'identity' field of app state (${commitment2}) does not match commitment of identity pcd in collection (${commitment1})` + ], + ...TAG + } satisfies ErrorReport); + }); + + it("logged in ; all identities mistmatched ; errors", async function () { + const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( + "testpassword123!@#asdf" + ); + const self: ZupassUserJson = { + commitment: identity1.commitment.toString(), + email: randomEmail(), + salt: saltAndEncryptionKey.salt, + terms_agreed: 1, + uuid: uuid() + }; + const pcds = new PCDCollection(pcdPackages); + pcds.add( + await SemaphoreIdentityPCDPackage.prove({ + identity: identity2 + }) + ); + expect(validateAppState(TAG_STR, self, identity3, pcds)).to.deep.eq({ + userUUID: self.uuid, + errors: [ + `commitment of identity pcd in collection (${commitment2}) does not match commitment in 'self' field of app state (${commitment1})`, + `commitment in 'self' field of app state (${commitment1}) does not match commitment of 'identity' field of app state (${commitment3})`, + `commitment of 'identity' field of app state (${commitment3}) does not match commitment of identity pcd in collection (${commitment2})` + ], ...TAG } satisfies ErrorReport); }); From 2c81bdbce501321de533b22dd2a7ed960f4fb071 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 12:38:35 -0800 Subject: [PATCH 55/71] state validation tests look good --- .../test/stateValidation.spec.ts | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index 8a284fc1d9..a8595a0335 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -29,6 +29,9 @@ function newEdSAPCD(): Promise { describe("validateAppState", async function () { const crypto = await PCDCrypto.newInstance(); + const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( + "testpassword123!@#asdf" + ); const pcdPackages = [SemaphoreIdentityPCDPackage, EdDSAPCDPackage]; const identity1 = new Identity( '["0xaa5fa3165e1ca129bd7a2b3bada18c5f81350faacf2edff59cf44eeba2e2d",' + @@ -105,9 +108,6 @@ describe("validateAppState", async function () { }); it("logged in ; no errors", async function () { - const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( - "testpassword123!@#asdf" - ); const self: ZupassUserJson = { commitment: identity1.commitment.toString(), email: randomEmail(), @@ -129,9 +129,6 @@ describe("validateAppState", async function () { }); it("logged in ; empty pcd collection ; errors", async function () { - const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( - "testpassword123!@#asdf" - ); const self: ZupassUserJson = { commitment: identity1.commitment.toString(), email: randomEmail(), @@ -151,9 +148,6 @@ describe("validateAppState", async function () { }); it("logged in ; missing pcd collection ; errors", async function () { - const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( - "testpassword123!@#asdf" - ); const self: ZupassUserJson = { commitment: identity1.commitment.toString(), email: randomEmail(), @@ -172,9 +166,6 @@ describe("validateAppState", async function () { }); it("logged in ; missing identity ; errors", async function () { - const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( - "testpassword123!@#asdf" - ); const self: ZupassUserJson = { commitment: identity1.commitment.toString(), email: randomEmail(), @@ -196,9 +187,6 @@ describe("validateAppState", async function () { }); it("logged in ; self missing commitment ; errors", async function () { - const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( - "testpassword123!@#asdf" - ); const self: ZupassUserJson = { commitment: identity1.commitment.toString(), email: randomEmail(), @@ -221,9 +209,6 @@ describe("validateAppState", async function () { }); it("logged in ; self commitment wrong ; errors", async function () { - const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( - "testpassword123!@#asdf" - ); const self: ZupassUserJson = { commitment: identity2.commitment.toString(), email: randomEmail(), @@ -248,9 +233,6 @@ describe("validateAppState", async function () { }); it("logged in ; pcd collection identity wrong ; errors", async function () { - const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( - "testpassword123!@#asdf" - ); const self: ZupassUserJson = { commitment: identity1.commitment.toString(), email: randomEmail(), @@ -275,9 +257,6 @@ describe("validateAppState", async function () { }); it("logged in ; appState identity wrong ; errors", async function () { - const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( - "testpassword123!@#asdf" - ); const self: ZupassUserJson = { commitment: identity1.commitment.toString(), email: randomEmail(), @@ -302,9 +281,6 @@ describe("validateAppState", async function () { }); it("logged in ; all identities mistmatched ; errors", async function () { - const saltAndEncryptionKey = await crypto.generateSaltAndEncryptionKey( - "testpassword123!@#asdf" - ); const self: ZupassUserJson = { commitment: identity1.commitment.toString(), email: randomEmail(), From fcec42d4a0f255dffc48a2a9e96f0df82fd10081 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 13:01:24 -0800 Subject: [PATCH 56/71] introduce another validation function - initial state --- apps/passport-client/pages/index.tsx | 37 +----- apps/passport-client/src/dispatch.ts | 6 +- apps/passport-client/src/localstorage.ts | 6 +- .../src/useSyncE2EEStorage.tsx | 6 +- apps/passport-client/src/validateState.ts | 125 ++++++++++++------ .../test/stateValidation.spec.ts | 30 +++-- 6 files changed, 109 insertions(+), 101 deletions(-) diff --git a/apps/passport-client/pages/index.tsx b/apps/passport-client/pages/index.tsx index efce8fd3cb..2b306495b0 100644 --- a/apps/passport-client/pages/index.tsx +++ b/apps/passport-client/pages/index.tsx @@ -66,10 +66,7 @@ import { import { registerServiceWorker } from "../src/registerServiceWorker"; import { AppState, StateEmitter } from "../src/state"; import { pollUser } from "../src/user"; -import { - logValidationErrors, - validateAndLogStateErrors -} from "../src/validateState"; +import { validateAndLogInitialAppState } from "../src/validateState"; class App extends React.Component { state = undefined as AppState | undefined; @@ -444,38 +441,8 @@ async function loadInitialState(): Promise { serverStorageHash: persistentSyncStatus.serverStorageHash }; - if ( - !validateAndLogStateErrors( - "loadInitialState", - state.self, - state.identity, - state.pcds - ) - ) { + if (!validateAndLogInitialAppState("loadInitialState", state)) { state.userInvalid = true; - } else { - const extraValidationErrors = []; - - // this case covers a logged in user. the only way the app can get a 'self' - // is by requesting one from the server, to do which one has to be logged in. - if (state.self) { - if (!state.encryptionKey) { - extraValidationErrors.push(`logged in user missing encryption key`); - } - - if (!state.identity) { - extraValidationErrors.push(`logged in user missing identity`); - } - } - - if (extraValidationErrors.length > 0) { - logValidationErrors({ - tag: "afterLoadInitialState", - errors: extraValidationErrors, - userUUID: self?.uuid - }); - state.userInvalid = true; - } } return state; diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 080686ad9a..001dd25452 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -52,7 +52,7 @@ import { uploadStorage } from "./useSyncE2EEStorage"; import { assertUnreachable } from "./util"; -import { validateAndLogStateErrors } from "./validateState"; +import { validateAndLogRunningAppState } from "./validateState"; export type Dispatcher = (action: Action) => void; @@ -344,7 +344,7 @@ async function finishAccountCreation( ) { // Verify that the identity is correct. if ( - !validateAndLogStateErrors( + !validateAndLogRunningAppState( "finishAccountCreation", user, state.identity, @@ -530,7 +530,7 @@ async function loadAfterLogin( )[0] as SemaphoreIdentityPCD; if ( - !validateAndLogStateErrors( + !validateAndLogRunningAppState( "loadAfterLogin", userResponse.value, identityPCD.claim.identity, diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index 8102f304ef..0d7ea2a358 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -12,14 +12,14 @@ import { SemaphoreSignaturePCD } from "@pcd/semaphore-signature-pcd"; import { Identity } from "@semaphore-protocol/identity"; import { z } from "zod"; import { getPackages } from "./pcdPackages"; -import { validateAndLogStateErrors } from "./validateState"; +import { validateAndLogRunningAppState } from "./validateState"; const OLD_PCDS_KEY = "pcds"; // deprecated const COLLECTION_KEY = "pcd_collection"; export async function savePCDs(pcds: PCDCollection): Promise { if ( - !validateAndLogStateErrors("savePCDs", undefined, undefined, pcds, true) + !validateAndLogRunningAppState("savePCDs", undefined, undefined, pcds, true) ) { console.error( "PCD Collection failed to validate - not writing it to localstorage" @@ -47,7 +47,7 @@ export async function loadPCDs(): Promise { ); if ( - !validateAndLogStateErrors( + !validateAndLogRunningAppState( "loadPCDs", undefined, undefined, diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index ad95dfd2da..5d664c116a 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -29,7 +29,7 @@ import { } from "./localstorage"; import { getPackages } from "./pcdPackages"; import { useOnStateChange } from "./subscribe"; -import { validateAndLogStateErrors } from "./validateState"; +import { validateAndLogRunningAppState } from "./validateState"; export type UpdateBlobKeyStorageInfo = { revision: string; @@ -138,7 +138,7 @@ export async function uploadSerializedStorage( storageHash: string ): Promise { if ( - !validateAndLogStateErrors( + !validateAndLogRunningAppState( "uploadSerializedStorage", user, userIdentity, @@ -237,7 +237,7 @@ export async function downloadStorage( ); if ( - !validateAndLogStateErrors( + !validateAndLogRunningAppState( "downloadStorage", undefined, undefined, diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 973b55d575..d67b6fd8c5 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -7,22 +7,30 @@ import { import { Identity } from "@semaphore-protocol/identity"; import { appConfig } from "./appConfig"; import { loadSelf } from "./localstorage"; +import { AppState } from "./state"; -/** - * Validates the application state using {@link validateAppState}. In the case - * that the state is invalid, returns `true`, and concurrently uploads the validation - * errors to the server for further inspection. - * - * In the case there are no validation errors, returns `false`. - */ -export function validateAndLogStateErrors( +export function validateAndLogInitialAppState( + tag: string, + state: AppState +): boolean { + const validationErrors = validateInitialAppState(tag, state); + + if (validationErrors.errors.length > 0) { + logValidationErrors(validationErrors); + return false; + } + + return true; +} + +export function validateAndLogRunningAppState( tag: string, self: User | undefined, identity: Identity | undefined, pcds: PCDCollection | undefined, forceCheckPCDs?: boolean ): boolean { - const validationErrors = validateAppState( + const validationErrors = validateRunningAppState( tag, self, identity, @@ -38,26 +46,67 @@ export function validateAndLogStateErrors( return true; } -/** - * Determines whether the app's global state contains valid data. If it does not, - * returns the set of things that are incorrect about it in a {@link ErrorReport} - * object. If there were no validation errors the result will contain an empty array - * of errors. - * - * The provided {@link PCDCollection} is not checked unless either this function - * determines the user is logged in or the {@link forceCheckPCDs} argument is `true`. - * - * Depending on where this function is called, pass in a unique {@link tag}, so - * that on the backend we can figure out where the validation failed. - */ -export function validateAppState( +export function validateInitialAppState( + tag: string, + appState: AppState | undefined +): ErrorReport { + return { + errors: getInitialAppStateValidationErrors(appState), + userUUID: appState?.self?.uuid, + tag + }; +} + +export function validateRunningAppState( tag: string, self: User | undefined, identity: Identity | undefined, pcds: PCDCollection | undefined, forceCheckPCDs?: boolean ): ErrorReport { - const validationErrors: string[] = []; + return { + errors: getRunningAppStateValidationErrors( + self, + identity, + pcds, + forceCheckPCDs + ), + userUUID: self?.uuid, + tag + }; +} + +export function getInitialAppStateValidationErrors(state: AppState): string[] { + const errors = [ + ...getRunningAppStateValidationErrors( + state.self, + state.identity, + state.pcds + ) + ]; + + // this case covers a logged in user. the only way the app can get a 'self' + // is by requesting one from the server, to do which one has to be logged in. + if (state.self) { + if (!state.encryptionKey) { + errors.push(`logged in user missing encryption key`); + } + + if (!state.identity) { + errors.push(`logged in user missing identity`); + } + } + + return errors; +} + +export function getRunningAppStateValidationErrors( + self: User | undefined, + identity: Identity | undefined, + pcds: PCDCollection | undefined, + forceCheckPCDs?: boolean +): string[] { + const errors: string[] = []; const loggedOut = !self; const identityPCDFromCollection = pcds?.getPCDsByType( SemaphoreIdentityPCDPackage.name @@ -65,30 +114,24 @@ export function validateAppState( if (forceCheckPCDs || !loggedOut) { if (!pcds) { - validationErrors.push("missing 'pcds'"); + errors.push("missing 'pcds'"); } if (pcds?.size() === 0) { - validationErrors.push("'pcds' contains no pcds"); + errors.push("'pcds' contains no pcds"); } if (!identityPCDFromCollection) { - validationErrors.push( - "'pcds' field in app state does not contain an identity PCD" - ); + errors.push("'pcds' field in app state does not contain an identity PCD"); } } if (loggedOut) { - return { - errors: validationErrors, - userUUID: undefined, - tag - }; + errors; } if (!identity) { - validationErrors.push("missing 'identity'"); + errors.push("missing 'identity'"); } const identityFromPCDCollection = identityPCDFromCollection?.claim?.identity; @@ -98,7 +141,7 @@ export function validateAppState( const commitmentFromIdentityField = identity?.commitment?.toString(); if (commitmentFromSelfField === undefined) { - validationErrors.push(`'self' missing a commitment`); + errors.push(`'self' missing a commitment`); } if ( @@ -113,31 +156,27 @@ export function validateAppState( // identity, and in the pcd collection if (commitmentOfIdentityPCDInCollection !== commitmentFromSelfField) { - validationErrors.push( + errors.push( `commitment of identity pcd in collection (${commitmentOfIdentityPCDInCollection})` + ` does not match commitment in 'self' field of app state (${commitmentFromSelfField})` ); } if (commitmentFromSelfField !== commitmentFromIdentityField) { - validationErrors.push( + errors.push( `commitment in 'self' field of app state (${commitmentFromSelfField})` + ` does not match commitment of 'identity' field of app state (${commitmentFromIdentityField})` ); } if (commitmentFromIdentityField !== commitmentOfIdentityPCDInCollection) { - validationErrors.push( + errors.push( `commitment of 'identity' field of app state (${commitmentFromIdentityField})` + ` does not match commitment of identity pcd in collection (${commitmentOfIdentityPCDInCollection})` ); } } - return { - errors: validationErrors, - userUUID: self?.uuid, - tag - }; + return errors; } /** diff --git a/apps/passport-client/test/stateValidation.spec.ts b/apps/passport-client/test/stateValidation.spec.ts index a8595a0335..f9148597ca 100644 --- a/apps/passport-client/test/stateValidation.spec.ts +++ b/apps/passport-client/test/stateValidation.spec.ts @@ -8,7 +8,7 @@ import { Identity } from "@semaphore-protocol/identity"; import { expect } from "chai"; import { v4 as uuid } from "uuid"; import { randomEmail } from "../src/util"; -import { ErrorReport, validateAppState } from "../src/validateState"; +import { ErrorReport, validateRunningAppState } from "../src/validateState"; function newEdSAPCD(): Promise { return EdDSAPCDPackage.prove({ @@ -51,7 +51,7 @@ describe("validateAppState", async function () { it("logged out ; no errors", async function () { expect( - validateAppState(TAG_STR, undefined, undefined, undefined) + validateRunningAppState(TAG_STR, undefined, undefined, undefined) ).to.deep.eq({ errors: [], userUUID: undefined, @@ -61,7 +61,7 @@ describe("validateAppState", async function () { it("logged out ; forceCheckPCDs=true; test of all error states", async function () { expect( - validateAppState( + validateRunningAppState( TAG_STR, undefined, undefined, @@ -78,7 +78,7 @@ describe("validateAppState", async function () { } satisfies ErrorReport); expect( - validateAppState( + validateRunningAppState( TAG_STR, undefined, undefined, @@ -96,7 +96,7 @@ describe("validateAppState", async function () { } satisfies ErrorReport); expect( - validateAppState(TAG_STR, undefined, undefined, undefined, true) + validateRunningAppState(TAG_STR, undefined, undefined, undefined, true) ).to.deep.eq({ errors: [ "missing 'pcds'", @@ -121,7 +121,7 @@ describe("validateAppState", async function () { identity: identity1 }) ); - expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ + expect(validateRunningAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ userUUID: self.uuid, errors: [], ...TAG @@ -137,7 +137,7 @@ describe("validateAppState", async function () { uuid: uuid() }; const pcds = new PCDCollection(pcdPackages); - expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ + expect(validateRunningAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ userUUID: self.uuid, errors: [ "'pcds' contains no pcds", @@ -155,7 +155,9 @@ describe("validateAppState", async function () { terms_agreed: 1, uuid: uuid() }; - expect(validateAppState(TAG_STR, self, identity1, undefined)).to.deep.eq({ + expect( + validateRunningAppState(TAG_STR, self, identity1, undefined) + ).to.deep.eq({ userUUID: self.uuid, errors: [ "missing 'pcds'", @@ -179,7 +181,7 @@ describe("validateAppState", async function () { identity: identity1 }) ); - expect(validateAppState(TAG_STR, self, undefined, pcds)).to.deep.eq({ + expect(validateRunningAppState(TAG_STR, self, undefined, pcds)).to.deep.eq({ userUUID: self.uuid, errors: ["missing 'identity'"], ...TAG @@ -201,7 +203,7 @@ describe("validateAppState", async function () { }) ); delete self.commitment; - expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ + expect(validateRunningAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ userUUID: self.uuid, errors: ["'self' missing a commitment"], ...TAG @@ -222,7 +224,7 @@ describe("validateAppState", async function () { identity: identity1 }) ); - expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ + expect(validateRunningAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ userUUID: self.uuid, errors: [ `commitment of identity pcd in collection (${commitment1}) does not match commitment in 'self' field of app state (${commitment2})`, @@ -246,7 +248,7 @@ describe("validateAppState", async function () { identity: identity2 }) ); - expect(validateAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ + expect(validateRunningAppState(TAG_STR, self, identity1, pcds)).to.deep.eq({ userUUID: self.uuid, errors: [ `commitment of identity pcd in collection (${commitment2}) does not match commitment in 'self' field of app state (${commitment1})`, @@ -270,7 +272,7 @@ describe("validateAppState", async function () { identity: identity1 }) ); - expect(validateAppState(TAG_STR, self, identity2, pcds)).to.deep.eq({ + expect(validateRunningAppState(TAG_STR, self, identity2, pcds)).to.deep.eq({ userUUID: self.uuid, errors: [ `commitment in 'self' field of app state (${commitment1}) does not match commitment of 'identity' field of app state (${commitment2})`, @@ -294,7 +296,7 @@ describe("validateAppState", async function () { identity: identity2 }) ); - expect(validateAppState(TAG_STR, self, identity3, pcds)).to.deep.eq({ + expect(validateRunningAppState(TAG_STR, self, identity3, pcds)).to.deep.eq({ userUUID: self.uuid, errors: [ `commitment of identity pcd in collection (${commitment2}) does not match commitment in 'self' field of app state (${commitment1})`, From 5fce68d5a59090cfa098c81bafb6482ae5ae0534 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 13:08:14 -0800 Subject: [PATCH 57/71] add comments --- apps/passport-client/src/validateState.ts | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index d67b6fd8c5..507abd66dd 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -9,6 +9,11 @@ import { appConfig } from "./appConfig"; import { loadSelf } from "./localstorage"; import { AppState } from "./state"; +/** + * Returns `true` if {@link validateInitialAppState} returns no errors, and `false` + * otherwise. In the case that errors were encountered, uploads an error report to + * the Zupass backend. + */ export function validateAndLogInitialAppState( tag: string, state: AppState @@ -23,6 +28,11 @@ export function validateAndLogInitialAppState( return true; } +/** + * Returns `true` if {@link validateRunningAppState} returns no errors, and `false` + * otherwise. In the case that errors were encountered, uploads an error report to + * the Zupass backend. + */ export function validateAndLogRunningAppState( tag: string, self: User | undefined, @@ -46,6 +56,10 @@ export function validateAndLogRunningAppState( return true; } +/** + * Validates state of a Zupass application that is just starting up (using the running + * app state checks alongside some extra ones), and returns an {@link ErrorReport}. + */ export function validateInitialAppState( tag: string, appState: AppState | undefined @@ -57,6 +71,9 @@ export function validateInitialAppState( }; } +/** + * Validates state of a running Zupass application and returns an {@link ErrorReport}. + */ export function validateRunningAppState( tag: string, self: User | undefined, @@ -76,6 +93,11 @@ export function validateRunningAppState( }; } +/** + * Validates state of a Zupass application that is just starting up. Uses + * {@link getRunningAppStateValidationErrors}, and performs some extra checks + * on top of it. + */ export function getInitialAppStateValidationErrors(state: AppState): string[] { const errors = [ ...getRunningAppStateValidationErrors( @@ -100,6 +122,10 @@ export function getInitialAppStateValidationErrors(state: AppState): string[] { return errors; } +/** + * Validates state of a running Zupass application. Returns a list of errors + * represented by strings. If the state is not invalid, returns an empty list. + */ export function getRunningAppStateValidationErrors( self: User | undefined, identity: Identity | undefined, From c49362bf174a8d3e57083f489bbef5e7b0f01a66 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 13:17:18 -0800 Subject: [PATCH 58/71] remove encryption key check --- apps/passport-client/src/validateState.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 507abd66dd..258ab0962b 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -57,8 +57,8 @@ export function validateAndLogRunningAppState( } /** - * Validates state of a Zupass application that is just starting up (using the running - * app state checks alongside some extra ones), and returns an {@link ErrorReport}. + * Validates state of a Zupass application that is just starting up (using the + * {@link validateRunningAppState} alongside some extra ones), and returns an {@link ErrorReport}. */ export function validateInitialAppState( tag: string, @@ -110,13 +110,8 @@ export function getInitialAppStateValidationErrors(state: AppState): string[] { // this case covers a logged in user. the only way the app can get a 'self' // is by requesting one from the server, to do which one has to be logged in. if (state.self) { - if (!state.encryptionKey) { - errors.push(`logged in user missing encryption key`); - } - - if (!state.identity) { - errors.push(`logged in user missing identity`); - } + // TODO: any extra checks that need to happen on immediate app startup should + // be put here. } return errors; From cfdc206a009baf04aebdbcbe34d35b3f6aa3db42 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 13:20:16 -0800 Subject: [PATCH 59/71] undo changes to rate limit service --- .../src/services/rateLimitService.ts | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/apps/passport-server/src/services/rateLimitService.ts b/apps/passport-server/src/services/rateLimitService.ts index 588a6bc48b..085a88b724 100644 --- a/apps/passport-server/src/services/rateLimitService.ts +++ b/apps/passport-server/src/services/rateLimitService.ts @@ -1,7 +1,12 @@ import { ONE_HOUR_MS } from "@pcd/util"; -import { clearExpiredActions } from "../database/queries/rateLimit"; +import { + checkRateLimit, + clearExpiredActions +} from "../database/queries/rateLimit"; import { ApplicationContext } from "../types"; +import { logger } from "../util/logger"; import { RollbarService } from "./rollbarService"; +import { traced } from "./telemetryService"; export type RateLimitedActionType = "CHECK_EMAIL_TOKEN" | "REQUEST_EMAIL_TOKEN"; @@ -54,35 +59,33 @@ export class RateLimitService { actionType: RateLimitedActionType, actionId: string ): Promise { - return true; + return traced( + "RateLimitService", + "requestRateLimitedAction", + async (span) => { + span?.setAttribute("actionType", actionType); + span?.setAttribute("actionId", actionId); - // return traced( - // "RateLimitService", - // "requestRateLimitedAction", - // async (span) => { - // span?.setAttribute("actionType", actionType); - // span?.setAttribute("actionId", actionId); + const result = checkRateLimit( + this.context.dbPool, + actionType, + actionId + ); - // const result = checkRateLimit( - // this.context.dbPool, - // actionType, - // actionId - // ); + if (!result) { + logger( + `[RATELIMIT] Action "${actionId}" of type "${actionType}" was rate-limited` + ); + this.rollbarService?.reportError( + new Error( + `Action "${actionId}" of type "${actionType}" was rate-limited` + ) + ); + } - // if (!result) { - // logger( - // `[RATELIMIT] Action "${actionId}" of type "${actionType}" was rate-limited` - // ); - // this.rollbarService?.reportError( - // new Error( - // `Action "${actionId}" of type "${actionType}" was rate-limited` - // ) - // ); - // } - - // return result; - // } - // ); + return result; + } + ); } } From 85d9e63122ec1a506c8c54f4a3d566ba2f52050f Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 13:27:23 -0800 Subject: [PATCH 60/71] fix bug --- apps/passport-client/src/validateState.ts | 2 +- yarn.lock | 27 ++--------------------- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 258ab0962b..45a66bbd63 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -148,7 +148,7 @@ export function getRunningAppStateValidationErrors( } if (loggedOut) { - errors; + return errors; } if (!identity) { diff --git a/yarn.lock b/yarn.lock index 3d4c69caa4..4862a74822 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4703,7 +4703,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^18.0.22", "@types/react@^18.2.6": +"@types/react@*", "@types/react@18.0.22", "@types/react@18.2.20", "@types/react@^18.0.22", "@types/react@^18.2.6": version "18.2.21" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA== @@ -4712,24 +4712,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@18.0.22": - version "18.0.22" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.22.tgz#97782d995d999617de116cf61f437f1351036fc7" - integrity sha512-4yWc5PyCkZN8ke8K9rQHkTXxHIWHxLzzW6RI1kXVoepkD3vULpKzC2sDtAMKn78h92BRYuzf+7b/ms7ajE6hFw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@18.2.20": - version "18.2.20" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.20.tgz#1605557a83df5c8a2cc4eeb743b3dfc0eb6aaeb2" - integrity sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/request@^2.48.8": version "2.48.8" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c" @@ -15488,12 +15470,7 @@ typedoc@^0.25.1: minimatch "^9.0.3" shiki "^0.14.1" -typescript@5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" - integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== - -typescript@^4.5.2, typescript@^4.9.5: +typescript@5.1.6, typescript@^4.5.2, typescript@^4.9.5: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== From 0b1e4fc20d54a2d0fca123a36a0e3647ceba3e50 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 14:39:38 -0800 Subject: [PATCH 61/71] fix merge conflict --- apps/passport-client/src/useSyncE2EEStorage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index a895998899..9afcd2324e 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -133,7 +133,8 @@ export async function uploadStorage( userIdentity, pcds, serializedStorage, - storageHash + storageHash, + knownRevision ); } From 050932310f2528bf66ef835a58cd391d64eb72c2 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 14:45:32 -0800 Subject: [PATCH 62/71] pass in extra validation arguments to tryDeserializeNewStorage --- apps/passport-client/src/dispatch.ts | 1 + apps/passport-client/src/useSyncE2EEStorage.tsx | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 5c2a0284b2..d12c3b428b 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -752,6 +752,7 @@ async function doSync( state.serverStorageRevision, state.serverStorageHash, state.self, + state.identity, state.pcds, state.subscriptions ); diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index 9afcd2324e..33236e3db8 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -261,6 +261,7 @@ export async function downloadAndMergeStorage( knownServerRevision: string | undefined, knownServerHash: string | undefined, appSelf: User, + appIdentity: Identity, appPCDs: PCDCollection, appSubscriptions: FeedSubscriptionManager ): Promise { @@ -290,6 +291,8 @@ export async function downloadAndMergeStorage( // Deserialize downloaded storage, which becomes the default new state if no // merge is necessary. const downloaded = await tryDeserializeNewStorage( + appSelf, + appIdentity, storageResult.value.storage ); if (downloaded === undefined) { @@ -352,7 +355,12 @@ export async function downloadAndMergeStorage( }; } +/** + * {@link appSelf} and {@link appIdentity} are used solely for validation purposes. + */ export async function tryDeserializeNewStorage( + appSelf: User, + appIdentity: Identity, storage: SyncedEncryptedStorage ): Promise< | undefined @@ -371,10 +379,9 @@ export async function tryDeserializeNewStorage( if ( !validateAndLogRunningAppState( "downloadStorage", - undefined, - undefined, - pcds, - true + appSelf, + appIdentity, + pcds ) ) { throw new Error("downloaded e2ee state failed to validate"); From b7c881da763f2a74f8d12b8f091fc423c8939414 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 16:44:32 -0800 Subject: [PATCH 63/71] slight refactor of logValidationErrors --- apps/passport-client/src/validateState.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/passport-client/src/validateState.ts b/apps/passport-client/src/validateState.ts index 45a66bbd63..1a5774c236 100644 --- a/apps/passport-client/src/validateState.ts +++ b/apps/passport-client/src/validateState.ts @@ -205,13 +205,20 @@ export function getRunningAppStateValidationErrors( * we have records and are able to identify common types of errors. Does not leak * sensitive information, such as decrypted versions of e2ee storage. */ -export async function logValidationErrors(errors: ErrorReport): Promise { +export async function logValidationErrors( + errorReport: ErrorReport +): Promise { + if (errorReport?.errors?.length === 0) { + console.log(`not logging empty error report`); + return; + } + try { const user = loadSelf(); - errors.userUUID = errors.userUUID ?? user?.uuid; - console.log(`encountered state validation errors: `, errors); + errorReport.userUUID = errorReport.userUUID ?? user?.uuid; + console.log(`encountered state validation errors: `, errorReport); await requestLogToServer(appConfig.zupassServer, "state-validation-error", { - ...errors + ...errorReport }); } catch (e) { console.log("error reporting errors", e); From c0a21289de551598f34ebe06b91d0b06b02bfe63 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 16:47:01 -0800 Subject: [PATCH 64/71] prevent validation error from being reported by loading pcdcollection from localstorage for logged out users --- apps/passport-client/pages/index.tsx | 2 +- apps/passport-client/src/localstorage.ts | 7 +++++-- apps/passport-client/src/useSyncE2EEStorage.tsx | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/passport-client/pages/index.tsx b/apps/passport-client/pages/index.tsx index 49ff323e80..763f6015ee 100644 --- a/apps/passport-client/pages/index.tsx +++ b/apps/passport-client/pages/index.tsx @@ -404,7 +404,7 @@ async function loadInitialState(): Promise { } const self = loadSelf(); - const pcds = await loadPCDs(); + const pcds = await loadPCDs(self); const encryptionKey = loadEncryptionKey(); const subscriptions = await loadSubscriptions(); const offlineTickets = loadOfflineTickets(); diff --git a/apps/passport-client/src/localstorage.ts b/apps/passport-client/src/localstorage.ts index 0d7ea2a358..323f461d7c 100644 --- a/apps/passport-client/src/localstorage.ts +++ b/apps/passport-client/src/localstorage.ts @@ -31,7 +31,10 @@ export async function savePCDs(pcds: PCDCollection): Promise { window.localStorage[COLLECTION_KEY] = serialized; } -export async function loadPCDs(): Promise { +/** + * {@link self} argument used only to modify validation behavior. + */ +export async function loadPCDs(self?: User): Promise { const oldPCDs = window.localStorage[OLD_PCDS_KEY]; if (oldPCDs) { const collection = new PCDCollection(await getPackages()); @@ -52,7 +55,7 @@ export async function loadPCDs(): Promise { undefined, undefined, collection, - true + self !== undefined ) ) { console.error( diff --git a/apps/passport-client/src/useSyncE2EEStorage.tsx b/apps/passport-client/src/useSyncE2EEStorage.tsx index 33236e3db8..e1fdecfe85 100644 --- a/apps/passport-client/src/useSyncE2EEStorage.tsx +++ b/apps/passport-client/src/useSyncE2EEStorage.tsx @@ -57,7 +57,7 @@ export async function updateBlobKeyForEncryptedStorage( ): Promise { const oldUser = loadSelf(); const newUser = { ...oldUser, salt: newSalt }; - const pcds = await loadPCDs(); + const pcds = await loadPCDs(oldUser); const subscriptions = await loadSubscriptions(); const { serializedStorage, storageHash } = await serializeStorage( From f4999f14d85bc1448f19d563613be7470ae6e4ff Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 17:03:47 -0800 Subject: [PATCH 65/71] early return on upload validation error; this doesn't cause upload loop b/c there is an even earlier early return that exits the sync code in the case that the userInvalid flat is set --- apps/passport-client/src/dispatch.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 996d9f7e13..d8c4176312 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -842,6 +842,10 @@ async function doSync( serverStorageHash: upRes.value.storageHash }; } else { + if (upRes.error.name === "ValidationError") { + return { userInvalid: true }; + } + // Upload failed. Update AppState if necessary, but not unnecessarily. // AppState updates will trigger another upload attempt. const needExtraDownload = upRes.error.name === "Conflict"; @@ -860,9 +864,6 @@ async function doSync( if (needExtraDownload && !state.extraDownloadRequested) { updates.extraDownloadRequested = true; } - if (upRes.error.name === "ValidationError") { - updates.userInvalid = true; - } return updates; } From c7dd380f847c317a81330e3dd81bdf1072823d5c Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 17:18:06 -0800 Subject: [PATCH 66/71] update copy and styling of the invalid user modal to communicate better --- .../components/modals/InvalidUserModal.tsx | 44 ++++++++++++++----- apps/passport-client/pages/index.tsx | 3 +- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/apps/passport-client/components/modals/InvalidUserModal.tsx b/apps/passport-client/components/modals/InvalidUserModal.tsx index e70df2d084..25015ba28f 100644 --- a/apps/passport-client/components/modals/InvalidUserModal.tsx +++ b/apps/passport-client/components/modals/InvalidUserModal.tsx @@ -7,8 +7,15 @@ import { AccountExportButton } from "../shared/AccountExportButton"; export function InvalidUserModal() { const dispatch = useDispatch(); - - const onExitClick = useCallback(() => { + const onLogoutClick = useCallback(() => { + if ( + !confirm( + "Are you sure you want to log out? " + + "We recommend that you export your account before doing so." + ) + ) { + return; + } dispatch({ type: "reset-passport" }); }, [dispatch]); @@ -16,24 +23,37 @@ export function InvalidUserModal() {

Invalid Zupass

-

- You've reset your Zupass account on another device, or your app has - ended up in an invalid state. Click the button below to log out. Then - you'll be able to sync your existing Zupass account onto this device. -

+

Your Zupass is in an invalid state. This can happen because:

+
    +
  • You reset your account on another device.
  • +
  • Zupass application state was corrupted on this device.
  • +
+

To resolve this, we recommend you either:

+
    +
  • Reload this page.
  • +
  • + Export your account data, log out of this account, and log in again. +
  • +
-

- You can export a copy of your local account data using the button below - in case you need it later. -

- +
); } const Container = styled.div` padding: 24px; + p { + margin-bottom: 8px; + } + ul { + list-style: circle; + margin-bottom: 8px; + li { + margin-left: 32px; + } + } `; diff --git a/apps/passport-client/pages/index.tsx b/apps/passport-client/pages/index.tsx index 15cb8259aa..a2f80acd76 100644 --- a/apps/passport-client/pages/index.tsx +++ b/apps/passport-client/pages/index.tsx @@ -443,8 +443,9 @@ async function loadInitialState(): Promise { importScreen: undefined }; - if (!validateAndLogInitialAppState("loadInitialState", state)) { + if (!validateAndLogInitialAppState("loadInitialState", state) || true) { state.userInvalid = true; + state.modal = { modalType: "invalid-participant" }; } return state; From c9cff96819b7b49315e63959807be189bc0d243f Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 17:26:52 -0800 Subject: [PATCH 67/71] add mailto link in invalid user modal --- apps/passport-client/components/core/index.tsx | 5 +++++ .../components/modals/InfoModal.tsx | 8 ++------ .../components/modals/InvalidUserModal.tsx | 16 ++++++++++++---- .../components/screens/ServerErrorScreen.tsx | 6 ++---- .../components/screens/TermsScreen.tsx | 2 +- .../components/shared/PrivacyNotice.tsx | 5 ++--- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/apps/passport-client/components/core/index.tsx b/apps/passport-client/components/core/index.tsx index 7a16fc4eb4..2edf7313db 100644 --- a/apps/passport-client/components/core/index.tsx +++ b/apps/passport-client/components/core/index.tsx @@ -1,4 +1,5 @@ import { Spacer } from "@pcd/passport-ui"; +import { ZUPASS_SUPPORT_EMAIL } from "@pcd/util"; import styled from "styled-components"; import { icons } from "../icons"; import { Button } from "./Button"; @@ -95,3 +96,7 @@ export function ZuLogo() { ); } + +export const SupportLink = () => { + return {ZUPASS_SUPPORT_EMAIL}; +}; diff --git a/apps/passport-client/components/modals/InfoModal.tsx b/apps/passport-client/components/modals/InfoModal.tsx index decc25eb81..dd8533a26a 100644 --- a/apps/passport-client/components/modals/InfoModal.tsx +++ b/apps/passport-client/components/modals/InfoModal.tsx @@ -1,4 +1,4 @@ -import { ZUPASS_GITHUB_REPOSITORY_URL, ZUPASS_SUPPORT_EMAIL } from "@pcd/util"; +import { ZUPASS_GITHUB_REPOSITORY_URL } from "@pcd/util"; import { CenterColumn, Spacer, TextCenter } from "../core"; import { icons } from "../icons"; @@ -20,11 +20,7 @@ export function InfoModal() { - For app support, contact{" "} - - {ZUPASS_SUPPORT_EMAIL} - - . + For app support, contact . diff --git a/apps/passport-client/components/modals/InvalidUserModal.tsx b/apps/passport-client/components/modals/InvalidUserModal.tsx index 25015ba28f..40a1f5a17a 100644 --- a/apps/passport-client/components/modals/InvalidUserModal.tsx +++ b/apps/passport-client/components/modals/InvalidUserModal.tsx @@ -2,9 +2,15 @@ import { Spacer } from "@pcd/passport-ui"; import { useCallback } from "react"; import styled from "styled-components"; import { useDispatch } from "../../src/appHooks"; -import { Button, H2 } from "../core"; +import { Button, H2, SupportLink } from "../core"; import { AccountExportButton } from "../shared/AccountExportButton"; +/** + * A Zupass client can sometimes end up with invalid local state. When that happens, + * we generally set {@link AppState.userInvalid} to true, and display this modal by + * setting {@link AppState.modal} to `{ modalType: "invalid-participant" }`. This modal + * explains what's going on + suggest paths to resolve the problem. + */ export function InvalidUserModal() { const dispatch = useDispatch(); const onLogoutClick = useCallback(() => { @@ -23,10 +29,10 @@ export function InvalidUserModal() {

Invalid Zupass

-

Your Zupass is in an invalid state. This can happen because:

+

Your Zupass is in an invalid state. This can happen when:

  • You reset your account on another device.
  • -
  • Zupass application state was corrupted on this device.
  • +
  • Zupass application state becomes corrupted on this device.

To resolve this, we recommend you either:

    @@ -35,7 +41,9 @@ export function InvalidUserModal() { Export your account data, log out of this account, and log in again.
- +

+ If this issue persists, please contact us at . +

diff --git a/apps/passport-client/components/screens/ServerErrorScreen.tsx b/apps/passport-client/components/screens/ServerErrorScreen.tsx index 11ffcd2af0..fb860bebcf 100644 --- a/apps/passport-client/components/screens/ServerErrorScreen.tsx +++ b/apps/passport-client/components/screens/ServerErrorScreen.tsx @@ -1,6 +1,5 @@ -import { ZUPASS_SUPPORT_EMAIL } from "@pcd/util"; import { useSearchParams } from "react-router-dom"; -import { CenterColumn, H1, Spacer, TextCenter } from "../core"; +import { CenterColumn, H1, Spacer, SupportLink, TextCenter } from "../core"; import { LinkButton } from "../core/Button"; import { AppContainer } from "../shared/AppContainer"; @@ -23,8 +22,7 @@ export function ServerErrorScreen() { {description} {!!description && } - For support, please send a message to{" "} - {ZUPASS_SUPPORT_EMAIL}. + For support, please send a message to . Return to Zupass diff --git a/apps/passport-client/components/screens/TermsScreen.tsx b/apps/passport-client/components/screens/TermsScreen.tsx index c6c22bedb7..14f8bfb703 100644 --- a/apps/passport-client/components/screens/TermsScreen.tsx +++ b/apps/passport-client/components/screens/TermsScreen.tsx @@ -1,5 +1,5 @@ import styled, { createGlobalStyle } from "styled-components"; -import { PrivacyNoticeText } from "../shared/PrivacyNotice"; +import { PrivacyNoticeText } from "../shared/PrivacyNoticeText"; export function TermsScreen() { return ( diff --git a/apps/passport-client/components/shared/PrivacyNotice.tsx b/apps/passport-client/components/shared/PrivacyNotice.tsx index 62f373d975..d4c9dcfa84 100644 --- a/apps/passport-client/components/shared/PrivacyNotice.tsx +++ b/apps/passport-client/components/shared/PrivacyNotice.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from "react"; import styled from "styled-components"; -import { Button, Spacer } from "../core"; +import { Button, Spacer, SupportLink } from "../core"; export function PrivacyNoticeText() { return ( @@ -244,8 +244,7 @@ export function PrivacyNoticeText() {

8. HOW TO CONTACT US

Should you have any questions about our privacy practices or this - Privacy Notice, please email us at{" "} - support@zupass.org. + Privacy Notice, please email us at .

); From c4a698236be05a2a8d60b433d31dc55d7992814b Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 17:32:41 -0800 Subject: [PATCH 68/71] change the way we set userInvalid to also force the modal to open --- apps/passport-client/src/dispatch.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index d8c4176312..5014b70e03 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -396,9 +396,8 @@ async function finishAccountCreation( !uploadResult.success && uploadResult.error.name === "ValidationError" ) { - update({ - userInvalid: true - }); + userInvalid(update); + return; } // Save user to local storage. This is done last because it unblocks @@ -555,7 +554,7 @@ async function loadAfterLogin( pcds ) ) { - update({ userInvalid: true }); + userInvalid(update); return; } @@ -843,7 +842,8 @@ async function doSync( }; } else { if (upRes.error.name === "ValidationError") { - return { userInvalid: true }; + userInvalid(update); + return; } // Upload failed. Update AppState if necessary, but not unnecessarily. From e7a843bf21fcdf03fdf82483b4a6dbebda70594a Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 17:34:16 -0800 Subject: [PATCH 69/71] don't force userInvalid true --- apps/passport-client/pages/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/passport-client/pages/index.tsx b/apps/passport-client/pages/index.tsx index a2f80acd76..f46115e573 100644 --- a/apps/passport-client/pages/index.tsx +++ b/apps/passport-client/pages/index.tsx @@ -443,7 +443,7 @@ async function loadInitialState(): Promise { importScreen: undefined }; - if (!validateAndLogInitialAppState("loadInitialState", state) || true) { + if (!validateAndLogInitialAppState("loadInitialState", state)) { state.userInvalid = true; state.modal = { modalType: "invalid-participant" }; } From e67b1d1d54fd298b5df11ab67cde06111f84fe95 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 17:35:39 -0800 Subject: [PATCH 70/71] add comment explaining early return --- apps/passport-client/src/dispatch.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/passport-client/src/dispatch.ts b/apps/passport-client/src/dispatch.ts index 5014b70e03..e29ebb4fa1 100644 --- a/apps/passport-client/src/dispatch.ts +++ b/apps/passport-client/src/dispatch.ts @@ -842,6 +842,9 @@ async function doSync( }; } else { if (upRes.error.name === "ValidationError") { + // early return on upload validation error; this doesn't cause upload + // loop b/c there is an even earlier early return that exits the sync + // code in the case that the userInvalid flag is set userInvalid(update); return; } From 6adbf57008990e63d388aa6a0868ad3e712d1445 Mon Sep 17 00:00:00 2001 From: Ivan Chub Date: Mon, 18 Dec 2023 17:38:35 -0800 Subject: [PATCH 71/71] fixed build --- apps/passport-client/components/modals/InfoModal.tsx | 2 +- apps/passport-client/components/screens/TermsScreen.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/passport-client/components/modals/InfoModal.tsx b/apps/passport-client/components/modals/InfoModal.tsx index dd8533a26a..1a02cd298b 100644 --- a/apps/passport-client/components/modals/InfoModal.tsx +++ b/apps/passport-client/components/modals/InfoModal.tsx @@ -1,5 +1,5 @@ import { ZUPASS_GITHUB_REPOSITORY_URL } from "@pcd/util"; -import { CenterColumn, Spacer, TextCenter } from "../core"; +import { CenterColumn, Spacer, SupportLink, TextCenter } from "../core"; import { icons } from "../icons"; export function InfoModal() { diff --git a/apps/passport-client/components/screens/TermsScreen.tsx b/apps/passport-client/components/screens/TermsScreen.tsx index 14f8bfb703..c6c22bedb7 100644 --- a/apps/passport-client/components/screens/TermsScreen.tsx +++ b/apps/passport-client/components/screens/TermsScreen.tsx @@ -1,5 +1,5 @@ import styled, { createGlobalStyle } from "styled-components"; -import { PrivacyNoticeText } from "../shared/PrivacyNoticeText"; +import { PrivacyNoticeText } from "../shared/PrivacyNotice"; export function TermsScreen() { return (