Summary
Audited @f-o-t/crypto (parsePkcs12) and @f-o-t/digital-certificate (parseCertificate) for password handling with special characters. Found three related gaps:
- No tests for passwords containing special characters (ASCII
!@#$, accented é ã ç, emoji).
- Legacy PBE content-decryption path (3DES/PBE-SHA1-3DES) is completely untested — real Brazilian A1 ICP-Brasil certificates commonly use this format.
passwordToBmpString silently produces invalid BMPString for passwords with characters outside Unicode BMP (U+10000+) — no error is raised, wrong key is derived.
Root Cause
1 — No test coverage for special-char passwords
All existing tests use ASCII-only passwords ("test1234", "test123"). The test fixtures:
digital-certificate/__tests__/fixtures/test-certificate.pfx → MAC: sha256, PBES2/PBKDF2/AES-256-CBC
e-signature/__tests__/fixtures/test.p12 → same (modern OpenSSL default)
No test verifies that a password like "Sênh@123!" (common in Brazilian enterprise setups) is handled correctly.
2 — Legacy PBE content-decryption path is untested
parsePkcs12 has two content-decryption paths:
| Path |
Code location |
Password encoding |
Tested? |
| PBES2 (AES-CBC + PBKDF2) |
decryptPbes2 → pbkdf2Sync(password, ...) |
UTF-8 (correct, OpenSSL-compatible) |
✅ MAC only |
| Legacy PBE (3DES / RC2 + PKCS#12 KDF) |
decryptPbe → pkcs12Kdf → passwordToBmpString |
UTF-16BE (correct per RFC 7292) |
❌ Never |
MAC verification always calls pkcs12Kdf (even for PBES2 PFX files), so passwordToBmpString is exercised — but only for MAC, not for content decryption. The 3DES decryptShroudedKeyBag path is never hit in any test.
Brazilian A1 certificates from ICP-Brasil CAs (the exact target of @f-o-t/digital-certificate) frequently use the legacy PBE-SHA1-3DES format — the path with zero coverage.
Verified via:
$ openssl pkcs12 -info -in __tests__/fixtures/test-certificate.pfx -password pass:test1234 -noout
MAC: sha256, Iteration 2048
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, ... ← modern only
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, ... ← modern only
3 — passwordToBmpString silently produces invalid BMPString for supplementary characters
File: libraries/crypto/src/pkcs12.ts:277
function passwordToBmpString(password: string): Uint8Array {
const result = new Uint8Array(password.length * 2 + 2);
for (let i = 0; i < password.length; i++) {
const code = password.charCodeAt(i); // ← returns UTF-16 code unit, not Unicode code point
result[i * 2] = (code >>> 8) & 0xff;
result[i * 2 + 1] = code & 0xff;
}
return result;
}
For supplementary characters like 😀 (U+1F600), JavaScript represents them as two UTF-16 surrogate code units (0xD83D, 0xDE00). charCodeAt returns those raw surrogate values and the function encodes them as-is. BMPString (per X.680 §8.3) only allows code points U+0000–U+FFFF; surrogates are not valid BMPString values.
Consequence: A password containing emoji produces a silently incorrect KDF input. The MAC verification and/or key decryption will fail with a misleading "wrong password?" error rather than an actionable message. No validation is raised at the API boundary.
Note: For all BMP characters (U+0000–U+FFFF) — including common ASCII special chars (!@#$%^&*) and Latin Extended accented chars (é, ã, ç, ñ) — the function is correct. The bug only affects U+10000+ code points.
Affected Files
libraries/crypto/src/pkcs12.ts:277 — passwordToBmpString: no guard against supplementary characters
libraries/digital-certificate/__tests__/certificate.test.ts — all tests use ASCII-only password "test1234"
libraries/e-signature/__tests__/sign-pdf.test.ts — all tests use ASCII-only password "test123"
libraries/digital-certificate/__tests__/fixtures/test-certificate.pfx — PBES2/AES only; no legacy 3DES fixture
Expected Behavior
- Passwords with common special characters (
!@#$%^&*()_+, accented é ã ç) work correctly for both PBES2 and legacy PBE formats.
- Passwords containing supplementary Unicode characters (U+10000+) raise a clear
Pkcs12Error explaining that BMPString only supports BMP characters, rather than deriving a wrong key silently.
- At least one test fixture uses legacy
PBE-SHA1-3DES format so that decryptShroudedKeyBag → decryptPbe → pkcs12Kdf is covered end-to-end.
Actual Behavior
- Special-char and non-ASCII passwords are untested — behavior is theoretically correct but unverified.
- Legacy PBE content decryption path has no test; a regression could go undetected.
- Passwords with emoji (or other supplementary chars) silently produce wrong key material and fail with a confusing "wrong password?" error.
Suggested Fix
A — Add validation in passwordToBmpString
// libraries/crypto/src/pkcs12.ts:277
function passwordToBmpString(password: string): Uint8Array {
// Validate no supplementary characters (> U+FFFF) — BMPString restriction
for (let i = 0; i < password.length; i++) {
const code = password.charCodeAt(i);
if (code >= 0xD800 && code <= 0xDFFF) {
throw new Pkcs12Error(
"Password contains a character outside the Unicode BMP (U+10000+). " +
"PKCS#12 BMPString encoding only supports U+0000–U+FFFF."
);
}
}
// ... existing encoding ...
}
B — Add a legacy PBE test fixture
Generate a 3DES fixture:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650 -nodes -subj "/CN=Test/O=Test"
openssl pkcs12 -export -legacy -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES \
-out test-legacy.pfx -inkey key.pem -in cert.pem -password pass:test1234
C — Add special-char password tests
// In both certificate.test.ts and sign-pdf.test.ts
it("parses PFX with special-character password", () => {
// Generate fixture with: openssl pkcs12 ... -password pass:Sênh@123!
const cert = parseCertificate(specialCharPfx, "Sênh@123!");
expect(cert.certPem).toContain("-----BEGIN CERTIFICATE-----");
});
References
- RFC 7292 Appendix B — PKCS#12 KDF password encoding (BMPString, UTF-16BE)
- RFC 8018 §5.2 — PBKDF2 (no encoding mandated; OpenSSL uses UTF-8)
libraries/crypto/src/pkcs12.ts:205 — pkcs12Kdf (uses passwordToBmpString)
libraries/crypto/src/pkcs12.ts:438 — decryptPbes2 (uses raw string → UTF-8 via Node.js)
libraries/crypto/src/pkcs12.ts:549 — decryptShroudedKeyBag (legacy 3DES path, untested)
Summary
Audited
@f-o-t/crypto(parsePkcs12) and@f-o-t/digital-certificate(parseCertificate) for password handling with special characters. Found three related gaps:!@#$, accentedé ã ç, emoji).passwordToBmpStringsilently produces invalid BMPString for passwords with characters outside Unicode BMP (U+10000+) — no error is raised, wrong key is derived.Root Cause
1 — No test coverage for special-char passwords
All existing tests use ASCII-only passwords (
"test1234","test123"). The test fixtures:No test verifies that a password like
"Sênh@123!"(common in Brazilian enterprise setups) is handled correctly.2 — Legacy PBE content-decryption path is untested
parsePkcs12has two content-decryption paths:decryptPbes2→pbkdf2Sync(password, ...)decryptPbe→pkcs12Kdf→passwordToBmpStringMAC verification always calls
pkcs12Kdf(even for PBES2 PFX files), sopasswordToBmpStringis exercised — but only for MAC, not for content decryption. The 3DESdecryptShroudedKeyBagpath is never hit in any test.Brazilian A1 certificates from ICP-Brasil CAs (the exact target of
@f-o-t/digital-certificate) frequently use the legacyPBE-SHA1-3DESformat — the path with zero coverage.Verified via:
3 —
passwordToBmpStringsilently produces invalid BMPString for supplementary charactersFile:
libraries/crypto/src/pkcs12.ts:277For supplementary characters like
😀(U+1F600), JavaScript represents them as two UTF-16 surrogate code units (0xD83D,0xDE00).charCodeAtreturns those raw surrogate values and the function encodes them as-is. BMPString (per X.680 §8.3) only allows code points U+0000–U+FFFF; surrogates are not valid BMPString values.Consequence: A password containing emoji produces a silently incorrect KDF input. The MAC verification and/or key decryption will fail with a misleading "wrong password?" error rather than an actionable message. No validation is raised at the API boundary.
Note: For all BMP characters (U+0000–U+FFFF) — including common ASCII special chars (
!@#$%^&*) and Latin Extended accented chars (é,ã,ç,ñ) — the function is correct. The bug only affects U+10000+ code points.Affected Files
libraries/crypto/src/pkcs12.ts:277—passwordToBmpString: no guard against supplementary characterslibraries/digital-certificate/__tests__/certificate.test.ts— all tests use ASCII-only password"test1234"libraries/e-signature/__tests__/sign-pdf.test.ts— all tests use ASCII-only password"test123"libraries/digital-certificate/__tests__/fixtures/test-certificate.pfx— PBES2/AES only; no legacy 3DES fixtureExpected Behavior
!@#$%^&*()_+, accentedé ã ç) work correctly for both PBES2 and legacy PBE formats.Pkcs12Errorexplaining that BMPString only supports BMP characters, rather than deriving a wrong key silently.PBE-SHA1-3DESformat so thatdecryptShroudedKeyBag→decryptPbe→pkcs12Kdfis covered end-to-end.Actual Behavior
Suggested Fix
A — Add validation in
passwordToBmpStringB — Add a legacy PBE test fixture
Generate a 3DES fixture:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650 -nodes -subj "/CN=Test/O=Test" openssl pkcs12 -export -legacy -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES \ -out test-legacy.pfx -inkey key.pem -in cert.pem -password pass:test1234C — Add special-char password tests
References
libraries/crypto/src/pkcs12.ts:205—pkcs12Kdf(usespasswordToBmpString)libraries/crypto/src/pkcs12.ts:438—decryptPbes2(uses raw string → UTF-8 via Node.js)libraries/crypto/src/pkcs12.ts:549—decryptShroudedKeyBag(legacy 3DES path, untested)