@blamejs/pki 0.8.7 → 0.8.9

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.
@@ -8,6 +8,7 @@ var constants = require("./constants");
8
8
  var schema = require("./schema-engine");
9
9
  var guard = require("./guard-all");
10
10
  var intrinsic = require("./guard-intrinsic");
11
+ var frameworkError = require("./framework-error");
11
12
  var _hasOwn = intrinsic.hasOwn;
12
13
  var _forEach = intrinsic.forEach;
13
14
  var _map = intrinsic.map;
@@ -59,26 +60,109 @@ var ct = require("./ct");
59
60
  var _PEM_BEGIN = "-----BEGIN ";
60
61
  var _PEM_END = "-----END ";
61
62
  var _PEM_DASHES = "-----";
63
+ var _BOM_LATIN1 = intrinsic.fromCharCode(0xef) + intrinsic.fromCharCode(0xbb) + intrinsic.fromCharCode(0xbf);
64
+ var _BOM_UTF16 = intrinsic.fromCharCode(0xfeff);
65
+
66
+ /** @internal How many characters of a byte-order mark open `text`. The mark reaches the scanner in
67
+ * whichever form the caller's decode produced: the three characters EF BB BF from a latin1 read or
68
+ * a Buffer, and the single U+FEFF from a UTF-8 one. Knowing one form and not the other means
69
+ * walking past the first boundary line of a file written in the other, which drops its first
70
+ * object without saying so. */
71
+ function _bomWidth(text) {
72
+ if (_strSlice(text, 0, 3) === _BOM_LATIN1) return 3;
73
+ return _charAt(text, 0) === _BOM_UTF16 ? 1 : 0;
74
+ }
75
+
76
+ /** @internal The offset just past the end of the line `at` sits on, and the offset of the next
77
+ * line's first character. A boundary is recognized at a line start only, which is what keeps prose
78
+ * quoting one mid-sentence from being read as a block (RFC 7468 sec. 2). */
79
+ function _lineEnd(text, at) {
80
+ var i = at;
81
+ while (i < text.length) {
82
+ var c = _charCodeAt(text, i);
83
+ if (c === 13 || c === 10) return i;
84
+ i += 1;
85
+ }
86
+ return i;
87
+ }
88
+ function _nextLineStart(text, lineEnd) {
89
+ if (lineEnd >= text.length) return text.length;
90
+ if (_charCodeAt(text, lineEnd) === 13 && _charCodeAt(text, lineEnd + 1) === 10) return lineEnd + 2;
91
+ return lineEnd + 1;
92
+ }
93
+
94
+ /** @internal RFC 7468 sec. 3: `labelchar = %x21-2C / %x2E-7E`, every printable character except
95
+ * the space and the hyphen-minus, and `label = [ labelchar *( ["-" / SP] labelchar ) ]`. So a
96
+ * label may be empty, may carry a hyphen or a space BETWEEN two of its characters, and may not
97
+ * open or close with one or carry two in a row. Reading a narrower set makes a conforming file
98
+ * look like it holds no blocks. */
99
+ function _isLabelChar(c) { return (c >= 0x21 && c <= 0x2c) || (c >= 0x2e && c <= 0x7e); }
100
+ function _isRfc7468Label(text, from, to) {
101
+ if (from === to) return true;
102
+ var prevWasChar = false;
103
+ for (var i = from; i < to; i++) {
104
+ var c = _charCodeAt(text, i);
105
+ if (_isLabelChar(c)) { prevWasChar = true; continue; }
106
+ if (c !== 0x2d && c !== 0x20) return false;
107
+ if (!prevWasChar) return false;
108
+ if (i + 1 === to) return false;
109
+ prevWasChar = false;
110
+ }
111
+ return prevWasChar;
112
+ }
113
+
114
+ /** @internal A boundary line: the keyword, a label, five hyphens, and the end of the line. RFC 7468
115
+ * sec. 3's parsing grammar ends a line with `W* eol`, so whitespace after the hyphens belongs to
116
+ * the line ending. Every ASCII whitespace character is taken that way, space, tab, vertical tab and
117
+ * form feed alike, because the cost of the two readings is not symmetric: treating one of them as
118
+ * content makes the object behind that boundary invisible, so a reader of one object hands back the
119
+ * first object of a file holding two and says nothing about the second. Anything that is not
120
+ * whitespace after the hyphens is content, and the line is prose. The label is everything between
121
+ * the keyword and the hyphens, because a label may itself carry a hyphen and only the line's end
122
+ * says where it stops. */
123
+ function _boundaryLabel(text, start, end, keyword) {
124
+ var p = start + keyword.length;
125
+ if (p > end || _strSlice(text, start, p) !== keyword) return null;
126
+ var lineEnd = end;
127
+ while (lineEnd > p) {
128
+ var w = _charCodeAt(text, lineEnd - 1);
129
+ if (w !== 0x20 && w !== 0x09 && w !== 0x0b && w !== 0x0c) break;
130
+ lineEnd -= 1;
131
+ }
132
+ var labelEnd = lineEnd - 5;
133
+ if (labelEnd < p || _strSlice(text, labelEnd, lineEnd) !== _PEM_DASHES) return null;
134
+ if (!_isRfc7468Label(text, p, labelEnd)) return null;
135
+ return _strSlice(text, p, labelEnd);
136
+ }
62
137
 
138
+ /** @internal One forward pass over the lines from `from`. The END is matched as a boundary line in
139
+ * its own right and its label is read off that line, rather than searching for the BEGIN label's
140
+ * own marker: searching for it runs a block's body on to the next matching END, swallowing whatever
141
+ * lies between, and rescanning from `bi + 1` after a miss walks the remainder of the file once per
142
+ * BEGIN line, which is quadratic on text a parse door accepts from anywhere. */
63
143
  function _findPemBlock(text, from) {
64
- var searchFrom = from;
65
- for (;;) {
66
- var bi = _strIndexOf(text, _PEM_BEGIN, searchFrom);
67
- if (bi === -1) return null;
68
- var p = bi + _PEM_BEGIN.length, ls = p;
69
- while (p < text.length) {
70
- var c = _charCodeAt(text, p);
71
- if (!((c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 32)) break;
72
- p += 1;
144
+ var at = from;
145
+ /** @internal A byte-order mark is not part of the first line's content, so a boundary on that
146
+ * line is still at a line start. */
147
+ if (at === 0) at = _bomWidth(text);
148
+ while (at < text.length) {
149
+ var end = _lineEnd(text, at);
150
+ var label = _boundaryLabel(text, at, end, _PEM_BEGIN);
151
+ if (label === null) { at = _nextLineStart(text, end); continue; }
152
+ var bodyStart = _nextLineStart(text, end);
153
+ var scan = bodyStart;
154
+ while (scan < text.length) {
155
+ var lineEnd = _lineEnd(text, scan);
156
+ var endLabel = _boundaryLabel(text, scan, lineEnd, _PEM_END);
157
+ if (endLabel !== null) {
158
+ return { index: at, label: label, endLabel: endLabel,
159
+ body: _strSlice(text, bodyStart, scan), end: _nextLineStart(text, lineEnd) };
160
+ }
161
+ scan = _nextLineStart(text, lineEnd);
73
162
  }
74
- if (p === ls || _strSlice(text, p, p + 5) !== _PEM_DASHES) { searchFrom = bi + 1; continue; }
75
- var label = _strSlice(text, ls, p);
76
- var bodyStart = p + 5;
77
- var endMarker = _PEM_END + label + _PEM_DASHES;
78
- var ei = _strIndexOf(text, endMarker, bodyStart);
79
- if (ei === -1) { searchFrom = bi + 1; continue; }
80
- return { index: bi, label: label, body: _strSlice(text, bodyStart, ei), end: ei + endMarker.length };
163
+ return null;
81
164
  }
165
+ return null;
82
166
  }
83
167
 
84
168
  function _stripB64Whitespace(s) {
@@ -110,11 +194,45 @@ function pemDecode(text, label, PemError) {
110
194
  });
111
195
  var m = _findPemBlock(text, 0);
112
196
  if (!m) throw new PemError("pem/no-block", "no PEM block found");
113
- if (label && m.label !== label) throw new PemError("pem/label-mismatch", "expected " + _stringify(label) + " block, got " + _stringify(m.label));
197
+ if (m.endLabel !== m.label) {
198
+ throw new PemError("pem/label-mismatch", "a block opened as " + _stringify(m.label) + " is closed as " + _stringify(m.endLabel) + " (RFC 7468 sec. 2 requires the same label on both boundaries)");
199
+ }
200
+ /** @internal `null` opts out of the label check. The empty string is a LABEL, which RFC 7468
201
+ * sec. 3 writes as `[ labelchar *( ... ) ]`, so a caller asking for it is asking for the empty
202
+ * label and not asking for nothing. */
203
+ if (label !== null && label !== undefined && m.label !== label) {
204
+ throw new PemError("pem/label-mismatch", "expected " + _stringify(label) + " block, got " + _stringify(m.label));
205
+ }
206
+ var rest = _countBlocksFrom(text, m.end);
207
+ if (rest.opened !== null) {
208
+ throw new PemError("pem/unterminated-block", "a block opened as " + _stringify(rest.opened) +
209
+ " after the first object is never closed, so what the input holds cannot be read; this verb reads one object");
210
+ }
211
+ if (rest.closed > 0) {
212
+ throw new PemError("pem/multiple-blocks", "the input holds " + (rest.closed + 1) +
213
+ " PEM blocks and this verb reads one object; read a file of several objects with pki.schema.pem.decodeBundle");
214
+ }
114
215
  var b64 = _stripB64Whitespace(m.body);
115
216
  return guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body");
116
217
  }
117
218
 
219
+ /** @internal What follows `from`: how many further blocks close, capped at the bundle ceiling so
220
+ * the count in a refusal is bounded, and the label of a block that opens and never closes. A
221
+ * trailing boundary with no end is an object too, and reading past it would take the first block
222
+ * and report nothing about what the rest of the file was trying to be. Each scan resumes where the
223
+ * last block ended, so the walk over the remainder is linear in the text rather than per block. */
224
+ function _countBlocksFrom(text, from) {
225
+ var n = 0, at = from, m;
226
+ while (n < constants.LIMITS.PEM_MAX_BLOCKS) {
227
+ m = _findPemBlockOrOpen(text, at);
228
+ if (m === null) break;
229
+ if (m.unterminated) return { closed: n, opened: m.label };
230
+ n++;
231
+ at = m.end;
232
+ }
233
+ return { closed: n, opened: null };
234
+ }
235
+
118
236
  function pemDecodeAll(text, label, PemError) {
119
237
  label = label || "CERTIFICATE";
120
238
  text = guard.text.decode(text, constants.LIMITS.PEM_MAX_BYTES, PemError, {
@@ -124,6 +242,9 @@ function pemDecodeAll(text, label, PemError) {
124
242
  var lastEnd = 0, m;
125
243
  while ((m = _findPemBlock(text, lastEnd)) !== null) {
126
244
  if (_hasNonWhitespace(_strSlice(text, lastEnd, m.index))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted around PEM blocks (RFC 8555 sec. 9.1)");
245
+ if (m.endLabel !== m.label) {
246
+ throw new PemError("pem/label-mismatch", "a block opened as " + _stringify(m.label) + " is closed as " + _stringify(m.endLabel) + " (RFC 7468 sec. 2 requires the same label on both boundaries)");
247
+ }
127
248
  if (m.label !== label) throw new PemError("pem/label-mismatch", "expected " + _stringify(label) + " block, got " + _stringify(m.label));
128
249
  var b64 = _stripB64Whitespace(m.body);
129
250
  _push(blocks, guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body"));
@@ -157,29 +278,173 @@ function pemEncode(der, label, PemError) {
157
278
  return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
158
279
  }
159
280
 
160
- function _keepBase64(s) {
161
- var out = "", runStart = 0, i;
162
- for (i = 0; i < s.length; i++) {
163
- var c = _charCodeAt(s, i);
164
- if (!((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 43 || c === 47 || c === 61)) {
165
- if (i > runStart) out += _strSlice(s, runStart, i);
166
- runStart = i + 1;
281
+ /** @internal The RFC 1421 headers an OpenSSL traditional encrypted key carries between the
282
+ * boundary and the body. They are not base64, so a reader that does not know them reports a
283
+ * transfer fault and sends the operator looking for the wrong thing. */
284
+ function _hasLegacyHeaders(body) {
285
+ var at = 0;
286
+ while (at < body.length) {
287
+ var end = _lineEnd(body, at);
288
+ var line = _strSlice(body, at, end);
289
+ if (_strSlice(line, 0, 10) === "Proc-Type:" || _strSlice(line, 0, 9) === "DEK-Info:") return true;
290
+ if (line !== "" && _strIndexOf(line, ":") === -1) return false;
291
+ at = _nextLineStart(body, end);
292
+ }
293
+ return false;
294
+ }
295
+
296
+ /** @internal A bound a caller may tighten and may not raise, which is how every other cap in this
297
+ * file reads: the ceiling is the toolkit's and the floor is the caller's business. */
298
+ function _blockCap(given, ceiling, what, PemError) {
299
+ if (given === undefined || given === null) return ceiling;
300
+ if (typeof given !== "number" || !intrinsic.isSafeInteger(given) || given < 1) {
301
+ throw new PemError("pem/bad-input", what + " must be a positive whole number");
302
+ }
303
+ if (given > ceiling) {
304
+ throw new PemError("pem/bad-input", what + " may only tighten the " + ceiling + " this toolkit reads, not raise it");
305
+ }
306
+ return given;
307
+ }
308
+
309
+ /** @internal Reads every encapsulated block of one text (RFC 7468 sec. 2). Explanatory text before,
310
+ * between and after the blocks is text, which is the difference between this and the sec. 9.1 chain
311
+ * profile `pemDecodeAll` enforces. Each row carries the label that named the object, because the
312
+ * label is the only thing separating a CERTIFICATE from a TRUSTED CERTIFICATE or a PRIVATE KEY, and
313
+ * the byte offset it started at, so a fault in a file of hundreds names the one object it is about. */
314
+ function pemDecodeBundle(text, opts, PemError, detect) {
315
+ opts = opts || {};
316
+ var maxBlocks = _blockCap(opts.maxBlocks, constants.LIMITS.PEM_MAX_BLOCKS, "maxBlocks", PemError);
317
+ var maxDecoded = _blockCap(opts.maxDecodedBytes, constants.LIMITS.PEM_MAX_DECODED_BYTES, "maxDecodedBytes", PemError);
318
+ var route = opts.route === true;
319
+ text = guard.text.decode(text, constants.LIMITS.PEM_MAX_BYTES, PemError, {
320
+ charset: "latin1", tooLarge: "pem/too-large", badInput: "pem/bad-input", label: "PEM input",
321
+ });
322
+
323
+ var rows = [], at = 0, decoded = 0;
324
+ for (;;) {
325
+ var m = _findPemBlockOrOpen(text, at);
326
+ if (m === null) break;
327
+ if (m.unterminated) {
328
+ throw new PemError("pem/unterminated-block", "a block opened as " + _stringify(m.label) +
329
+ " at byte " + m.index + " is never closed (RFC 7468 sec. 2)");
330
+ }
331
+ if (m.endLabel !== m.label) {
332
+ throw new PemError("pem/end-mismatch", "the block at byte " + m.index + " is opened as " +
333
+ _stringify(m.label) + " and closed as " + _stringify(m.endLabel) + " (RFC 7468 sec. 2)");
334
+ }
335
+ if (rows.length === maxBlocks) {
336
+ throw new PemError("pem/too-many-blocks", "the file carries more than the " + maxBlocks + " objects this read admits");
337
+ }
338
+ if (_hasLegacyHeaders(m.body)) {
339
+ throw new PemError("pem/legacy-headers", "the object at byte " + m.index +
340
+ " carries RFC 1421 Proc-Type and DEK-Info headers, so its body is encrypted under a passphrase this reader does not take");
341
+ }
342
+ var b64 = _stripB64Whitespace(m.body);
343
+ var der = guard.encoding.base64(b64, null, function (c, msg) {
344
+ return new PemError(c, "object " + rows.length + " at byte " + m.index + ": " + msg);
345
+ }, "pem/bad-base64", "PEM base64 body");
346
+ decoded += der.length;
347
+ if (decoded > maxDecoded) {
348
+ throw new PemError("pem/too-large", "the objects of this file decode to more than the " +
349
+ maxDecoded + " bytes this read admits");
167
350
  }
351
+ var row = { index: rows.length, offset: m.index, label: m.label, der: der };
352
+ if (route) {
353
+ var name = detect(der, m.label);
354
+ row.format = _labelFormat(m.label, name, m.index, PemError);
355
+ }
356
+ _push(rows, row);
357
+ at = m.end;
358
+ }
359
+ if (rows.length === 0) throw new PemError("pem/no-block", "no PEM block found");
360
+ return rows;
361
+ }
362
+
363
+ /** @internal What the label claims, against what the bytes are. Deciding silently is the case
364
+ * RFC 7468 sec. 2's security considerations point at, so a disagreement is a verdict. The two
365
+ * private-key structures RFC 5958 defines each have their own label (RFC 7468 sec. 10 and
366
+ * sec. 11) and their own detector, so neither is read as the other. */
367
+ function _labelFormat(label, detected, offset, PemError) {
368
+ var claimed = PEM_LABEL_FORMAT[label];
369
+ if (claimed === undefined) return null;
370
+ if (detected !== claimed) {
371
+ throw new PemError("pem/label-structure-mismatch", "the object at byte " + offset + " is labeled " +
372
+ _stringify(label) + ", which names the " + claimed + " structure, and its bytes are " +
373
+ (detected === null ? "no structure this toolkit reads" : "the " + detected + " structure"));
374
+ }
375
+ return claimed;
376
+ }
377
+
378
+ /** @internal The RFC 7468 sec. 5 to sec. 13 labels this toolkit has a parser for. A label outside
379
+ * it is a known object of an unknown kind, which is a row rather than a refusal. The legacy labels
380
+ * sec. 5, sec. 6 and sec. 8 say a generator SHOULD NOT emit are deliberately absent. */
381
+ var PEM_LABEL_FORMAT = intrinsic.assign(intrinsic.create(null), {
382
+ "CERTIFICATE": "x509",
383
+ "X509 CRL": "crl",
384
+ "CERTIFICATE REQUEST": "csr",
385
+ "NEW CERTIFICATE REQUEST": "csr",
386
+ "PRIVATE KEY": "pkcs8",
387
+ "ENCRYPTED PRIVATE KEY": "pkcs8-encrypted",
388
+ "RSA PRIVATE KEY": "pkcs1",
389
+ "RSA PUBLIC KEY": "pkcs1-public",
390
+ "EC PRIVATE KEY": "sec1",
391
+ "CMS": "cms",
392
+ "ATTRIBUTE CERTIFICATE": "attrcert",
393
+ "PKCS12": "pkcs12",
394
+ });
395
+
396
+ /** @internal One block, or the fact that one was opened and never closed. `_findPemBlock` answers
397
+ * null for both "no more blocks" and "a block that does not end", and a reader of a whole file has
398
+ * to tell those apart to name the fault. */
399
+ function _findPemBlockOrOpen(text, from) {
400
+ var m = _findPemBlock(text, from);
401
+ if (m !== null) return m;
402
+ var at = from;
403
+ if (at === 0) at = _bomWidth(text);
404
+ while (at < text.length) {
405
+ var end = _lineEnd(text, at);
406
+ var label = _boundaryLabel(text, at, end, _PEM_BEGIN);
407
+ if (label !== null) return { unterminated: true, index: at, label: label };
408
+ at = _nextLineStart(text, end);
409
+ }
410
+ return null;
411
+ }
412
+
413
+ /** @internal Writes an ordered list of objects to one text. Each label goes through the same
414
+ * validation the single-object writer applies, so a label carrying a boundary cannot put a block
415
+ * into the file that the caller never named. */
416
+ function pemEncodeBundle(objects, PemError) {
417
+ if (!intrinsic.isArray(objects)) {
418
+ throw new PemError("pem/bad-input", "encodeBundle takes an array of { label, der } objects");
419
+ }
420
+ var n = objects.length, out = "";
421
+ if (n === 0) throw new PemError("pem/bad-input", "encodeBundle takes at least one object");
422
+ if (n > constants.LIMITS.PEM_MAX_BLOCKS) {
423
+ throw new PemError("pem/too-many-blocks", "more than the " + constants.LIMITS.PEM_MAX_BLOCKS + " objects this writer admits");
424
+ }
425
+ for (var i = 0; i < n; i++) {
426
+ var o = objects[i];
427
+ if (o === null || typeof o !== "object") {
428
+ throw new PemError("pem/bad-input", "object " + i + " must be { label, der }");
429
+ }
430
+ out += pemEncode(o.der, o.label, PemError);
168
431
  }
169
- if (i > runStart) out += _strSlice(s, runStart, i);
170
432
  return out;
171
433
  }
172
434
 
173
- // @internal
174
- function pemDecodeLenient(text, label) {
175
- var begin = _PEM_BEGIN + label + _PEM_DASHES;
176
- var end = _PEM_END + label + _PEM_DASHES;
177
- var bi = _strIndexOf(text, begin);
178
- if (bi === -1) return null;
179
- var bodyStart = bi + begin.length;
180
- var ei = _strIndexOf(text, end, bodyStart);
181
- if (ei === -1) return null;
182
- return _bufferFrom(_keepBase64(_strSlice(text, bodyStart, ei)), "base64");
435
+ /** @internal The entity doors (a signer certificate, a TSA certificate, a CMS message given as PEM)
436
+ * read through the same scanner every parse door reads through, so the two cannot disagree about
437
+ * the same bytes. `null` means the file holds no block under `label`, which is what lets a caller
438
+ * try a second label. Every other fault is the caller's own `E(code, message)`, so the verb keeps
439
+ * its error code while the message names what the file actually is. */
440
+ function pemDecodeLenient(text, label, E, code) {
441
+ try {
442
+ return pemDecode(text, label, frameworkError.PemError);
443
+ } catch (e) {
444
+ if (e && (e.code === "pem/no-block" || e.code === "pem/label-mismatch")) return null;
445
+ if (typeof E === "function") throw E(code, "the PEM input is not one " + label + " object: " + e.message);
446
+ throw e;
447
+ }
183
448
  }
184
449
 
185
450
  /** @internal Every refusal below is built from opts, so opts is checked before the first one can be
@@ -196,27 +461,42 @@ function _requireCoerceOpts(opts) {
196
461
  }
197
462
  }
198
463
 
464
+ /** @internal The largest input either arm of `coerceToDer` could go on to accept: PEM text for the
465
+ * armored arm, DER for the other. Past it neither arm can succeed, so the size is decided first. */
466
+ var _COERCE_MAX_BYTES = constants.LIMITS.PEM_MAX_BYTES > constants.LIMITS.DER_MAX_BYTES
467
+ ? constants.LIMITS.PEM_MAX_BYTES : constants.LIMITS.DER_MAX_BYTES;
468
+
199
469
  function coerceToDer(input, opts) {
200
470
  _requireCoerceOpts(opts);
201
471
  if (typeof input === "string") return pemDecode(input, opts.pemLabel, opts.PemError);
202
472
  if (guard.bytes.isByteSource(input)) {
203
473
  var buf = guard.bytes.source(input, opts.ErrorClass, opts.prefix + "/bad-input", "parse");
474
+ /** @internal The ceiling is applied before the armor probe rather than after it. The probe
475
+ * walks until the first byte no text file carries, which on DER is its first byte and on
476
+ * printable input is the whole of it, so a buffer past every cap either arm could accept
477
+ * would otherwise be read end to end on the way to being refused for its size. */
478
+ guard.limits.byteCap(buf, _COERCE_MAX_BYTES, function (c, m) { return new opts.ErrorClass(c, m); },
479
+ opts.prefix + "/too-large", "parse input");
204
480
  return _isPemArmor(buf) ? pemDecode(buf, opts.pemLabel, opts.PemError) : buf;
205
481
  }
206
482
  throw new opts.ErrorClass(opts.prefix + "/bad-input", "parse expects a DER BufferSource (Buffer, typed array, DataView, or ArrayBuffer) or a PEM string");
207
483
  }
208
484
 
485
+ /** @internal Whether a buffer is armored text rather than DER. Read from a fixed window at the
486
+ * front, the answer is wrong for a file whose explanatory text is longer than the window, and the
487
+ * caller then hands PEM to the DER decoder and reports the DER as malformed. So the whole buffer is
488
+ * walked, and it costs nothing on DER: the walk stops at the first byte no text file carries, which
489
+ * for a DER SEQUENCE is its first byte. */
209
490
  function _isPemArmor(buf) {
210
- var head = _bufToString(_subarray(buf, 0, 4096), "latin1");
211
- var idx = _strIndexOf(head, "-----BEGIN");
212
- if (idx === -1) return false;
213
- for (var i = 0; i < idx; i++) {
491
+ var n = buf.length;
492
+ for (var i = 0; i < n; i++) {
214
493
  var c = buf[i];
215
494
  var textByte = (c >= 0x20 && c < 0x7f) || c === 0x09 || c === 0x0a || c === 0x0d ||
216
495
  c === 0xef || c === 0xbb || c === 0xbf;
217
496
  if (!textByte) return false;
497
+ if (c === 0x2d && _bufToString(_subarray(buf, i, i + 10), "latin1") === "-----BEGIN") return true;
218
498
  }
219
- return true;
499
+ return false;
220
500
  }
221
501
 
222
502
  /** @internal The codec's resource refusals, as distinct from its malformed-input refusals. A
@@ -1585,6 +1865,8 @@ module.exports = {
1585
1865
  dnsNameProblem: dnsNameProblem,
1586
1866
  pemDecode: pemDecode,
1587
1867
  pemDecodeAll: pemDecodeAll,
1868
+ pemDecodeBundle: pemDecodeBundle,
1869
+ pemEncodeBundle: pemEncodeBundle,
1588
1870
  pemDecodeLenient: pemDecodeLenient,
1589
1871
  pemEncode: pemEncode,
1590
1872
  isJsWhitespace: _isJsWhitespace,