import { describe, expect, it } from "@effect/vitest"; import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/unstable/process"; import { beforeEach, vi } from "vite-plus/test"; import { ChromiumKeyError, decodeWindowsWrappedKey, readLinuxSecret, resolveChromiumKeys, unwrapWindowsDpapiKey, } from "./ChromiumKeys.ts"; import { LinuxBrowserSecretPath } from "./LinuxBrowserSecret.ts"; const { getPassword } = vi.hoisted(() => ({ getPassword: vi.fn<() => string | null>() })); vi.mock("@napi-rs/keyring", () => ({ Entry: class { getPassword = getPassword; }, })); beforeEach(() => { getPassword.mockReset(); }); type CapturedCommand = { readonly command: string; readonly args: ReadonlyArray; readonly options: { readonly stdin?: string; readonly env?: Readonly>; }; }; const helperLayer = (input: { readonly stdout?: string; readonly stderr?: string; readonly stdoutStream?: Stream.Stream; readonly stderrStream?: Stream.Stream; readonly exitCode?: number; readonly spawnError?: PlatformError.PlatformError; readonly capture?: (command: CapturedCommand) => void; }) => Layer.merge( Layer.succeed(LinuxBrowserSecretPath, "/bundled/browser-secret/t3-browser-secret"), Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => input.spawnError ? Effect.fail(input.spawnError) : Effect.succeed( ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(1), exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(input.exitCode ?? 0)), isRunning: Effect.succeed(false), kill: () => Effect.void, unref: Effect.succeed(Effect.void), stdin: Sink.drain, stdout: input.stdoutStream ?? Stream.encodeText(Stream.make(input.stdout ?? "")), stderr: input.stderrStream ?? Stream.encodeText(Stream.make(input.stderr ?? "")), all: Stream.empty, getInputFd: () => Sink.drain, getOutputFd: () => Stream.empty, }), ).pipe( Effect.tap(() => Effect.sync(() => input.capture?.(command as CapturedCommand))), ), ), ), ); describe("macOS Chromium secrets", () => { const request = { platform: "darwin", keychainService: "Chrome Safe Storage", keychainAccount: "Chrome", linuxSecretApplication: undefined, } as const; const noProcesses = Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("must not spawn")), ); it.effect("derives the cookie key from the keychain secret", () => Effect.gen(function* () { getPassword.mockReturnValue("macos-secret"); const keys = yield* resolveChromiumKeys(request); expect(keys.cbcV10?.toString("hex")).toBe("3df7306fb1eac353289565a2f6b64f74"); }).pipe(Effect.provide(noProcesses)), ); it.effect("reports a missing keychain entry", () => Effect.gen(function* () { getPassword.mockReturnValue(null); const error = yield* resolveChromiumKeys(request).pipe(Effect.flip); expect(error.reason).toBe("keychainItemMissing"); }).pipe(Effect.provide(noProcesses)), ); it.effect("preserves a denied keychain approval", () => Effect.gen(function* () { const denied = new Error("User denied access"); getPassword.mockImplementation(() => { throw denied; }); const error = yield* resolveChromiumKeys(request).pipe(Effect.flip); expect(error.reason).toBe("needsKeychainApproval"); expect(error.cause).toBe(denied); }).pipe(Effect.provide(noProcesses)), ); }); describe("Linux Chromium secrets", () => { it.effect("retains a missing helper failure alongside the keyring-free fallback", () => Effect.gen(function* () { const keys = yield* resolveChromiumKeys({ platform: "linux", keychainService: undefined, keychainAccount: undefined, linuxSecretApplication: "chromium", }); expect(keys.cbcV10).toHaveLength(16); expect(keys.cbcV11).toBeUndefined(); expect(keys.cbcV11Error?.reason).toBe("keychainUnavailable"); }).pipe( Effect.provide( helperLayer({ spawnError: PlatformError.systemError({ _tag: "NotFound", module: "ChildProcess", method: "spawn", }), }), ), ), ); it.effect("reports an unconfigured helper without searching PATH", () => readLinuxSecret("chromium").pipe( Effect.flip, Effect.tap((error) => Effect.sync(() => expect(error.reason).toBe("keychainUnavailable"))), Effect.provideService( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("must not spawn")), ), Effect.provideService(LinuxBrowserSecretPath, undefined), ), ); it.effect("looks up the browser's libsecret application attribute", () => { let captured: CapturedCommand | undefined; return Effect.gen(function* () { const keys = yield* resolveChromiumKeys({ platform: "linux", keychainService: "ignored macOS service", keychainAccount: "ignored macOS account", linuxSecretApplication: "msedge", }); expect(captured?.command).toBe("/bundled/browser-secret/t3-browser-secret"); expect(captured?.args).toEqual(["msedge"]); expect(captured?.options.stdin).toBe("ignore"); expect(keys.cbcV10).toHaveLength(16); expect(keys.cbcV11).toHaveLength(16); }).pipe( Effect.provide( helperLayer({ stdout: "linux-secret", capture: (value) => (captured = value) }), ), ); }); it.effect("reports an unavailable Secret Service backend as a read failure", () => Effect.gen(function* () { const error = yield* readLinuxSecret("chrome").pipe(Effect.flip); expect(error).toBeInstanceOf(ChromiumKeyError); expect(error.reason).toBe("keychainUnavailable"); }).pipe( Effect.provide( helperLayer({ stderr: "Cannot autolaunch D-Bus without X11 $DISPLAY", exitCode: 1 }), ), ), ); it.effect("preserves trailing whitespace in the stored secret", () => Effect.gen(function* () { const secret = yield* readLinuxSecret("chrome"); expect(secret).toBe("linux-secret \t\n"); }).pipe(Effect.provide(helperLayer({ stdout: "linux-secret \t\n" }))), ); it.effect("drains stdout and stderr concurrently", () => Effect.gen(function* () { const stderrDrainStarted = yield* Deferred.make(); const stdout = Stream.fromEffect(Deferred.await(stderrDrainStarted)).pipe( Stream.flatMap(() => Stream.encodeText(Stream.make("linux-secret"))), ); const stderr = Stream.fromEffect(Deferred.succeed(stderrDrainStarted, undefined)).pipe( Stream.drain, ); const secret = yield* readLinuxSecret("chrome").pipe( Effect.provide(helperLayer({ stdoutStream: stdout, stderrStream: stderr })), ); expect(secret).toBe("linux-secret"); }), ); it.effect( "preserves the desktop environment and identifies denial without parsing stderr", () => { let captured: CapturedCommand | undefined; return Effect.gen(function* () { const error = yield* readLinuxSecret("brave").pipe(Effect.flip); expect(error).toBeInstanceOf(ChromiumKeyError); expect(error.reason).toBe("needsKeychainApproval"); expect(captured?.options.env?.LC_ALL).toBe("localized"); expect(captured?.options.env?.PATH).toBe("/synthetic/bin"); expect(captured?.options.env?.SESSION_MARKER).toBe("kept"); }).pipe( Effect.provide( helperLayer({ stderr: "Zugriff verweigert", exitCode: 3, capture: (value) => (captured = value), }), ), Effect.provideService(HostProcessEnvironment, { PATH: "/synthetic/bin", SESSION_MARKER: "kept", LC_ALL: "localized", }), ); }, ); it.effect("does not discard a denied unlock prompt while resolving keys", () => Effect.gen(function* () { const error = yield* resolveChromiumKeys({ platform: "linux", keychainService: undefined, keychainAccount: undefined, linuxSecretApplication: "brave", }).pipe(Effect.flip); expect(error.reason).toBe("needsKeychainApproval"); }).pipe(Effect.provide(helperLayer({ stderr: "Keyring is locked", exitCode: 3 }))), ); it.effect("keeps the v10 fallback when the Secret Service backend is unavailable", () => Effect.gen(function* () { const keys = yield* resolveChromiumKeys({ platform: "linux", keychainService: undefined, keychainAccount: undefined, linuxSecretApplication: "chrome", }); expect(keys.cbcV10).toHaveLength(16); expect(keys.cbcV11).toBeUndefined(); }).pipe( Effect.provide( helperLayer({ stderr: "Cannot autolaunch D-Bus without X11 $DISPLAY", exitCode: 1, }), ), ), ); it.effect("keeps the v10 fallback when no matching v11 secret exists", () => Effect.gen(function* () { const keys = yield* resolveChromiumKeys({ platform: "linux", keychainService: undefined, keychainAccount: undefined, linuxSecretApplication: "vivaldi", }); expect(keys.cbcV10).toHaveLength(16); expect(keys.cbcV11).toBeUndefined(); }).pipe(Effect.provide(helperLayer({ exitCode: 2 }))), ); }); describe("Windows Chromium secrets", () => { it.effect("accepts only DPAPI-wrapped non-app-bound keys", () => Effect.gen(function* () { const wrapped = Buffer.from("wrapped-key"); const encoded = Buffer.concat([Buffer.from("DPAPI"), wrapped]).toString("base64"); const localState = `{"os_crypt":{"encrypted_key":"${encoded}"}}`; expect(yield* decodeWindowsWrappedKey(localState)).toEqual(wrapped); const appBound = yield* decodeWindowsWrappedKey( `{"os_crypt":{"encrypted_key":"${encoded}","app_bound_encrypted_key":"present"}}`, ).pipe(Effect.flip); expect(appBound.reason).toBe("unsupportedPlatform"); const malformed = yield* decodeWindowsWrappedKey( `{"os_crypt":{"encrypted_key":"${wrapped.toString("base64")}"}}`, ).pipe(Effect.flip); expect(malformed.reason).toBe("readFailed"); }), ); it.effect("unwraps the binary key through PowerShell without placing it in argv", () => { let captured: CapturedCommand | undefined; const wrapped = Buffer.from("wrapped-key"); const key = Buffer.from("0123456789abcdef0123456789abcdef"); return Effect.gen(function* () { expect(yield* unwrapWindowsDpapiKey(wrapped)).toEqual(key); expect(captured?.command).toBe( "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", ); expect(captured?.args).toContain("-NonInteractive"); expect(captured?.args.join(" ")).not.toContain(wrapped.toString("base64")); }).pipe( Effect.provide( helperLayer({ stdout: key.toString("base64"), capture: (value) => (captured = value) }), ), Effect.provideService(HostProcessEnvironment, { SystemRoot: "C:\\Windows" }), ); }); });