Skip to content

Commit b8a6683

Browse files
chrisleekrclaude
andauthored
fix(sanitizer): detect AGE-SECRET-KEY-1 vault identity in secret scanner (#144)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e805b8a commit b8a6683

2 files changed

Lines changed: 25 additions & 0 deletions

File tree

‎src/core/__tests__/sanitizer.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ describe("sanitizer", () => {
162162
["slack-token-app", `xoxa-${"abc123".repeat(2)}`],
163163
["slack-token-refresh", `xoxr-${"abc123".repeat(2)}`],
164164
["slack-token-session", `xoxs-${"abc123".repeat(2)}`],
165+
["age-secret-key", `AGE-SECRET-KEY-1${"A".repeat(58)}`],
165166
])("scanForSecrets detects %s embedded in prose", (expectedName, sampleSecret) => {
166167
const body = `Note from setup: my key is ${sampleSecret}. Do not share.`;
167168
const warnings = scanForSecrets(body, "/tmp/leaky.md");
@@ -192,6 +193,18 @@ describe("sanitizer", () => {
192193
expect(warnings.some((w) => w.includes("anthropic-api-key"))).toBe(true);
193194
});
194195

196+
test("redactSecretLiterals rewrites an age secret key pasted as a whole JSON value", () => {
197+
// An MCP env block like { AGENTSYNC_KEY: "AGE-SECRET-KEY-1..." } must be
198+
// redacted, not just aborted, so the rest of the structured config survives.
199+
const ageKey = `AGE-SECRET-KEY-1${"A".repeat(58)}`;
200+
const input = { env: { AGENTSYNC_KEY: ageKey } };
201+
const result = redactSecretLiterals(input);
202+
const value = result.value as { env: { AGENTSYNC_KEY: string } };
203+
expect(value.env.AGENTSYNC_KEY).not.toBe(ageKey);
204+
expect(value.env.AGENTSYNC_KEY.startsWith("$AGENTSYNC_REDACTED")).toBeTrue();
205+
expect(result.warnings.length).toBeGreaterThan(0);
206+
});
207+
195208
test("scanForSecrets does NOT false-positive on long alphanumeric runs in prose", () => {
196209
const prose =
197210
"Commit 0123456789abcdef0123456789abcdef0123456789abcdef contains a fix. " +

‎src/core/sanitizer.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ const WHOLE_VALUE_SECRET_PATTERNS = [
6666
/^sk-[a-zA-Z0-9]{20,}$/,
6767
/^ghp_[a-zA-Z0-9]{36}$/,
6868
/^xoxb-[0-9]+-[a-zA-Z0-9]+$/,
69+
// AgentSync's own vault identity. Bech32 HRP "AGE-SECRET-KEY-", separator
70+
// "1", uppercased, so the body is a strict subset of [A-Z0-9]. A native
71+
// X25519 key is a fixed 32 bytes, which bech32-encodes to exactly 58 chars,
72+
// so we bound to the exact length like the other fixed-size patterns. The
73+
// hyphens in the prefix mean the base64 catch-all below can never match it,
74+
// so this anchored entry is required to redact a key pasted as a JSON value.
75+
/^AGE-SECRET-KEY-1[A-Z0-9]{58}$/,
6976
/^[A-Za-z0-9+/]{40,}={0,2}$/,
7077
];
7178

@@ -87,6 +94,11 @@ export const EMBEDDED_SECRET_PATTERNS: ReadonlyArray<{ name: string; pattern: Re
8794
{ name: "aws-access-key", pattern: /AKIA[0-9A-Z]{16}/ },
8895
{ name: "google-api-key", pattern: /AIza[0-9A-Za-z_-]{35}/ },
8996
{ name: "slack-token", pattern: /xox[abprs]-[A-Za-z0-9-]{10,}/ },
97+
// The vault's own age identity decrypts every artifact AgentSync has ever
98+
// stored, retroactively and irreversibly once committed to git history. A
99+
// native X25519 key bech32-encodes to a fixed 58-char body; the 16-char
100+
// prefix makes false positives on prose effectively impossible.
101+
{ name: "age-secret-key", pattern: /AGE-SECRET-KEY-1[A-Z0-9]{58}/ },
90102
];
91103

92104
export interface RedactionResult<T> {

0 commit comments

Comments
 (0)