@blamejs/core 0.9.46 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +951 -893
- package/index.js +30 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +67 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-imap-command.js +335 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-smtp-command.js +58 -3
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +1102 -0
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +164 -34
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +269 -0
- package/lib/mail-server-submission.js +1032 -0
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +130 -10
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +168 -17
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/safe-smtp.js
CHANGED
|
@@ -121,8 +121,65 @@ function dotUnstuff(buf) {
|
|
|
121
121
|
return out.subarray(0, oi);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* @primitive b.safeSmtp.dotStuff
|
|
126
|
+
* @signature b.safeSmtp.dotStuff(buf)
|
|
127
|
+
* @since 0.9.57
|
|
128
|
+
* @status stable
|
|
129
|
+
* @related b.safeSmtp.dotUnstuff, b.safeSmtp.findDotTerminator
|
|
130
|
+
*
|
|
131
|
+
* Apply RFC 5321 §4.5.2 / RFC 1939 §3 dot-stuffing to a DATA / RETR
|
|
132
|
+
* body buffer. Lines that start with `.` get an extra `.` prepended
|
|
133
|
+
* so the receiver's parser doesn't mistake them for the terminator.
|
|
134
|
+
*
|
|
135
|
+
* Strict CRLF-aware: a line boundary is any of:
|
|
136
|
+
* - start of buffer
|
|
137
|
+
* - byte sequence \r\n (canonical CRLF)
|
|
138
|
+
*
|
|
139
|
+
* Bare LF inside a line is NOT treated as a line boundary, so a body
|
|
140
|
+
* containing `\n` (CVE-2023-51764 smuggling shape) doesn't gain
|
|
141
|
+
* spurious dot-stuffing that would confuse a downstream parser. The
|
|
142
|
+
* upstream caller is expected to either canonicalize or refuse bare-LF
|
|
143
|
+
* via `b.guardSmtpCommand.detectBodySmuggling`.
|
|
144
|
+
*
|
|
145
|
+
* Output guarantees a trailing `\r\n` so the caller can append the
|
|
146
|
+
* `.\r\n` terminator without worrying about whether the body already
|
|
147
|
+
* ended with one.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* var body = Buffer.from(".secret\r\n.\r\nmore\r\n");
|
|
151
|
+
* b.safeSmtp.dotStuff(body).toString("utf8");
|
|
152
|
+
* // → "..secret\r\n..\r\nmore\r\n"
|
|
153
|
+
*/
|
|
154
|
+
function dotStuff(buf) {
|
|
155
|
+
if (!Buffer.isBuffer(buf)) {
|
|
156
|
+
throw new SafeSmtpError("safe-smtp/bad-input",
|
|
157
|
+
"dotStuff: input must be a Buffer");
|
|
158
|
+
}
|
|
159
|
+
if (buf.length === 0) return buf;
|
|
160
|
+
// Worst case: every byte is a line-start dot — 2x length. Pre-allocate
|
|
161
|
+
// upper bound; subarray to actual length at return.
|
|
162
|
+
var out = Buffer.alloc(buf.length * 2);
|
|
163
|
+
var oi = 0;
|
|
164
|
+
// First byte: if `.`, prepend `.` (line-start).
|
|
165
|
+
if (buf[0] === 0x2e /* . */) out[oi++] = 0x2e;
|
|
166
|
+
out[oi++] = buf[0];
|
|
167
|
+
for (var i = 1; i < buf.length; i += 1) {
|
|
168
|
+
out[oi++] = buf[i];
|
|
169
|
+
// Inspect the byte AFTER a canonical \r\n line boundary. If it's
|
|
170
|
+
// `.`, prepend the stuffing dot. Match strictly on the CRLF
|
|
171
|
+
// sequence; bare LF is not a line boundary here.
|
|
172
|
+
if (i >= 1 && buf[i - 1] === 0x0d && buf[i] === 0x0a &&
|
|
173
|
+
i + 1 < buf.length && buf[i + 1] === 0x2e) {
|
|
174
|
+
out[oi++] = 0x2e;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out.subarray(0, oi);
|
|
178
|
+
}
|
|
179
|
+
|
|
124
180
|
module.exports = {
|
|
125
181
|
findDotTerminator: findDotTerminator,
|
|
126
182
|
dotUnstuff: dotUnstuff,
|
|
183
|
+
dotStuff: dotStuff,
|
|
127
184
|
SafeSmtpError: SafeSmtpError,
|
|
128
185
|
};
|
package/lib/safe-url.js
CHANGED
|
@@ -380,8 +380,45 @@ function parse(url, opts) {
|
|
|
380
380
|
return parsed;
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/**
|
|
384
|
+
* @primitive b.safeUrl.format
|
|
385
|
+
* @signature b.safeUrl.format(url)
|
|
386
|
+
* @since 0.10.0
|
|
387
|
+
* @status stable
|
|
388
|
+
*
|
|
389
|
+
* Defensive wrapper around URL formatting that translates the
|
|
390
|
+
* assertion-class throw documented in [CVE-2026-21712](https://nvd.nist.gov/vuln/detail/CVE-2026-21712)
|
|
391
|
+
* (IDN crash via legacy `url.format()`) into a typed
|
|
392
|
+
* `safe-url/format-failed` refusal. Accepts either a string URL or a
|
|
393
|
+
* `URL` instance; returns the canonical string form.
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* var out = b.safeUrl.format("https://example.com/a?q=1");
|
|
397
|
+
* // → "https://example.com/a?q=1"
|
|
398
|
+
*/
|
|
399
|
+
function format(url) {
|
|
400
|
+
try {
|
|
401
|
+
if (url instanceof URL) {
|
|
402
|
+
return url.href;
|
|
403
|
+
}
|
|
404
|
+
if (typeof url !== "string") {
|
|
405
|
+
throw new SafeUrlError("safe-url/format-bad-input",
|
|
406
|
+
"safeUrl.format: url must be a string or URL instance");
|
|
407
|
+
}
|
|
408
|
+
// Constructing URL() is the path that surfaces the IDN-crash on
|
|
409
|
+
// older Node — wrap so the listener never crashes.
|
|
410
|
+
var u = new URL(url); // allow:raw-new-url — safeUrl.format wraps URL ctor for CVE-2026-21712; this IS the safe wrapper. // allow:raw-byte-literal — no byte literal; suppresses cross-detector false-positive from neighboring text
|
|
411
|
+
return u.href;
|
|
412
|
+
} catch (e) {
|
|
413
|
+
if (e && e.isSafeUrlError) throw e;
|
|
414
|
+
throw new SafeUrlError("safe-url/format-failed",
|
|
415
|
+
"safeUrl.format refused: " + ((e && e.message) || String(e)));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
383
419
|
module.exports = {
|
|
384
420
|
parse: parse,
|
|
421
|
+
format: format,
|
|
385
422
|
SafeUrlError: SafeUrlError,
|
|
386
423
|
ALLOW_HTTP_TLS: ALLOW_HTTP_TLS,
|
|
387
424
|
ALLOW_HTTP_ALL: ALLOW_HTTP_ALL,
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.safeVcard
|
|
4
|
+
* @nav Parsers
|
|
5
|
+
* @title Safe vCard
|
|
6
|
+
* @order 126
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Bounded RFC 6350 vCard 4.0 parser. Walks the content-line grammar
|
|
10
|
+
* (`BEGIN:VCARD` ... `END:VCARD`) into a JSON AST that the CardDAV
|
|
11
|
+
* stack stores per-tenant. Compatible with the RFC 2425 / 2426
|
|
12
|
+
* shape that legacy CardDAV clients still emit when they negotiate
|
|
13
|
+
* `VERSION:3.0`; the parser admits both versions and exposes the
|
|
14
|
+
* declared `VERSION` field on the resulting card.
|
|
15
|
+
*
|
|
16
|
+
* Substrate for the contacts storage protocol (`b.mail.dav`).
|
|
17
|
+
*
|
|
18
|
+
* Defense posture mirrors `b.safeIcal` — the vCard grammar shares
|
|
19
|
+
* the line-folding + property-parameter shape with iCalendar but
|
|
20
|
+
* does not carry an RRULE-class amplifier; the equivalent
|
|
21
|
+
* amplifier here is the `PHOTO` / `LOGO` / `SOUND` / `KEY`
|
|
22
|
+
* inline-embedded-binary properties which a hostile vCard can
|
|
23
|
+
* stuff with megabytes of base64 to exhaust storage.
|
|
24
|
+
*
|
|
25
|
+
* Caps:
|
|
26
|
+
*
|
|
27
|
+
* - Total bytes (256 KiB strict / 1 MiB balanced / 4 MiB
|
|
28
|
+
* permissive) — refused before parsing begins.
|
|
29
|
+
* - PHOTO / LOGO / SOUND / KEY inline-embed bytes (1 MiB strict
|
|
30
|
+
* / 4 MiB balanced / 16 MiB permissive) — refused when the
|
|
31
|
+
* declared property value or data: URI body exceeds the cap.
|
|
32
|
+
* - Per-line bytes after unfolding (8 KiB strict / 32 KiB
|
|
33
|
+
* balanced / 128 KiB permissive).
|
|
34
|
+
* - Total cards in a stream (16 strict / 256 balanced / 4096
|
|
35
|
+
* permissive). RFC 6350 §3.2 permits chained BEGIN:VCARD /
|
|
36
|
+
* END:VCARD pairs.
|
|
37
|
+
*
|
|
38
|
+
* Header-injection / control-char defense: refuses NUL, C0 control
|
|
39
|
+
* bytes (other than TAB), and DEL (0x7F) inside property values.
|
|
40
|
+
*
|
|
41
|
+
* Property allowlist: every property name must either appear in the
|
|
42
|
+
* RFC 6350 §6 property registry or carry the `X-` experimental
|
|
43
|
+
* prefix. Unknown bare names are refused.
|
|
44
|
+
*
|
|
45
|
+
* Explicit non-goals (deferred — operator escape hatch noted):
|
|
46
|
+
*
|
|
47
|
+
* - **vCard 4.0 to 3.0 conversion (RFC 6868)** — the parser
|
|
48
|
+
* exposes both shapes via the declared `VERSION`; round-tripping
|
|
49
|
+
* between them happens at the CardDAV layer when an old client
|
|
50
|
+
* requests a 4.0-only card.
|
|
51
|
+
* - **xCard XML / jCard JSON (RFC 6351 / 7095)** — the JSON AST
|
|
52
|
+
* this module emits is convertible to jCard but the framework
|
|
53
|
+
* does not currently ship the canonicalization.
|
|
54
|
+
* - **Vendor extensions** — operator extends via
|
|
55
|
+
* `opts.extraProperties` until the relevant slice lands.
|
|
56
|
+
*
|
|
57
|
+
* @card
|
|
58
|
+
* Bounded RFC 6350 vCard 4.0 parser — caps total bytes, per-card
|
|
59
|
+
* line bytes, PHOTO / LOGO / SOUND / KEY inline-embed bytes, total
|
|
60
|
+
* cards in a stream; refuses NUL / C0 / DEL in values; allowlists
|
|
61
|
+
* property names.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
var C = require("./constants");
|
|
65
|
+
var { defineClass } = require("./framework-error");
|
|
66
|
+
|
|
67
|
+
var SafeVcardError = defineClass("SafeVcardError", { alwaysPermanent: true });
|
|
68
|
+
|
|
69
|
+
var PROFILES = Object.freeze({
|
|
70
|
+
strict: Object.freeze({
|
|
71
|
+
maxBytes: C.BYTES.kib(256),
|
|
72
|
+
maxLineBytes: C.BYTES.kib(8),
|
|
73
|
+
maxEmbedBytes: C.BYTES.mib(1),
|
|
74
|
+
maxCards: 16, // allow:raw-byte-literal — card count cap, not byte size
|
|
75
|
+
maxPropertiesPerCard: 256, // allow:raw-byte-literal — prop count cap, not byte size
|
|
76
|
+
}),
|
|
77
|
+
balanced: Object.freeze({
|
|
78
|
+
maxBytes: C.BYTES.mib(1),
|
|
79
|
+
maxLineBytes: C.BYTES.kib(32),
|
|
80
|
+
maxEmbedBytes: C.BYTES.mib(4),
|
|
81
|
+
maxCards: 256, // allow:raw-byte-literal — card count cap, not byte size
|
|
82
|
+
maxPropertiesPerCard: 1024, // allow:raw-byte-literal — prop count cap, not byte size
|
|
83
|
+
}),
|
|
84
|
+
permissive: Object.freeze({
|
|
85
|
+
maxBytes: C.BYTES.mib(4),
|
|
86
|
+
maxLineBytes: C.BYTES.kib(128),
|
|
87
|
+
maxEmbedBytes: C.BYTES.mib(16),
|
|
88
|
+
maxCards: 4096, // allow:raw-byte-literal — card count cap, not byte size
|
|
89
|
+
maxPropertiesPerCard: 4096, // allow:raw-byte-literal — prop count cap, not byte size
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
94
|
+
hipaa: "strict",
|
|
95
|
+
"pci-dss": "strict",
|
|
96
|
+
gdpr: "strict",
|
|
97
|
+
soc2: "strict",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Property-name allowlist per RFC 6350 §6 (vCard 4.0 property
|
|
101
|
+
// registry) + RFC 2426 §3 (legacy 3.0 properties retained for
|
|
102
|
+
// compatibility) + RFC 6474 (BIRTHPLACE / DEATHPLACE / DEATHDATE) +
|
|
103
|
+
// RFC 6715 (XML / EXPERTISE / HOBBY / INTEREST / ORG-DIRECTORY) +
|
|
104
|
+
// RFC 6473 (KIND extension).
|
|
105
|
+
var KNOWN_PROPERTIES = Object.freeze({
|
|
106
|
+
// General (RFC 6350 §6.1)
|
|
107
|
+
BEGIN: true, END: true, SOURCE: true, KIND: true, XML: true,
|
|
108
|
+
// Identification (RFC 6350 §6.2)
|
|
109
|
+
FN: true, N: true, NICKNAME: true, PHOTO: true, BDAY: true,
|
|
110
|
+
ANNIVERSARY: true, GENDER: true,
|
|
111
|
+
// Delivery addressing (RFC 6350 §6.3)
|
|
112
|
+
ADR: true,
|
|
113
|
+
// Communications (RFC 6350 §6.4)
|
|
114
|
+
TEL: true, EMAIL: true, IMPP: true, LANG: true,
|
|
115
|
+
// Geographical (RFC 6350 §6.5)
|
|
116
|
+
TZ: true, GEO: true,
|
|
117
|
+
// Organizational (RFC 6350 §6.6)
|
|
118
|
+
TITLE: true, ROLE: true, LOGO: true, ORG: true, MEMBER: true,
|
|
119
|
+
RELATED: true,
|
|
120
|
+
// Explanatory (RFC 6350 §6.7)
|
|
121
|
+
CATEGORIES: true, NOTE: true, PRODID: true, REV: true, SOUND: true,
|
|
122
|
+
UID: true, CLIENTPIDMAP: true, URL: true, VERSION: true,
|
|
123
|
+
// Security (RFC 6350 §6.8)
|
|
124
|
+
KEY: true,
|
|
125
|
+
// Calendar (RFC 6350 §6.9)
|
|
126
|
+
FBURL: true, CALADRURI: true, CALURI: true,
|
|
127
|
+
// RFC 6474 — birthplace / deathplace / deathdate
|
|
128
|
+
BIRTHPLACE: true, DEATHPLACE: true, DEATHDATE: true,
|
|
129
|
+
// RFC 6715 — vCard4 extension properties
|
|
130
|
+
EXPERTISE: true, HOBBY: true, INTEREST: true, "ORG-DIRECTORY": true,
|
|
131
|
+
// RFC 2426 legacy — admitted under VERSION:3.0 for round-trip
|
|
132
|
+
// compatibility with older CardDAV clients.
|
|
133
|
+
MAILER: true, AGENT: true, CLASS: true, PROFILE: true, NAME: true,
|
|
134
|
+
LABEL: true, SORT_STRING: true, "SORT-STRING": true,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Properties whose body can carry inline base64 / data: URI bytes and
|
|
138
|
+
// therefore enforce `maxEmbedBytes`.
|
|
139
|
+
var EMBED_PROPERTIES = Object.freeze({
|
|
140
|
+
PHOTO: true, LOGO: true, SOUND: true, KEY: true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @primitive b.safeVcard.parse
|
|
145
|
+
* @signature b.safeVcard.parse(text, opts?)
|
|
146
|
+
* @since 0.9.81
|
|
147
|
+
* @status stable
|
|
148
|
+
* @related b.safeIcal.parse, b.mail.dav.create
|
|
149
|
+
*
|
|
150
|
+
* Parse RFC 6350 vCard 4.0 text into a JSON AST. Returns
|
|
151
|
+
* `{ vcards: [{ version, properties: { FN: [{ params, value }], ... } }, ...] }`.
|
|
152
|
+
*
|
|
153
|
+
* Throws `SafeVcardError` with codes:
|
|
154
|
+
* `safe-vcard/oversize-bytes` /
|
|
155
|
+
* `oversize-line-bytes` / `oversize-cards` /
|
|
156
|
+
* `oversize-properties-per-card` / `oversize-embed` /
|
|
157
|
+
* `missing-vcard` / `unterminated-vcard` /
|
|
158
|
+
* `unknown-property` / `control-char-in-value` /
|
|
159
|
+
* `bad-line` / `bad-input` / `bad-opt`.
|
|
160
|
+
*
|
|
161
|
+
* @opts
|
|
162
|
+
* profile: "strict" | "balanced" | "permissive", // default strict
|
|
163
|
+
* compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2", // -> strict
|
|
164
|
+
* extraProperties: string[], // operator-extended allowlist
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* var ast = b.safeVcard.parse(
|
|
168
|
+
* "BEGIN:VCARD\r\n" +
|
|
169
|
+
* "VERSION:4.0\r\n" +
|
|
170
|
+
* "FN:Alice Example\r\n" +
|
|
171
|
+
* "EMAIL:alice@example.com\r\n" +
|
|
172
|
+
* "TEL;TYPE=cell:+1-555-0100\r\n" +
|
|
173
|
+
* "END:VCARD\r\n"
|
|
174
|
+
* );
|
|
175
|
+
* ast.vcards[0].properties.FN[0].value; // -> "Alice Example"
|
|
176
|
+
*/
|
|
177
|
+
function parse(text, opts) {
|
|
178
|
+
opts = opts || {};
|
|
179
|
+
var caps = _resolveCaps(opts);
|
|
180
|
+
var extraProps = _toSet(opts.extraProperties);
|
|
181
|
+
|
|
182
|
+
if (typeof text !== "string" && !Buffer.isBuffer(text)) {
|
|
183
|
+
throw new SafeVcardError("safe-vcard/bad-input",
|
|
184
|
+
"safeVcard.parse: input must be string or Buffer (got " + typeof text + ")");
|
|
185
|
+
}
|
|
186
|
+
var s = typeof text === "string" ? text : text.toString("utf8");
|
|
187
|
+
var byteLen = Buffer.byteLength(s, "utf8");
|
|
188
|
+
if (byteLen > caps.maxBytes) {
|
|
189
|
+
throw new SafeVcardError("safe-vcard/oversize-bytes",
|
|
190
|
+
"safeVcard.parse: input " + byteLen + " bytes exceeds maxBytes=" + caps.maxBytes);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var lines = _unfold(s, caps);
|
|
194
|
+
var vcards = [];
|
|
195
|
+
var idx = 0;
|
|
196
|
+
while (idx < lines.length) {
|
|
197
|
+
// Skip blank or non-content lines until BEGIN:VCARD.
|
|
198
|
+
while (idx < lines.length && lines[idx].name !== "BEGIN") idx++;
|
|
199
|
+
if (idx >= lines.length) break;
|
|
200
|
+
if (lines[idx].value.toUpperCase() !== "VCARD") {
|
|
201
|
+
throw new SafeVcardError("safe-vcard/missing-vcard",
|
|
202
|
+
"safeVcard.parse: BEGIN line at position " + (idx + 1) +
|
|
203
|
+
" is not VCARD (got '" + lines[idx].value + "')");
|
|
204
|
+
}
|
|
205
|
+
var parsed = _parseVcard(lines, idx, caps, extraProps);
|
|
206
|
+
vcards.push(parsed.card);
|
|
207
|
+
idx = parsed.nextIdx;
|
|
208
|
+
if (vcards.length > caps.maxCards) {
|
|
209
|
+
throw new SafeVcardError("safe-vcard/oversize-cards",
|
|
210
|
+
"safeVcard.parse: stream contains more than maxCards=" + caps.maxCards);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (vcards.length === 0) {
|
|
214
|
+
throw new SafeVcardError("safe-vcard/missing-vcard",
|
|
215
|
+
"safeVcard.parse: no BEGIN:VCARD found");
|
|
216
|
+
}
|
|
217
|
+
return { vcards: vcards };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @primitive b.safeVcard.compliancePosture
|
|
222
|
+
* @signature b.safeVcard.compliancePosture(name)
|
|
223
|
+
* @since 0.9.81
|
|
224
|
+
* @status stable
|
|
225
|
+
* @related b.safeVcard.parse
|
|
226
|
+
*
|
|
227
|
+
* Map a compliance-posture name to its profile. Returns the profile
|
|
228
|
+
* string for a known posture, `null` for unknown names.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* b.safeVcard.compliancePosture("hipaa"); // -> "strict"
|
|
232
|
+
* b.safeVcard.compliancePosture("loose"); // -> null
|
|
233
|
+
*/
|
|
234
|
+
function compliancePosture(name) {
|
|
235
|
+
return COMPLIANCE_POSTURES[name] || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---- Internal ----
|
|
239
|
+
|
|
240
|
+
function _resolveCaps(opts) {
|
|
241
|
+
var name = "strict";
|
|
242
|
+
if (typeof opts.profile === "string") {
|
|
243
|
+
name = opts.profile;
|
|
244
|
+
} else if (typeof opts.compliancePosture === "string") {
|
|
245
|
+
name = COMPLIANCE_POSTURES[opts.compliancePosture] || "strict";
|
|
246
|
+
}
|
|
247
|
+
var caps = PROFILES[name];
|
|
248
|
+
if (!caps) {
|
|
249
|
+
throw new SafeVcardError("safe-vcard/bad-opt",
|
|
250
|
+
"safeVcard.parse: unknown profile '" + name +
|
|
251
|
+
"' (expected strict|balanced|permissive)");
|
|
252
|
+
}
|
|
253
|
+
return caps;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function _toSet(arr) {
|
|
257
|
+
var set = Object.create(null);
|
|
258
|
+
if (!Array.isArray(arr)) return set;
|
|
259
|
+
for (var i = 0; i < arr.length; i++) {
|
|
260
|
+
if (typeof arr[i] === "string") set[arr[i].toUpperCase()] = true;
|
|
261
|
+
}
|
|
262
|
+
return set;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _unfold(s, caps) {
|
|
266
|
+
// RFC 6350 §3.2 — line unfolding is identical to RFC 5545 §3.1.
|
|
267
|
+
var raw = s.replace(/\r\n?|\n/g, "\n").split("\n");
|
|
268
|
+
var unfolded = [];
|
|
269
|
+
for (var i = 0; i < raw.length; i++) {
|
|
270
|
+
var line = raw[i];
|
|
271
|
+
if (line.length === 0) continue;
|
|
272
|
+
var firstChar = line.charCodeAt(0);
|
|
273
|
+
if (firstChar === 0x20 || firstChar === 0x09) { // allow:raw-byte-literal — SPACE / HTAB fold markers per RFC 6350 §3.2
|
|
274
|
+
if (unfolded.length === 0) {
|
|
275
|
+
throw new SafeVcardError("safe-vcard/bad-line",
|
|
276
|
+
"safeVcard.parse: continuation line before any content line");
|
|
277
|
+
}
|
|
278
|
+
unfolded[unfolded.length - 1] += line.slice(1);
|
|
279
|
+
} else {
|
|
280
|
+
unfolded.push(line);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
var parsed = [];
|
|
284
|
+
for (var j = 0; j < unfolded.length; j++) {
|
|
285
|
+
var u = unfolded[j];
|
|
286
|
+
if (Buffer.byteLength(u, "utf8") > caps.maxLineBytes) {
|
|
287
|
+
throw new SafeVcardError("safe-vcard/oversize-line-bytes",
|
|
288
|
+
"safeVcard.parse: unfolded line " + (j + 1) +
|
|
289
|
+
" exceeds maxLineBytes=" + caps.maxLineBytes);
|
|
290
|
+
}
|
|
291
|
+
parsed.push(_parseContentLine(u));
|
|
292
|
+
}
|
|
293
|
+
return parsed;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function _parseContentLine(line) {
|
|
297
|
+
var colonIdx = _findUnquotedColon(line);
|
|
298
|
+
if (colonIdx < 0) {
|
|
299
|
+
throw new SafeVcardError("safe-vcard/bad-line",
|
|
300
|
+
"safeVcard.parse: content line missing ':' separator: " + _preview(line));
|
|
301
|
+
}
|
|
302
|
+
var head = line.slice(0, colonIdx);
|
|
303
|
+
var value = line.slice(colonIdx + 1);
|
|
304
|
+
|
|
305
|
+
for (var k = 0; k < value.length; k++) {
|
|
306
|
+
var cc = value.charCodeAt(k);
|
|
307
|
+
if ((cc < 0x20 && cc !== 0x09) || cc === 0x7F) { // allow:raw-byte-literal — C0 + DEL refusal
|
|
308
|
+
throw new SafeVcardError("safe-vcard/control-char-in-value",
|
|
309
|
+
"safeVcard.parse: control char 0x" + cc.toString(16) +
|
|
310
|
+
" in property value (header-injection defense)");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// RFC 6350 §3.3 — property name may be prefixed by an optional
|
|
315
|
+
// group token (group "."). Strip and retain the group.
|
|
316
|
+
var segs = _splitUnquoted(head, ";");
|
|
317
|
+
var nameRaw = segs[0];
|
|
318
|
+
var group = null;
|
|
319
|
+
var dotIdx = nameRaw.indexOf(".");
|
|
320
|
+
if (dotIdx >= 0) {
|
|
321
|
+
group = nameRaw.slice(0, dotIdx);
|
|
322
|
+
nameRaw = nameRaw.slice(dotIdx + 1);
|
|
323
|
+
}
|
|
324
|
+
var name = nameRaw.toUpperCase();
|
|
325
|
+
var params = Object.create(null);
|
|
326
|
+
for (var p = 1; p < segs.length; p++) {
|
|
327
|
+
var seg = segs[p];
|
|
328
|
+
var eq = seg.indexOf("=");
|
|
329
|
+
if (eq < 0) {
|
|
330
|
+
throw new SafeVcardError("safe-vcard/bad-line",
|
|
331
|
+
"safeVcard.parse: malformed parameter '" + seg + "'");
|
|
332
|
+
}
|
|
333
|
+
var pname = seg.slice(0, eq).toUpperCase();
|
|
334
|
+
var pvalue = seg.slice(eq + 1);
|
|
335
|
+
if (pname === "__proto__" || pname === "constructor" || pname === "prototype") continue;
|
|
336
|
+
if (params[pname]) {
|
|
337
|
+
params[pname].push(_stripDoubleQuotes(pvalue));
|
|
338
|
+
} else {
|
|
339
|
+
params[pname] = [_stripDoubleQuotes(pvalue)];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return { name: name, group: group, params: params, value: value };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function _findUnquotedColon(line) {
|
|
346
|
+
var inQ = false;
|
|
347
|
+
for (var i = 0; i < line.length; i++) {
|
|
348
|
+
var c = line.charCodeAt(i);
|
|
349
|
+
if (c === 0x22) { inQ = !inQ; continue; } // allow:raw-byte-literal — DQUOTE per RFC 6350 §3.3
|
|
350
|
+
if (c === 0x3A && !inQ) return i; // allow:raw-byte-literal — colon separator per RFC 6350 §3.3
|
|
351
|
+
}
|
|
352
|
+
return -1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function _splitUnquoted(s, sep) {
|
|
356
|
+
var out = [];
|
|
357
|
+
var inQ = false;
|
|
358
|
+
var start = 0;
|
|
359
|
+
for (var i = 0; i < s.length; i++) {
|
|
360
|
+
var c = s.charAt(i);
|
|
361
|
+
if (c === '"') { inQ = !inQ; continue; }
|
|
362
|
+
if (c === sep && !inQ) {
|
|
363
|
+
out.push(s.slice(start, i));
|
|
364
|
+
start = i + 1;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
out.push(s.slice(start));
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function _stripDoubleQuotes(s) {
|
|
372
|
+
if (s.length >= 2 && s.charAt(0) === '"' && s.charAt(s.length - 1) === '"') {
|
|
373
|
+
return s.slice(1, -1);
|
|
374
|
+
}
|
|
375
|
+
return s;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function _parseVcard(lines, startIdx, caps, extraProps) {
|
|
379
|
+
var properties = Object.create(null);
|
|
380
|
+
var version = null;
|
|
381
|
+
var propertyCount = 0;
|
|
382
|
+
var i = startIdx + 1;
|
|
383
|
+
while (i < lines.length) {
|
|
384
|
+
var ln = lines[i];
|
|
385
|
+
if (ln.name === "END") {
|
|
386
|
+
if (ln.value.toUpperCase() !== "VCARD") {
|
|
387
|
+
throw new SafeVcardError("safe-vcard/unterminated-vcard",
|
|
388
|
+
"safeVcard.parse: BEGIN:VCARD closed by END:" + ln.value);
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
card: { version: version || "4.0", properties: properties },
|
|
392
|
+
nextIdx: i + 1,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (ln.name === "BEGIN") {
|
|
396
|
+
throw new SafeVcardError("safe-vcard/bad-line",
|
|
397
|
+
"safeVcard.parse: nested BEGIN inside VCARD (vCard does not support sub-components)");
|
|
398
|
+
}
|
|
399
|
+
var pn = ln.name;
|
|
400
|
+
if (!KNOWN_PROPERTIES[pn] && !extraProps[pn] && pn.indexOf("X-") !== 0) {
|
|
401
|
+
throw new SafeVcardError("safe-vcard/unknown-property",
|
|
402
|
+
"safeVcard.parse: unknown property '" + pn +
|
|
403
|
+
"' (extend via opts.extraProperties or use X- prefix)");
|
|
404
|
+
}
|
|
405
|
+
if (pn === "VERSION") version = ln.value;
|
|
406
|
+
if (EMBED_PROPERTIES[pn]) {
|
|
407
|
+
// RFC 6350 §6.2.4 — PHOTO/LOGO/SOUND/KEY values can be a URI
|
|
408
|
+
// (including data:) or a base64 blob (3.0-style). Compute the
|
|
409
|
+
// byte length of the decoded form when it is data: or pure
|
|
410
|
+
// base64; otherwise apply the raw-string byte length.
|
|
411
|
+
var embedBytes = _embedByteLength(ln.value);
|
|
412
|
+
if (embedBytes > caps.maxEmbedBytes) {
|
|
413
|
+
throw new SafeVcardError("safe-vcard/oversize-embed",
|
|
414
|
+
"safeVcard.parse: " + pn + " embed " + embedBytes +
|
|
415
|
+
" bytes exceeds maxEmbedBytes=" + caps.maxEmbedBytes);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
propertyCount += 1;
|
|
419
|
+
if (propertyCount > caps.maxPropertiesPerCard) {
|
|
420
|
+
throw new SafeVcardError("safe-vcard/oversize-properties-per-card",
|
|
421
|
+
"safeVcard.parse: property count exceeds maxPropertiesPerCard=" +
|
|
422
|
+
caps.maxPropertiesPerCard);
|
|
423
|
+
}
|
|
424
|
+
if (pn === "__proto__" || pn === "constructor" || pn === "prototype") {
|
|
425
|
+
i += 1;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (!properties[pn]) properties[pn] = [];
|
|
429
|
+
properties[pn].push({
|
|
430
|
+
group: ln.group,
|
|
431
|
+
params: ln.params,
|
|
432
|
+
value: ln.value,
|
|
433
|
+
});
|
|
434
|
+
i += 1;
|
|
435
|
+
}
|
|
436
|
+
throw new SafeVcardError("safe-vcard/unterminated-vcard",
|
|
437
|
+
"safeVcard.parse: BEGIN:VCARD never closed (missing END)");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function _embedByteLength(value) {
|
|
441
|
+
// data:<mime>;base64,<payload> — decoded bytes are (3/4) * payload
|
|
442
|
+
// length (rounding for padding).
|
|
443
|
+
var dataMatch = /^data:[^;,]*;base64,(.*)$/i.exec(value);
|
|
444
|
+
if (dataMatch) {
|
|
445
|
+
var payload = dataMatch[1].replace(/\s+/g, "");
|
|
446
|
+
return Math.floor(payload.length * 3 / 4); // allow:raw-byte-literal — base64 3/4 decode ratio per RFC 4648 §4
|
|
447
|
+
}
|
|
448
|
+
// ENCODING=b / ENCODING=BASE64 puts the raw base64 in the value
|
|
449
|
+
// directly (the param is parsed separately upstream; we do not have
|
|
450
|
+
// access here, so check whether the payload is base64-shaped).
|
|
451
|
+
if (/^[A-Za-z0-9+/=\r\n\t ]+$/.test(value) && value.length > 32) { // allow:raw-byte-literal — heuristic threshold for base64 detection
|
|
452
|
+
var compact = value.replace(/\s+/g, "");
|
|
453
|
+
if (compact.length > 0 && compact.length % 4 === 0) {
|
|
454
|
+
return Math.floor(compact.length * 3 / 4); // allow:raw-byte-literal — base64 3/4 decode ratio per RFC 4648 §4
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return Buffer.byteLength(value, "utf8");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function _preview(s) {
|
|
461
|
+
if (typeof s !== "string") s = String(s);
|
|
462
|
+
return s.length > 64 ? s.slice(0, 64) + "..." : s; // allow:raw-byte-literal — log-preview length cap
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
module.exports = {
|
|
466
|
+
parse: parse,
|
|
467
|
+
compliancePosture: compliancePosture,
|
|
468
|
+
PROFILES: PROFILES,
|
|
469
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
470
|
+
KNOWN_PROPERTIES: KNOWN_PROPERTIES,
|
|
471
|
+
EMBED_PROPERTIES: EMBED_PROPERTIES,
|
|
472
|
+
SafeVcardError: SafeVcardError,
|
|
473
|
+
};
|
|
@@ -207,20 +207,39 @@ function verify(assetPath, signaturePath, pubkeyPem) {
|
|
|
207
207
|
// single allocation peak, not the 2× peak that Buffer.concat([...chunks])
|
|
208
208
|
// produces. 64 KiB chunks match the framework's hash-while-streaming
|
|
209
209
|
// convention elsewhere.
|
|
210
|
+
//
|
|
211
|
+
// CRYPTO-2 hardening (v0.9.58): fstat the asset BEFORE the read loop
|
|
212
|
+
// for every alg path, clamp every readSync to (assetStat.size -
|
|
213
|
+
// fullOff), and reject if the final fullOff diverges from
|
|
214
|
+
// assetStat.size. A grow-during-read race (writer appends as we
|
|
215
|
+
// hash) previously fed extra bytes to the hashers but not to the
|
|
216
|
+
// pre-sized fullBuf — the returned sha3_512 then didn't match what
|
|
217
|
+
// signature-verify or the operator's later byte-set compare saw.
|
|
218
|
+
// The clamp + final-equality refusal forces every hash + verify byte
|
|
219
|
+
// to come from the same {0..assetStat.size} range fixed at open
|
|
220
|
+
// time.
|
|
221
|
+
var assetStat = nodeFs.fstatSync(assetFd);
|
|
210
222
|
var sha256 = nodeCrypto.createHash("sha256");
|
|
211
223
|
var sha3 = nodeCrypto.createHash("sha3-512");
|
|
212
224
|
var verifier = (alg === "ecdsa-p384") ? nodeCrypto.createVerify("sha3-512") : null;
|
|
213
225
|
var fullBuf = null;
|
|
214
226
|
var fullOff = 0;
|
|
215
227
|
if (verifier === null) {
|
|
216
|
-
var assetStat = nodeFs.fstatSync(assetFd);
|
|
217
228
|
fullBuf = Buffer.allocUnsafe(assetStat.size);
|
|
218
229
|
}
|
|
219
230
|
|
|
220
231
|
try {
|
|
221
232
|
var chunk = Buffer.allocUnsafe(64 * 1024); // allow:raw-byte-literal — module is zero-dep by contract; cannot import C.BYTES
|
|
222
233
|
while (true) {
|
|
223
|
-
var
|
|
234
|
+
var remaining = assetStat.size - fullOff;
|
|
235
|
+
if (remaining <= 0) break;
|
|
236
|
+
// Clamp the read to the remaining bytes the verifier and hashers
|
|
237
|
+
// are allowed to see. Without this, a concurrent appender grows
|
|
238
|
+
// the file under us and the readSync returns more bytes than the
|
|
239
|
+
// fullBuf was sized for.
|
|
240
|
+
var capped = chunk.length; // allow:raw-byte-literal — buffer length is the read upper bound
|
|
241
|
+
if (remaining < capped) capped = remaining;
|
|
242
|
+
var n = nodeFs.readSync(assetFd, chunk, 0, capped, null);
|
|
224
243
|
if (n === 0) break;
|
|
225
244
|
var slice = chunk.subarray(0, n);
|
|
226
245
|
sha256.update(slice);
|
|
@@ -228,12 +247,22 @@ function verify(assetPath, signaturePath, pubkeyPem) {
|
|
|
228
247
|
if (verifier) verifier.update(slice);
|
|
229
248
|
if (fullBuf) {
|
|
230
249
|
slice.copy(fullBuf, fullOff);
|
|
231
|
-
fullOff += n;
|
|
232
250
|
}
|
|
251
|
+
fullOff += n;
|
|
233
252
|
}
|
|
234
253
|
} finally {
|
|
235
254
|
nodeFs.closeSync(assetFd);
|
|
236
255
|
}
|
|
256
|
+
// Final byte-count gate. If fullOff != assetStat.size, the file was
|
|
257
|
+
// truncated under us (read fewer bytes than stat said) or grew
|
|
258
|
+
// beyond what the clamp let through. Both cases mean the hashers
|
|
259
|
+
// and verifier saw a different byte set than the on-disk file.
|
|
260
|
+
if (fullOff !== assetStat.size) {
|
|
261
|
+
throw new Error("standalone-verifier.verify: asset '" + assetPath +
|
|
262
|
+
"' changed size during read (expected " + assetStat.size +
|
|
263
|
+
" bytes per fstat, read " + fullOff +
|
|
264
|
+
" bytes) — refusing to return a hash that may not match the on-disk file");
|
|
265
|
+
}
|
|
237
266
|
|
|
238
267
|
var sha256Hex = sha256.digest("hex");
|
|
239
268
|
var sha3Hex = sha3.digest("hex");
|