fix(desktop): recover connection catalogs after safeStorage key loss#23476

open7
main
fix/issue-8341
opened3d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 3d ago
0 / 4 viewed4 files changedLine totals unavailable

apps/desktop/src/app/DesktopConnectionCatalogStore.ts · Earlier revision 248ab74f · right 463

Original diff
@@ -430,6 +434,41 @@ export const make = Effect.gen(function* () {
     });
   });
 
+  // Electron regenerates its key when the OS keyring entry is lost, orphaning the catalog for
+  // good. Read it as empty so the user can reconnect, but keep a copy first: the catalog stays
+  // in place until the next save replaces it, and the content-derived name keeps repeated and
+  // concurrent reads to one backup.
+  const preserveUndecryptableCatalog = Effect.fn(
+    "desktop.connectionCatalogStore.preserveUndecryptableCatalog",
+  )(function* (
+    document: EncryptedConnectionCatalogDocument,
+    encryptedCatalog: Uint8Array,
+    decryptError: ElectronSafeStorage.ElectronSafeStorageDecryptError,
+  ) {
+    const digest = NodeCrypto.createHash("sha256").update(encryptedCatalog).digest("hex");
+    const backupPath = `${catalogPath}.${digest}.undecryptable`;
+    const preserveError = (cause: unknown) =>
+      new DesktopConnectionCatalogStoreWriteError({
+        operation: "preserve-undecryptable-catalog",
+        path: backupPath,
+        cause,
+      });
+    if (!(yield* fileSystem.exists(backupPath).pipe(Effect.mapError(preserveError)))) {
+      const suffix = (yield* crypto.randomUUIDv4().pipe(Effect.mapError(preserveError))).replace(
+        /-/g,
+        "",
+      );
+      // Back up the document that failed to decrypt, not the file at `catalogPath`: a
+      // concurrent `set` may already have replaced it.
+      yield* writeDocument({ fileSystem, path, catalogPath: backupPath, document, suffix });

apps/desktop/src/app/DesktopConnectionCatalogStore.ts · Earlier revision 248ab74f · right 464–467

Original diff
@@ -430,6 +434,41 @@ export const make = Effect.gen(function* () {
     });
   });
 
+  // Electron regenerates its key when the OS keyring entry is lost, orphaning the catalog for
+  // good. Read it as empty so the user can reconnect, but keep a copy first: the catalog stays
+  // in place until the next save replaces it, and the content-derived name keeps repeated and
+  // concurrent reads to one backup.
+  const preserveUndecryptableCatalog = Effect.fn(
+    "desktop.connectionCatalogStore.preserveUndecryptableCatalog",
+  )(function* (
+    document: EncryptedConnectionCatalogDocument,
+    encryptedCatalog: Uint8Array,
+    decryptError: ElectronSafeStorage.ElectronSafeStorageDecryptError,
+  ) {
+    const digest = NodeCrypto.createHash("sha256").update(encryptedCatalog).digest("hex");
+    const backupPath = `${catalogPath}.${digest}.undecryptable`;
+    const preserveError = (cause: unknown) =>
+      new DesktopConnectionCatalogStoreWriteError({
+        operation: "preserve-undecryptable-catalog",
+        path: backupPath,
+        cause,
+      });
+    if (!(yield* fileSystem.exists(backupPath).pipe(Effect.mapError(preserveError)))) {
+      const suffix = (yield* crypto.randomUUIDv4().pipe(Effect.mapError(preserveError))).replace(
+        /-/g,
+        "",
+      );
+      // Back up the document that failed to decrypt, not the file at `catalogPath`: a
+      // concurrent `set` may already have replaced it.
+      yield* writeDocument({ fileSystem, path, catalogPath: backupPath, document, suffix });
+      yield* Effect.logWarning(
+        "The desktop connection catalog can no longer be decrypted; starting a new one.",
+        { catalogPath, backupPath, error: decryptError },
+      );

apps/desktop/src/app/DesktopConnectionCatalogStore.ts · right 464–467