fix(desktop): recover connection catalogs after safeStorage key loss#23476
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