From: Stefan Gasser Date: Mon, 26 Jan 2026 08:18:03 +0000 (+0100) Subject: Rename API_KEY_OPENAI to API_KEY_SK with expanded pattern (#61) X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=d9b4e9df877b4b6e4a287730a139b33a64b1c59d;p=sgasser-llm-shield.git Rename API_KEY_OPENAI to API_KEY_SK with expanded pattern (#61) - Rename API_KEY_OPENAI to API_KEY_SK for broader coverage - Expand pattern from /sk-.../ to /sk[-_].../ to match both hyphen and underscore - Reduce minimum length from 45 to 20 chars for Stripe compatibility - Now detects: OpenAI, Anthropic, Stripe, RevenueCat, and similar sk-prefixed keys - Add tests for Stripe (sk_test_, sk_live_), Anthropic (sk-ant-), RevenueCat (sk_) - Update all documentation and config examples --- diff --git a/README.md b/README.md index 7f1abeb..bcfd14f 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Works with OpenAI, Anthropic, and compatible tools: **Secrets** - OpenSSH private keys - PEM private keys -- OpenAI API keys +- API keys with sk- or sk_ prefix (OpenAI, Anthropic, Stripe, etc.) - AWS access keys - GitHub tokens - JWT tokens diff --git a/config.example.yaml b/config.example.yaml index 319d059..3c20aaf 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -117,7 +117,7 @@ secrets_detection: # - PEM_PRIVATE_KEY: PEM formats (RSA, PRIVATE KEY, ENCRYPTED PRIVATE KEY) # # API Keys (opt-in): - # - API_KEY_OPENAI: OpenAI API keys (sk-...) + # - API_KEY_SK: Secret keys with sk- or sk_ prefix (OpenAI, Anthropic, Stripe, RevenueCat) # - API_KEY_AWS: AWS Access Keys (AKIA...) # - API_KEY_GITHUB: GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_) # @@ -133,7 +133,7 @@ secrets_detection: - OPENSSH_PRIVATE_KEY - PEM_PRIVATE_KEY # Uncomment to detect API keys and tokens: - # - API_KEY_OPENAI + # - API_KEY_SK # - API_KEY_AWS # - API_KEY_GITHUB # - JWT_TOKEN diff --git a/docs/concepts/secrets-detection.mdx b/docs/concepts/secrets-detection.mdx index f969773..9752c6a 100644 --- a/docs/concepts/secrets-detection.mdx +++ b/docs/concepts/secrets-detection.mdx @@ -20,7 +20,7 @@ PasteGuard detects secrets before PII detection and can block, mask, or route re | Type | Pattern | |------|---------| -| `API_KEY_OPENAI` | `sk-...` (48+ chars) | +| `API_KEY_SK` | `sk-...` or `sk_...` (20+ chars) - OpenAI, Anthropic, Stripe, RevenueCat | | `API_KEY_AWS` | `AKIA...` (20 chars) | | `API_KEY_GITHUB` | `ghp_...`, `gho_...`, `ghu_...`, `ghs_...`, `ghr_...` (40+ chars) | @@ -82,7 +82,7 @@ When secrets are detected: ``` X-PasteGuard-Secrets-Detected: true -X-PasteGuard-Secrets-Types: OPENSSH_PRIVATE_KEY,API_KEY_OPENAI +X-PasteGuard-Secrets-Types: OPENSSH_PRIVATE_KEY,API_KEY_SK ``` If secrets were masked: diff --git a/docs/configuration/secrets-detection.mdx b/docs/configuration/secrets-detection.mdx index d117228..2705e6b 100644 --- a/docs/configuration/secrets-detection.mdx +++ b/docs/configuration/secrets-detection.mdx @@ -72,7 +72,7 @@ secrets_detection: ```yaml secrets_detection: entities: - - API_KEY_OPENAI # sk-... (48+ chars) + - API_KEY_SK # sk-/sk_ prefix (OpenAI, Anthropic, Stripe, RevenueCat) - API_KEY_AWS # AKIA... (20 chars) - API_KEY_GITHUB # ghp_, gho_, ghu_, ghs_, ghr_ (40+ chars) ``` diff --git a/src/config.ts b/src/config.ts index 324b5c0..00e24ab 100644 --- a/src/config.ts +++ b/src/config.ts @@ -95,7 +95,7 @@ const DashboardSchema = z.object({ const SecretEntityTypes = [ "OPENSSH_PRIVATE_KEY", "PEM_PRIVATE_KEY", - "API_KEY_OPENAI", + "API_KEY_SK", "API_KEY_AWS", "API_KEY_GITHUB", "JWT_TOKEN", diff --git a/src/masking/placeholders.test.ts b/src/masking/placeholders.test.ts index d46dee2..d60a084 100644 --- a/src/masking/placeholders.test.ts +++ b/src/masking/placeholders.test.ts @@ -41,8 +41,8 @@ describe("generatePlaceholder", () => { describe("generateSecretPlaceholder", () => { test("generates secret placeholder", () => { - const result = generateSecretPlaceholder("API_KEY_OPENAI", 1); - expect(result).toBe("[[API_KEY_OPENAI_1]]"); + const result = generateSecretPlaceholder("API_KEY_SK", 1); + expect(result).toBe("[[API_KEY_SK_1]]"); }); test("generates secret placeholder with different type and count", () => { diff --git a/src/masking/placeholders.ts b/src/masking/placeholders.ts index 66f38b2..5669436 100644 --- a/src/masking/placeholders.ts +++ b/src/masking/placeholders.ts @@ -10,7 +10,7 @@ export const PLACEHOLDER_DELIMITERS = { /** PII placeholder format: [[TYPE_N]] e.g. [[PERSON_1]], [[EMAIL_ADDRESS_2]] */ export const PII_PLACEHOLDER_FORMAT = "[[{TYPE}_{N}]]"; -/** Secrets placeholder format: [[TYPE_N]] e.g. [[API_KEY_OPENAI_1]] */ +/** Secrets placeholder format: [[TYPE_N]] e.g. [[API_KEY_SK_1]] */ export const SECRET_PLACEHOLDER_FORMAT = "[[{N}]]"; /** @@ -22,7 +22,7 @@ export function generatePlaceholder(format: string, type: string, count: number) /** * Generates a secret placeholder string - * {N} is replaced with TYPE_COUNT e.g. API_KEY_OPENAI_1 + * {N} is replaced with TYPE_COUNT e.g. API_KEY_SK_1 */ export function generateSecretPlaceholder(type: string, count: number): string { return SECRET_PLACEHOLDER_FORMAT.replace("{N}", `${type}_${count}`); diff --git a/src/secrets/detect.test.ts b/src/secrets/detect.test.ts index 0337d83..bfb68a7 100644 --- a/src/secrets/detect.test.ts +++ b/src/secrets/detect.test.ts @@ -180,8 +180,13 @@ describe("detectSecrets", () => { }); }); -// Test data for new secret types +// Test data for secret types const openaiApiKey = "sk-proj-abc123def456ghi789jkl012mno345pqr678stu901vwx"; +const anthropicApiKey = + "sk-ant-api03-abc123def456ghi789jkl012mno345pqr678stu901vwxyz0123456789abcdefghijklmnopqrstuvwxyz"; +const stripeTestKey = "sk_test_abc123def456ghi789jkl012"; +const stripeLiveKey = "sk_live_xyz789abc123def456ghi789"; +const revenueCatKey = "sk_abcdefghijklmnopqrstuvwx"; const awsAccessKey = "AKIAIOSFODNN7EXAMPLE"; const githubToken = "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1234"; const githubOAuthToken = "gho_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx5678"; @@ -192,18 +197,50 @@ const bearerToken = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9abcdefghijk"; describe("detectSecrets - API Keys", () => { const apiKeyConfig: SecretsDetectionConfig = { ...defaultConfig, - entities: ["API_KEY_OPENAI", "API_KEY_AWS", "API_KEY_GITHUB"], + entities: ["API_KEY_SK", "API_KEY_AWS", "API_KEY_GITHUB"], }; - test("detects OpenAI API key", () => { + test("detects OpenAI API key (sk-proj-...)", () => { const text = `My API key is ${openaiApiKey}`; const result = detectSecrets(text, apiKeyConfig); expect(result.detected).toBe(true); expect(result.matches).toHaveLength(1); - expect(result.matches[0].type).toBe("API_KEY_OPENAI"); + expect(result.matches[0].type).toBe("API_KEY_SK"); expect(result.matches[0].count).toBe(1); expect(result.locations).toBeDefined(); - expect(result.locations?.[0].type).toBe("API_KEY_OPENAI"); + expect(result.locations?.[0].type).toBe("API_KEY_SK"); + }); + + test("detects Anthropic API key (sk-ant-...)", () => { + const text = `Anthropic key: ${anthropicApiKey}`; + const result = detectSecrets(text, apiKeyConfig); + expect(result.detected).toBe(true); + expect(result.matches).toHaveLength(1); + expect(result.matches[0].type).toBe("API_KEY_SK"); + }); + + test("detects Stripe test key (sk_test_...)", () => { + const text = `STRIPE_SECRET_KEY=${stripeTestKey}`; + const result = detectSecrets(text, apiKeyConfig); + expect(result.detected).toBe(true); + expect(result.matches).toHaveLength(1); + expect(result.matches[0].type).toBe("API_KEY_SK"); + }); + + test("detects Stripe live key (sk_live_...)", () => { + const text = `export STRIPE_KEY="${stripeLiveKey}"`; + const result = detectSecrets(text, apiKeyConfig); + expect(result.detected).toBe(true); + expect(result.matches).toHaveLength(1); + expect(result.matches[0].type).toBe("API_KEY_SK"); + }); + + test("detects RevenueCat key (sk_...)", () => { + const text = `revenuecat_api_key: ${revenueCatKey}`; + const result = detectSecrets(text, apiKeyConfig); + expect(result.detected).toBe(true); + expect(result.matches).toHaveLength(1); + expect(result.matches[0].type).toBe("API_KEY_SK"); }); test("detects AWS access key", () => { @@ -236,7 +273,7 @@ describe("detectSecrets - API Keys", () => { const result = detectSecrets(text, apiKeyConfig); expect(result.detected).toBe(true); expect(result.matches).toHaveLength(3); - expect(result.matches.find((m) => m.type === "API_KEY_OPENAI")).toBeDefined(); + expect(result.matches.find((m) => m.type === "API_KEY_SK")).toBeDefined(); expect(result.matches.find((m) => m.type === "API_KEY_AWS")).toBeDefined(); expect(result.matches.find((m) => m.type === "API_KEY_GITHUB")).toBeDefined(); }); @@ -247,6 +284,12 @@ describe("detectSecrets - API Keys", () => { expect(result.detected).toBe(false); }); + test("avoids false positive - sk_ prefix but too short", () => { + const text = "This sk_short is not valid"; + const result = detectSecrets(text, apiKeyConfig); + expect(result.detected).toBe(false); + }); + test("avoids false positive - AKIA prefix but wrong length", () => { const text = "AKIA12345 is not valid"; const result = detectSecrets(text, apiKeyConfig); @@ -611,7 +654,7 @@ describe("detectSecrets - Mixed secret types", () => { entities: [ "OPENSSH_PRIVATE_KEY", "PEM_PRIVATE_KEY", - "API_KEY_OPENAI", + "API_KEY_SK", "API_KEY_AWS", "API_KEY_GITHUB", "JWT_TOKEN", diff --git a/src/secrets/mask.test.ts b/src/secrets/mask.test.ts index 3bd8564..dcfce62 100644 --- a/src/secrets/mask.test.ts +++ b/src/secrets/mask.test.ts @@ -14,6 +14,7 @@ import { } from "./mask"; const sampleSecret = "sk-proj-abc123def456ghi789jkl012mno345pqr678stu901vwx"; +const stripeSecret = "sk_live_abc123def456ghi789jkl012"; /** Helper to create a minimal request from messages */ function createRequest(messages: OpenAIMessage[]): OpenAIRequest { @@ -24,35 +25,35 @@ describe("secrets placeholder format", () => { test("uses [[TYPE_N]] format", () => { const text = `My API key is ${sampleSecret}`; const locations: SecretLocation[] = [ - { start: 14, end: 14 + sampleSecret.length, type: "API_KEY_OPENAI" }, + { start: 14, end: 14 + sampleSecret.length, type: "API_KEY_SK" }, ]; const result = maskSecrets(text, locations); - expect(result.masked).toBe("My API key is [[API_KEY_OPENAI_1]]"); + expect(result.masked).toBe("My API key is [[API_KEY_SK_1]]"); }); test("increments counter per secret type", () => { const anotherSecret = "sk-proj-xyz789abc123def456ghi789jkl012mno345pqr678"; const text = `Key1: ${sampleSecret} Key2: ${anotherSecret}`; const locations: SecretLocation[] = [ - { start: 6, end: 6 + sampleSecret.length, type: "API_KEY_OPENAI" }, + { start: 6, end: 6 + sampleSecret.length, type: "API_KEY_SK" }, { start: 6 + sampleSecret.length + 7, end: 6 + sampleSecret.length + 7 + anotherSecret.length, - type: "API_KEY_OPENAI", + type: "API_KEY_SK", }, ]; const result = maskSecrets(text, locations); - expect(result.masked).toContain("[[API_KEY_OPENAI_1]]"); - expect(result.masked).toContain("[[API_KEY_OPENAI_2]]"); + expect(result.masked).toContain("[[API_KEY_SK_1]]"); + expect(result.masked).toContain("[[API_KEY_SK_2]]"); }); test("tracks different secret types separately", () => { const awsKey = "AKIAIOSFODNN7EXAMPLE"; const text = `OpenAI: ${sampleSecret} AWS: ${awsKey}`; const locations: SecretLocation[] = [ - { start: 8, end: 8 + sampleSecret.length, type: "API_KEY_OPENAI" }, + { start: 8, end: 8 + sampleSecret.length, type: "API_KEY_SK" }, { start: 8 + sampleSecret.length + 6, end: 8 + sampleSecret.length + 6 + awsKey.length, @@ -61,9 +62,20 @@ describe("secrets placeholder format", () => { ]; const result = maskSecrets(text, locations); - expect(result.masked).toContain("[[API_KEY_OPENAI_1]]"); + expect(result.masked).toContain("[[API_KEY_SK_1]]"); expect(result.masked).toContain("[[API_KEY_AWS_1]]"); }); + + test("masks sk_ prefix keys (Stripe)", () => { + const text = `Stripe key: ${stripeSecret}`; + const locations: SecretLocation[] = [ + { start: 12, end: 12 + stripeSecret.length, type: "API_KEY_SK" }, + ]; + const result = maskSecrets(text, locations); + + expect(result.masked).toBe("Stripe key: [[API_KEY_SK_1]]"); + expect(result.context.mapping["[[API_KEY_SK_1]]"]).toBe(stripeSecret); + }); }); describe("maskRequest with MessageSecretsResult", () => { @@ -74,13 +86,13 @@ describe("maskRequest with MessageSecretsResult", () => { ]); // spanLocations[0] = first message (user), spanLocations[1] = second message (assistant) const detection = createSecretsResultFromSpans([ - [{ start: 10, end: 10 + sampleSecret.length, type: "API_KEY_OPENAI" }], + [{ start: 10, end: 10 + sampleSecret.length, type: "API_KEY_SK" }], [], ]); const { masked, context } = maskRequest(request, detection, openaiExtractor); - expect(masked.messages[0].content).toContain("[[API_KEY_OPENAI_1]]"); + expect(masked.messages[0].content).toContain("[[API_KEY_SK_1]]"); expect(masked.messages[0].content).not.toContain(sampleSecret); expect(masked.messages[1].content).toBe("I'll help you with that."); expect(Object.keys(context.mapping)).toHaveLength(1); @@ -92,14 +104,14 @@ describe("maskRequest with MessageSecretsResult", () => { { role: "user", content: `Key2: ${sampleSecret}` }, ]); const detection = createSecretsResultFromSpans([ - [{ start: 6, end: 6 + sampleSecret.length, type: "API_KEY_OPENAI" }], - [{ start: 6, end: 6 + sampleSecret.length, type: "API_KEY_OPENAI" }], + [{ start: 6, end: 6 + sampleSecret.length, type: "API_KEY_SK" }], + [{ start: 6, end: 6 + sampleSecret.length, type: "API_KEY_SK" }], ]); const { masked, context } = maskRequest(request, detection, openaiExtractor); - expect(masked.messages[0].content).toBe("Key1: [[API_KEY_OPENAI_1]]"); - expect(masked.messages[1].content).toBe("Key2: [[API_KEY_OPENAI_1]]"); + expect(masked.messages[0].content).toBe("Key1: [[API_KEY_SK_1]]"); + expect(masked.messages[1].content).toBe("Key2: [[API_KEY_SK_1]]"); expect(Object.keys(context.mapping)).toHaveLength(1); }); @@ -115,13 +127,13 @@ describe("maskRequest with MessageSecretsResult", () => { ]); // Two spans: text content at index 0, image is skipped const detection = createSecretsResultFromSpans([ - [{ start: 5, end: 5 + sampleSecret.length, type: "API_KEY_OPENAI" }], + [{ start: 5, end: 5 + sampleSecret.length, type: "API_KEY_SK" }], ]); const { masked } = maskRequest(request, detection, openaiExtractor); const content = masked.messages[0].content as Array<{ type: string; text?: string }>; - expect(content[0].text).toBe("Key: [[API_KEY_OPENAI_1]]"); + expect(content[0].text).toBe("Key: [[API_KEY_SK_1]]"); expect(content[1].type).toBe("image_url"); }); }); @@ -129,7 +141,7 @@ describe("maskRequest with MessageSecretsResult", () => { describe("streaming with secrets placeholders", () => { test("buffers partial [[ placeholder", () => { const context = createSecretsMaskingContext(); - context.mapping["[[API_KEY_OPENAI_1]]"] = sampleSecret; + context.mapping["[[API_KEY_SK_1]]"] = sampleSecret; const { output, remainingBuffer } = unmaskSecretsStreamChunk("", "Key: [[API_KEY", context); @@ -139,11 +151,11 @@ describe("streaming with secrets placeholders", () => { test("completes buffered placeholder across chunks", () => { const context = createSecretsMaskingContext(); - context.mapping["[[API_KEY_OPENAI_1]]"] = sampleSecret; + context.mapping["[[API_KEY_SK_1]]"] = sampleSecret; const { output, remainingBuffer } = unmaskSecretsStreamChunk( "[[API_KEY", - "_OPENAI_1]] done", + "_SK_1]] done", context, ); @@ -169,14 +181,14 @@ Please store them securely. { start: originalText.indexOf(sampleSecret), end: originalText.indexOf(sampleSecret) + sampleSecret.length, - type: "API_KEY_OPENAI", + type: "API_KEY_SK", }, ]; const { masked, context } = maskSecrets(originalText, locations); expect(masked).not.toContain(sampleSecret); - expect(masked).toContain("[[API_KEY_OPENAI_1]]"); + expect(masked).toContain("[[API_KEY_SK_1]]"); const restored = unmaskSecrets(masked, context); expect(restored).toBe(originalText); @@ -186,7 +198,7 @@ Please store them securely. describe("unmaskSecretsResponse", () => { test("unmasks all choices in response", () => { const context = createSecretsMaskingContext(); - context.mapping["[[API_KEY_OPENAI_1]]"] = sampleSecret; + context.mapping["[[API_KEY_SK_1]]"] = sampleSecret; const response: OpenAIResponse = { id: "test", @@ -198,7 +210,7 @@ describe("unmaskSecretsResponse", () => { index: 0, message: { role: "assistant", - content: "Your key is [[API_KEY_OPENAI_1]]", + content: "Your key is [[API_KEY_SK_1]]", }, finish_reason: "stop", }, @@ -244,16 +256,16 @@ describe("edge cases", () => { test("reuses placeholder for duplicate secret values", () => { const text = `Key1: ${sampleSecret} Key2: ${sampleSecret}`; const locations: SecretLocation[] = [ - { start: 6, end: 6 + sampleSecret.length, type: "API_KEY_OPENAI" }, + { start: 6, end: 6 + sampleSecret.length, type: "API_KEY_SK" }, { start: 6 + sampleSecret.length + 7, end: 6 + sampleSecret.length * 2 + 7, - type: "API_KEY_OPENAI", + type: "API_KEY_SK", }, ]; const result = maskSecrets(text, locations); - expect(result.masked).toBe("Key1: [[API_KEY_OPENAI_1]] Key2: [[API_KEY_OPENAI_1]]"); + expect(result.masked).toBe("Key1: [[API_KEY_SK_1]] Key2: [[API_KEY_SK_1]]"); expect(Object.keys(result.context.mapping)).toHaveLength(1); }); @@ -262,18 +274,18 @@ describe("edge cases", () => { maskSecrets( `Key: ${sampleSecret}`, - [{ start: 5, end: 5 + sampleSecret.length, type: "API_KEY_OPENAI" }], + [{ start: 5, end: 5 + sampleSecret.length, type: "API_KEY_SK" }], context, ); const anotherSecret = "sk-proj-xyz789abc123def456ghi789jkl012mno345pqr678"; const result2 = maskSecrets( `Another: ${anotherSecret}`, - [{ start: 9, end: 9 + anotherSecret.length, type: "API_KEY_OPENAI" }], + [{ start: 9, end: 9 + anotherSecret.length, type: "API_KEY_SK" }], context, ); - expect(result2.masked).toBe("Another: [[API_KEY_OPENAI_2]]"); + expect(result2.masked).toBe("Another: [[API_KEY_SK_2]]"); expect(Object.keys(context.mapping)).toHaveLength(2); }); }); diff --git a/src/secrets/patterns/api-keys.ts b/src/secrets/patterns/api-keys.ts index 57e33e8..b7fda29 100644 --- a/src/secrets/patterns/api-keys.ts +++ b/src/secrets/patterns/api-keys.ts @@ -5,22 +5,25 @@ import { detectPattern } from "./utils"; * API keys detector * * Detects: - * - API_KEY_OPENAI: OpenAI API keys (sk-...) + * - API_KEY_SK: Secret keys with sk- or sk_ prefix (OpenAI, Anthropic, Stripe, RevenueCat) * - API_KEY_AWS: AWS Access Keys (AKIA...) * - API_KEY_GITHUB: GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_) */ export const apiKeysDetector: PatternDetector = { - patterns: ["API_KEY_OPENAI", "API_KEY_AWS", "API_KEY_GITHUB"], + patterns: ["API_KEY_SK", "API_KEY_AWS", "API_KEY_GITHUB"], detect(text: string, enabledTypes: Set) { const matches: SecretsMatch[] = []; const locations: SecretLocation[] = []; - // OpenAI API keys: sk-... followed by alphanumeric chars - // Modern format: sk-proj-... or sk-... with 48+ total chars - if (enabledTypes.has("API_KEY_OPENAI")) { - const openaiPattern = /sk-[a-zA-Z0-9_-]{45,}/g; - detectPattern(text, openaiPattern, "API_KEY_OPENAI", matches, locations); + // Secret keys with sk- or sk_ prefix: + // - OpenAI: sk-proj-... (48+ chars) + // - Anthropic: sk-ant-api03-... (~100 chars) + // - Stripe: sk_test_..., sk_live_... (24-32 chars after prefix) + // - RevenueCat, Moyasar: sk_... (various lengths) + if (enabledTypes.has("API_KEY_SK")) { + const skPattern = /sk[-_][a-zA-Z0-9_-]{20,}/g; + detectPattern(text, skPattern, "API_KEY_SK", matches, locations); } // AWS access keys: AKIA followed by 16 uppercase alphanumeric chars diff --git a/src/secrets/patterns/types.ts b/src/secrets/patterns/types.ts index 500016a..6a1563e 100644 --- a/src/secrets/patterns/types.ts +++ b/src/secrets/patterns/types.ts @@ -4,7 +4,7 @@ export type SecretEntityType = | "OPENSSH_PRIVATE_KEY" | "PEM_PRIVATE_KEY" - | "API_KEY_OPENAI" + | "API_KEY_SK" | "API_KEY_AWS" | "API_KEY_GITHUB" | "JWT_TOKEN"