Daeseon Yoo
Back to project
·Tech retro·3 min·Review needed

v0.2 Layer 1 — SecretMasker: 10-pattern regex redact before LLM + audit log

First layer of v0.2's 5-layer security architecture: a 286-LOC SecretMasker that redacts API keys, credit cards, and Korean RRNs at the AnalyzeCoordinator boundary — before the instruction reaches the external LLM or hits the audit log on disk.

AI version

Backfilled entry — original commit shipped with [no-log] and no log file was written at the time.

What was done

Added SecretMasker.swift (91 LOC) plus 168 LOC of tests and wired it into AnalyzeCoordinator.run() so that every AnalyzeRequest.instruction flowing through the system is regex-redacted before two specific things happen:

  1. The instruction is handed to the dispatcher (which calls the external Vision LLM).
  2. The instruction is persisted into SessionAuditEntry on disk.

Ten patterns are applied, in this specific order (specific prefixes before generic ones to avoid false positives):

  • sk-ant-...[REDACTED:anthropic-key]
  • sk-proj-...[REDACTED:openai-key]
  • sk-...[REDACTED:openai-key]
  • AIza...[REDACTED:google-key]
  • AKIA...[REDACTED:aws-access-key]
  • github_pat_...[REDACTED:github-token]
  • ghp_...[REDACTED:github-token]
  • xox[abprs]-...[REDACTED:slack-token]
  • -----BEGIN ... PRIVATE KEY-----[REDACTED:private-key]
  • Korean RRN (901101-1234567 shape) → [REDACTED:rrn]
  • Credit card (4-4-4-4, dash or space separated) → [REDACTED:card]

Email is deliberately not masked — the product use case "where do I type my email?" requires the instruction to keep the email visible.

Two entry points are exposed:

  • SecretMasker.mask(_:) — applies all patterns and returns the redacted string.
  • SecretMasker.detect(_:) — returns [(name, matched)] without redacting, for an eventual user-facing alert ("we noticed a secret in your instruction").

Why it was needed

From the commit message: this is the first concrete commit against the product-vision-global-multi-platform memory's 5-layer security plan. The product sends the user's screen and instruction to an external Vision LLM, and the instruction is also persisted to disk as an audit entry. Both surfaces are data boundaries where a leaked API key or card number would survive past the live session — the LLM provider's logs on one side, the local disk on the other.

The masker collapses both boundaries into a single chokepoint at the top of AnalyzeCoordinator.run() so there is exactly one place to reason about "did this secret leave the process boundary?"

Files / functions changed

  • Sources/ScreenBridge/SecretMasker.swift (NEW, 91 LOC) — SecretMasker enum with Pattern struct, patterns static list, mask(_:), detect(_:).
  • Sources/ScreenBridge/AnalyzeCoordinator.swift (+25 LOC at top of run()):
    • Computes maskedInstruction = SecretMasker.mask(req.instruction) once per call.
    • If masked differs from original, logs a [secret] instruction masked — N pattern(s): ... notice via Log.dispatcher (pattern names only, not the matched secret).
    • Constructs a new maskedReq: AnalyzeRequest and passes it to dispatcher.analyze(...) instead of the original.
    • Writes the masked instruction into both currentInstruction (so continuation calls don't resurface the secret) and SessionAuditEntry.instruction (so the disk record is already redacted).
  • Tests/ScreenBridgeTests/SecretMaskerTests.swift (NEW, 168 LOC, 17 tests) — 11 mask cases (one per pattern plus safe-text and email-pass-through), 3 detect cases including multiple secrets in one string, plus credit-card dash/space separator coverage.
  • DECISIONS.md (+6 lines) — R9 entries: "Secret mask at AnalyzeCoordinator.run boundary" and "Email mask deferred (instruction visibility)".

Result / verification

  • swift build — 3.87s.
  • swift test — 133/133 passing (+17 new from SecretMaskerTests).
  • The [secret] instruction masked log notice uses privacy: .public only for the pattern name and count — never for the matched substring — so even the persistent unified log on disk doesn't carry the raw secret.

Open / what didn't get done

  • Detect-only path (detect(_:)) exists but is not yet wired into a user-facing alert. Commit message marks this as "v0.3+ TODO".
  • Image masking is explicitly out of scope: the Vision LLM runs OCR on the captured PNG, and redacting pixels of an in-flight screenshot is a much harder problem. The masker only covers the text instruction, not the image. v0.3+ may revisit.
  • Layers 2–5 of the v0.2 plan remain to be built: sensitive-app exclusion by bundleID (1Password, KakaoBank), sensitive-region opt-out, local LLM swap (Apple Foundation Model), and audit-log 7-day rotation.
  • Email pattern is intentionally absent — kept as a decision (R9), not an oversight.

Commit — 2ffc163

Review needed

No human review on this entry yet.