mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 16:56:33 +00:00
fix(firmware): diagnostic dump + smart-quote / BOM / multi-quote handling
Adds aggressive normalisation to tryParsePrivateKey:
- Strip UTF-8 BOM
- Replace smart quotes (" " ' ') with ASCII
- Strip multiple layers of wrapping quotes
- Combine escape-unfold with quote-strip (env vars that quote AND escape)
- Strip whitespace inside base64 candidate before decode
On parse failure, dumps length + head/tail samples + first-byte hex so
the operator can spot exactly what shape the env var arrived in.
This commit is contained in:
parent
936e6170a6
commit
6473f0fc95
1 changed files with 50 additions and 22 deletions
|
|
@ -150,11 +150,17 @@ function loadOrCreateKeyPair(
|
|||
log.info("firmware: signing key loaded from BF_FIRMWARE_SIGNING_KEY env");
|
||||
return { privateKey: parsed, publicKeyPem: String(pub) };
|
||||
}
|
||||
// Diagnostic dump so the operator can spot common pitfalls (smart quotes,
|
||||
// wrong key type, base64-of-binary instead of base64-of-PEM, etc).
|
||||
const head = envKey.slice(0, 60).replace(/\n/g, "\\n");
|
||||
const tail = envKey.slice(-40).replace(/\n/g, "\\n");
|
||||
const hexFirst = Array.from(envKey.slice(0, 8))
|
||||
.map((c) => c.charCodeAt(0).toString(16).padStart(2, "0"))
|
||||
.join(" ");
|
||||
log.warn(
|
||||
"firmware: BF_FIRMWARE_SIGNING_KEY set but could not be parsed as PEM " +
|
||||
"(checked literal, unescaped \\n, CRLF→LF, base64-wrapped). Falling " +
|
||||
"back to on-disk key / fresh generation. Existing kiosks will need a " +
|
||||
"re-pair if the public key changes.",
|
||||
`firmware: BF_FIRMWARE_SIGNING_KEY (${String(envKey.length)} chars) failed PEM parse. ` +
|
||||
`head="${head}" tail="${tail}" hex0..7=${hexFirst}. ` +
|
||||
`Falling back to on-disk key / fresh generation.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -176,36 +182,58 @@ function loadOrCreateKeyPair(
|
|||
|
||||
/**
|
||||
* Try several normalisations of an env-supplied PEM string. Coolify / docker
|
||||
* compose env passing routinely strips real newlines or wraps in quotes.
|
||||
* compose env passing routinely strips real newlines, wraps in quotes,
|
||||
* injects smart-quote unicode, drops BOMs in front, or doubles up escapes.
|
||||
*/
|
||||
function tryParsePrivateKey(raw: string): KeyObject | null {
|
||||
const candidates: string[] = [];
|
||||
candidates.push(raw);
|
||||
|
||||
// Always start with a "cleaned" baseline: strip BOM + smart quotes →
|
||||
// ASCII quotes + trim. Most env-injection quirks land here.
|
||||
const cleaned = raw
|
||||
.replace(/^/, "")
|
||||
.replace(/[“”]/g, '"')
|
||||
.replace(/[‘’]/g, "'")
|
||||
.trim();
|
||||
|
||||
candidates.push(cleaned);
|
||||
|
||||
// \n / \r\n escape sequences → real newlines.
|
||||
if (raw.includes("\\n")) {
|
||||
candidates.push(raw.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n"));
|
||||
if (cleaned.includes("\\n")) {
|
||||
candidates.push(cleaned.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n"));
|
||||
}
|
||||
// CRLF → LF.
|
||||
if (cleaned.includes("\r")) candidates.push(cleaned.replace(/\r\n?/g, "\n"));
|
||||
// Strip surrounding single / double quotes (one or more layers).
|
||||
let unq = cleaned;
|
||||
while (/^["'](.*)["']$/s.test(unq)) {
|
||||
const m = unq.match(/^["'](.*)["']$/s)!;
|
||||
unq = m[1]!;
|
||||
}
|
||||
if (unq !== cleaned) candidates.push(unq);
|
||||
// Combination: stripped + escape-decoded.
|
||||
if (unq.includes("\\n")) {
|
||||
candidates.push(unq.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n"));
|
||||
}
|
||||
// CRLF → LF
|
||||
if (raw.includes("\r")) candidates.push(raw.replace(/\r\n?/g, "\n"));
|
||||
// Strip surrounding single / double quotes.
|
||||
const m = raw.match(/^["'](.*)["']$/s);
|
||||
if (m && m[1]) candidates.push(m[1]);
|
||||
// Base64-encoded entire PEM (some platforms recommend this for safety).
|
||||
if (/^[A-Za-z0-9+/=\s]+$/.test(raw.trim())) {
|
||||
if (/^[A-Za-z0-9+/=\s]+$/.test(cleaned)) {
|
||||
try {
|
||||
const decoded = Buffer.from(raw.trim(), "base64").toString("utf8");
|
||||
const decoded = Buffer.from(cleaned.replace(/\s+/g, ""), "base64").toString("utf8");
|
||||
if (decoded.includes("BEGIN")) candidates.push(decoded);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
// Recover from "BEGIN PRIVATE KEY-----<body>-----END PRIVATE KEY" with no
|
||||
// internal line breaks: re-inject 64-char-wide line breaks around the body.
|
||||
if (/-----BEGIN [^-]+-----.*-----END [^-]+-----/.test(raw) && !raw.includes("\n")) {
|
||||
const pemMatch = raw.match(/-----BEGIN ([^-]+)-----(.*)-----END \1-----/s);
|
||||
if (pemMatch) {
|
||||
const header = pemMatch[1]!;
|
||||
const body = pemMatch[2]!.replace(/\s+/g, "");
|
||||
const wrapped = body.match(/.{1,64}/g)?.join("\n") ?? body;
|
||||
candidates.push(`-----BEGIN ${header}-----\n${wrapped}\n-----END ${header}-----\n`);
|
||||
for (const variant of [cleaned, unq]) {
|
||||
if (/-----BEGIN [^-]+-----.*-----END [^-]+-----/.test(variant)
|
||||
&& !variant.includes("\n")) {
|
||||
const pemMatch = variant.match(/-----BEGIN ([^-]+)-----(.*)-----END \1-----/s);
|
||||
if (pemMatch) {
|
||||
const header = pemMatch[1]!;
|
||||
const body = pemMatch[2]!.replace(/\s+/g, "");
|
||||
const wrapped = body.match(/.{1,64}/g)?.join("\n") ?? body;
|
||||
candidates.push(`-----BEGIN ${header}-----\n${wrapped}\n-----END ${header}-----\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue