# Files changed · PR \#13656 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/13656/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(desktop\): recover undecryptable connection catalogs safely](https://git.cafe/pingdotgg/t3code/pull/13656.md)

Version: 1

Head commit: 14640edea7d4aaf218e02b2f7a9028428528f754

Base commit: e5a46d6c5d00b89afba5274a94d42428c8d79763

Comparison base: e5a46d6c5d00b89afba5274a94d42428c8d79763

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts

Status: modified; +65; −18

```diff
@@ -405,7 +405,7 @@
     ),
   );
 
-  it.effect("surfaces a catalog that can no longer be decrypted without deleting it", () =>
+  it.effect("preserves an undecryptable catalog and allows a fresh encrypted catalog", () =>
     Effect.gen(function* () {
       const path = yield* Path.Path;
       const fileSystem = yield* FileSystem.FileSystem;
@@ -413,31 +413,78 @@
         prefix: "t3-desktop-connection-catalog-test-",
       });
       const failDecrypt = yield* Ref.make(false);
-      const layer = makeLayer(baseDir, true, failDecrypt);
       const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
+        Effect.provide(makeLayer(baseDir, true, failDecrypt)),
+      );
+      const stateDir = path.join(baseDir, "userdata");
+      const catalogPath = path.join(stateDir, "connection-catalog.json");
+      const catalog = '{"schemaVersion":1,"targets":[]}';
+      assert.isTrue(yield* store.set(catalog));
+      const original = yield* fileSystem.readFileString(catalogPath);
+      yield* Ref.set(failDecrypt, true);
+
+      const results = yield* Effect.all([store.get, store.get], { concurrency: "unbounded" });
+      assert.deepStrictEqual(results, [Option.none(), Option.none()]);
-        Effect.provide(layer),
+      const backups = (yield* fileSystem.readDirectory(stateDir)).filter((name) =>
+        name.endsWith(".undecryptable"),
       );
+      assert.equal(backups.length, 1);
+      const backupPath = path.join(stateDir, backups[0]!);
+      assert.equal(yield* fileSystem.readFileString(backupPath), original);
+      assert.isFalse(yield* fileSystem.exists(catalogPath));
 
+      yield* Ref.set(failDecrypt, false);
+      assert.isTrue(yield* store.set('{"schemaVersion":1,"targets":[],"profiles":[]}'));
+      assert.deepStrictEqual(
+        yield* store.get,
+        Option.some('{"schemaVersion":1,"targets":[],"profiles":[]}'),
+      );
+      assert.equal(yield* fileSystem.readFileString(backupPath), original);
+    }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
+  );
+
+  it.effect("does not discard an undecryptable catalog when preserving it fails", () =>
+    Effect.gen(function* () {
+      const fileSystem = yield* FileSystem.FileSystem;
+      const path = yield* Path.Path;
+      const baseDir = yield* fileSystem.makeTempDirectoryScoped({
+        prefix: "t3-desktop-connection-catalog-test-",
+      });
+      const failDecrypt = yield* Ref.make(false);
+      const normalStore = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
+        Effect.provide(makeLayer(baseDir, true, failDecrypt)),
+      );
+      yield* normalStore.set("{}");
+      const catalogPath = path.join(baseDir, "userdata", "connection-catalog.json");
+      const original = yield* fileSystem.readFileString(catalogPath);
+      const permissionError = PlatformError.systemError({
+        _tag: "PermissionDenied",
+        module: "FileSystem",
+        method: "rename",
+        pathOrDescriptor: catalogPath,
+      });
+      const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
+        Effect.provide(
+          makeLayer(
+            baseDir,
+            true,
+            failDecrypt,
+            Layer.succeed(FileSystem.FileSystem, {
+              ...fileSystem,
+              rename: () => Effect.fail(permissionError),
+            }),
+          ),
+        ),
-      assert.isTrue(yield* store.set('{"schemaVersion":1,"targets":[]}'));
+      );
       yield* Ref.set(failDecrypt, true);
       const error = yield* store.get.pipe(Effect.flip);
       assert.instanceOf(
         error,
-        DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreProtectionError,
-      );
-      assert.equal(error.operation, "decrypt-catalog");
-      assert.equal(error.catalogPath, path.join(baseDir, "userdata", "connection-catalog.json"));
-      assert.instanceOf(error.cause, ElectronSafeStorage.ElectronSafeStorageDecryptError);
-      const decryptError = error.cause as ElectronSafeStorage.ElectronSafeStorageDecryptError;
-      assert.instanceOf(decryptError.cause, Error);
-      assert.equal(decryptError.cause.message, "invalid encrypted catalog");
-      assert.equal(
+        DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreWriteError,
-        error.message,
-        `Desktop connection catalog protection failed during decrypt-catalog at ${path.join(baseDir, "userdata", "connection-catalog.json")}.`,
       );
-      assert.notEqual(error.message, decryptError.message);
+      assert.equal(error.operation, "preserve-undecryptable-catalog");
+      assert.strictEqual(error.cause, permissionError);
+      assert.equal(yield* fileSystem.readFileString(catalogPath), original);
-      yield* Ref.set(failDecrypt, false);
-      assert.deepStrictEqual(yield* store.get, Option.some('{"schemaVersion":1,"targets":[]}'));
     }).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
   );
 });
```

## apps/desktop/src/app/DesktopConnectionCatalogStore.ts

Status: modified; +40; −3

```diff
@@ -21,6 +21,7 @@
 import * as Option from "effect/Option";
 import * as Path from "effect/Path";
 import * as Schema from "effect/Schema";
+import * as Semaphore from "effect/Semaphore";
 
 import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts";
 import * as DesktopSavedEnvironments from "../settings/DesktopSavedEnvironments.ts";
@@ -52,6 +53,7 @@
   "create-directory",
   "write-temporary-file",
   "replace-catalog-file",
+  "preserve-undecryptable-catalog",
 ]);
 
 const DesktopConnectionCatalogStoreMigrationOperation = Schema.Literals([
@@ -151,6 +153,7 @@
   {
     readonly get: Effect.Effect<
       Option.Option<string>,
+      | DesktopConnectionCatalogStoreWriteError
       | DesktopConnectionCatalogStoreReadError
       | DesktopConnectionCatalogStoreDocumentDecodeError
       | DesktopConnectionCatalogStoreDecodeError
@@ -385,6 +388,7 @@
   const crypto = yield* Crypto.Crypto;
   const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments;
   const catalogPath = path.join(environment.stateDir, "connection-catalog.json");
+  const mutex = yield* Semaphore.make(1);
   const encryptionAvailable = safeStorage.isEncryptionAvailable.pipe(
     Effect.mapError(
       (cause) =>
@@ -495,14 +499,46 @@
         ),
       );
       return Option.some(decrypted);
+    }).pipe(
+      Effect.catchTag(
+        "DesktopConnectionCatalogStoreProtectionError",
+        (
+          error,
+        ): Effect.Effect<
+          Option.Option<string>,
+          DesktopConnectionCatalogStoreProtectionError | DesktopConnectionCatalogStoreWriteError
+        > =>
+          error.operation !== "decrypt-catalog"
+            ? Effect.fail(error)
+            : Effect.gen(function* () {
+                const backupPath = `${catalogPath}.${yield* crypto.randomUUIDv4}.undecryptable`;
+                yield* fileSystem.rename(catalogPath, backupPath);
+                yield* Effect.logWarning(
+                  "Saved connections could not be decrypted. Preserved the catalog; reconnect saved remote environments.",
+                  { catalogPath, backupPath },
+                );
+                return Option.none<string>();
+              }).pipe(
+                Effect.mapError(
+                  (cause) =>
+                    new DesktopConnectionCatalogStoreWriteError({
+                      operation: "preserve-undecryptable-catalog",
+                      path: catalogPath,
+                      cause,
+                    }),
+                ),
+              ),
+      ),
+      mutex.withPermits(1),
+      Effect.withSpan("desktop.connectionCatalogStore.get"),
-    }).pipe(Effect.withSpan("desktop.connectionCatalogStore.get")),
-    set: Effect.fn("desktop.connectionCatalogStore.set")(function* (catalog) {
+    ),
+    set: Effect.fn("desktop.connectionCatalogStore.set")(function* (catalog: string) {
       if (!(yield* encryptionAvailable)) {
         return false;
       }
       yield* writeCatalog(catalog);
       return true;
+    }, mutex.withPermits(1)),
-    }),
     clear: fileSystem.remove(catalogPath, { force: true }).pipe(
       Effect.catch((error) =>
         Effect.logWarning("Could not clear the desktop connection catalog.", {
@@ -510,6 +546,7 @@
           error,
         }),
       ),
+      mutex.withPermits(1),
       Effect.withSpan("desktop.connectionCatalogStore.clear"),
     ),
   });
```
