142 lines
7.5 KiB
Markdown
142 lines
7.5 KiB
Markdown
---
|
|
title: "Public Credentials Handling"
|
|
version: 3.8.40
|
|
lastUpdated: 2026-06-28
|
|
---
|
|
|
|
# Public Credentials Handling
|
|
|
|
> **Source of truth:** `open-sse/utils/publicCreds.ts`
|
|
> **Tests:** `tests/unit/publicCreds.test.ts`
|
|
> **Last updated:** 2026-06-28 — v3.8.40
|
|
> **Audience:** Engineers integrating providers that ship public OAuth client_id / client_secret / Firebase Web API keys in their public CLIs.
|
|
> **Status:** **MANDATORY** for all new code that embeds upstream identifiers.
|
|
|
|
## Why this exists
|
|
|
|
|
|
- [OAuth 2.0 for native apps (PKCE)](https://developers.google.com/identity/protocols/oauth2/native-app) — OAuth client_id / client_secret for installed apps are public; PKCE provides the actual security.
|
|
- [Firebase API keys](https://firebase.google.com/docs/projects/api-keys) — Web client identifiers are public by design.
|
|
|
|
OmniRoute must embed these values so users who do not configure `.env` still get a working OAuth flow out of the box. Without an embedded fallback, the Gemini / Antigravity / Windsurf providers stop working for any user who follows the "just clone and run" path.
|
|
|
|
However, literal values like `AIzaSy…`, `GOCSPX-…`, `…apps.googleusercontent.com` are matched by **GitHub Secret Scanning**, **Semgrep**, and similar pattern scanners. Every release becomes a noisy stream of false positives, push protection blocks legitimate commits, and operators stop trusting the alert feed.
|
|
|
|
The `open-sse/utils/publicCreds.ts` helper solves both constraints at once:
|
|
|
|
- Embeds the public identifier as a **XOR-masked byte sequence** (no scanner pattern in source).
|
|
- Decodes at runtime via `decodePublicCred` / `resolvePublicCred`.
|
|
- Detects raw values that already follow well-known prefixes (`AIza`, `GOCSPX-`, `<digits>-<32hex>.apps.googleusercontent.com`, `Iv1.<hex>`) and passes them through unchanged, so users with raw values in their existing `.env` keep working with **zero migration**.
|
|
|
|
This is **obfuscation, not encryption.** Anyone reading the source can recover the value — which is fine because the value is public by design. The only goal is to avoid scanner regex matches.
|
|
|
|
## The mandatory pattern
|
|
|
|
### 1. Adding a new public credential
|
|
|
|
When you need to embed a new upstream-provided value that:
|
|
|
|
- comes from a public CLI / desktop app / browser bundle, **and**
|
|
- the upstream provider documents (or treats) it as a public client identifier, **and**
|
|
- a pattern scanner would otherwise match it (`AIza…`, `GOCSPX-…`, `<digits>-…apps.googleusercontent.com`, etc.),
|
|
|
|
…follow this checklist:
|
|
|
|
1. Generate the masked byte sequence:
|
|
|
|
```bash
|
|
node --import tsx/esm -e \
|
|
'import("./open-sse/utils/publicCreds.ts").then(m =>
|
|
console.log(JSON.stringify(Array.from(
|
|
Buffer.from(m.encodePublicCred("THE_PUBLIC_VALUE"), "base64")
|
|
))))'
|
|
```
|
|
|
|
2. Add a new entry to `EMBEDDED_DEFAULTS` in `open-sse/utils/publicCreds.ts` with a **neutral key name** (`<provider>_id`, `<provider>_alt`, `<provider>_fb`, etc.). Do **not** use names like `client_secret` or `api_key` in the helper — those words trigger Semgrep generic-secret rules.
|
|
|
|
3. Add a `keyof typeof EMBEDDED_DEFAULTS` to the public type union (it is inferred automatically).
|
|
|
|
4. In the consumer code, replace the hardcoded literal with:
|
|
|
|
```ts
|
|
// single env override
|
|
clientSecret: resolvePublicCred("provider_alt", "PROVIDER_OAUTH_CLIENT_SECRET"),
|
|
|
|
// multiple env aliases (first non-empty wins)
|
|
clientId: resolvePublicCredMulti("provider_id", [
|
|
"PROVIDER_CLI_OAUTH_CLIENT_ID",
|
|
"PROVIDER_OAUTH_CLIENT_ID",
|
|
]),
|
|
|
|
// no env override (always embedded default)
|
|
firebaseApiKey: resolvePublicCred("provider_fb"),
|
|
```
|
|
|
|
5. Remove the literal from `.env.example` (replace with comment-only documentation pointing readers here):
|
|
|
|
```dotenv
|
|
# ── Provider (Google / Firebase / etc.) ──
|
|
# Public OAuth credentials are baked into the code via
|
|
# open-sse/utils/publicCreds.ts. Set these vars only to use your own.
|
|
# PROVIDER_OAUTH_CLIENT_ID=
|
|
# PROVIDER_OAUTH_CLIENT_SECRET=
|
|
```
|
|
|
|
6. Update `tests/unit/publicCreds.test.ts` to add a shape assertion for the new key (verify format, not literal value — see existing tests for the pattern).
|
|
|
|
7. **Never** add `AIza…` / `GOCSPX-…` / `…apps.googleusercontent.com` literals to test files. Use the `FAKE_*` constants built from `.join("")` fragments (see existing tests).
|
|
|
|
### 2. Consumers
|
|
|
|
- **Read from `resolvePublicCred()` / `resolvePublicCredMulti()` only** — never call `decodePublicCredBytes()` directly outside the helper.
|
|
- The helper is intentionally cheap (linear byte XOR) and safe to call at module-load time; defaults are computed once.
|
|
- The env override always wins. If a user sets `PROVIDER_OAUTH_CLIENT_SECRET=GOCSPX-myown`, the helper passes that raw value straight through.
|
|
|
|
### 3. Forbidden patterns
|
|
|
|
❌ **Never** do any of the following in production code (`src/`, `open-sse/`, `electron/`, `bin/`):
|
|
|
|
```ts
|
|
// BAD: literal value triggers Secret Scanning + Semgrep
|
|
clientSecret: process.env.PROVIDER_OAUTH_CLIENT_SECRET || "GOCSPX-realvalue",
|
|
|
|
// BAD: base64 of the literal — GitHub still detects since Feb/2025
|
|
clientSecret: process.env.PROVIDER_OAUTH_CLIENT_SECRET ||
|
|
Buffer.from("R09DU1BYLXJlYWx2YWx1ZQ==", "base64").toString(),
|
|
|
|
// BAD: string concatenation that re-assembles the pattern at runtime
|
|
clientSecret: "GO" + "CS" + "PX-" + "realvalue",
|
|
|
|
// BAD: hex/ROT13 encoding — different obfuscation, same risk of detection
|
|
clientSecret: hexDecode("474f4353..."),
|
|
```
|
|
|
|
These all eventually trip a scanner. Use `resolvePublicCred()`.
|
|
|
|
❌ **Never** add literal credentials to `.env.example`. Users who need real upstream values can extract them from the public CLI themselves, or use their own OAuth registration.
|
|
|
|
❌ **Never** dismiss a new secret-scanning alert without first checking whether the credential should be moved to this helper.
|
|
|
|
## Related controls
|
|
|
|
- `RAW_VALUE_PATTERN` in `publicCreds.ts` enumerates the prefixes that trigger passthrough (retrocompat). Extend it only for documented public credential formats, never for proprietary secrets.
|
|
- `.env.example` lives in CI's `check-env-doc-sync` script — when you remove a var here, make sure the docs match.
|
|
- The `npm run test:vitest` and `node --import tsx/esm --test tests/unit/publicCreds.test.ts` suites must both stay green.
|
|
|
|
## When NOT to use this helper
|
|
|
|
This helper is **only** for credentials that are:
|
|
|
|
1. Distributed publicly by the upstream provider (CLI binary, browser bundle, official docs).
|
|
2. Documented or strongly implied to be non-confidential (PKCE-protected, Firebase Web key, similar).
|
|
|
|
For everything else — operator-issued tokens, per-tenant secrets, your own OAuth app's client_secret, encryption keys, JWT secrets, database passwords — use **env vars only** (`process.env.FOO`, `||` fallback to empty / explicit error). These belong in `.env` and the [encrypted credentials store](./COMPLIANCE.md), not in source.
|
|
|
|
## References
|
|
|
|
- [Google: OAuth 2.0 for native apps](https://developers.google.com/identity/protocols/oauth2/native-app)
|
|
- [Firebase: API keys for client identification](https://firebase.google.com/docs/projects/api-keys)
|
|
- [GitHub Secret Scanning supported secrets](https://docs.github.com/en/code-security/secret-scanning/introduction/supported-secret-scanning-patterns)
|
|
- [GitHub: base64 detection for tokens (Feb 2025)](https://github.blog/changelog/2025-02-14-secret-scanning-detects-base64-encoded-github-tokens/)
|
|
- Commit introducing this helper: `1a39c31f` — _fix(security): mask public upstream creds + centralize error sanitization_
|