@blamejs/core 0.10.15 → 0.11.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 +5 -0
- package/index.js +18 -0
- package/lib/auth/oauth.js +187 -0
- package/lib/auth/saml.js +1366 -13
- package/lib/cms-codec.js +141 -0
- package/lib/compliance.js +73 -0
- package/lib/csp.js +271 -0
- package/lib/dbsc.js +299 -0
- package/lib/fedcm.js +264 -0
- package/lib/hal.js +125 -0
- package/lib/http-client.js +46 -10
- package/lib/importmap-integrity.js +90 -0
- package/lib/jsonapi.js +230 -0
- package/lib/lro.js +200 -0
- package/lib/mail-crypto-pgp.js +312 -2
- package/lib/mail-crypto-smime.js +530 -69
- package/lib/metrics.js +62 -12
- package/lib/middleware/security-headers.js +2 -1
- package/lib/ssrf-guard.js +71 -10
- package/lib/standard-webhooks.js +183 -0
- package/lib/web-push-vapid.js +322 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-crypto-pgp.js
CHANGED
|
@@ -120,6 +120,7 @@ var lazyRequire = require("./lazy-require");
|
|
|
120
120
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
121
121
|
var nodeCrypto = require("node:crypto");
|
|
122
122
|
var validateOpts = require("./validate-opts");
|
|
123
|
+
var numericBounds = require("./numeric-bounds");
|
|
123
124
|
var { defineClass } = require("./framework-error");
|
|
124
125
|
|
|
125
126
|
var MailCryptoError = defineClass("MailCryptoError", { alwaysPermanent: true });
|
|
@@ -923,12 +924,321 @@ function _audit(auditHandle, action, outcome, metadata) {
|
|
|
923
924
|
} catch (_e) { /* drop-silent — audit failures must not crash callers */ }
|
|
924
925
|
}
|
|
925
926
|
|
|
927
|
+
// ---- v0.10.16 experimental encrypt/decrypt + WKD ----
|
|
928
|
+
//
|
|
929
|
+
// PQC PGP encrypt/decrypt for ML-KEM-1024 recipients shipped under
|
|
930
|
+
// `experimental` namespace (RFC 9580bis PKESK ML-KEM codepoints
|
|
931
|
+
// haven't IANA-registered yet). Framework-private envelope matching
|
|
932
|
+
// the v0.10.10 `b.jose.jwe.experimental` precedent. Operators
|
|
933
|
+
// integrating with peers running this same framework get
|
|
934
|
+
// encrypt/decrypt today; cross-implementation interop waits for IANA.
|
|
935
|
+
|
|
936
|
+
var bCrypto = require("./crypto");
|
|
937
|
+
var pqcSoftware = require("./pqc-software");
|
|
938
|
+
|
|
939
|
+
var PGP_PQ_MAGIC = Buffer.from("BJ-PGP-PQ", "ascii"); // allow:raw-byte-literal — 9-byte framework magic
|
|
940
|
+
var PGP_PQ_VERSION = 1; // allow:raw-byte-literal — envelope version
|
|
941
|
+
|
|
942
|
+
function experimentalEncrypt(opts) {
|
|
943
|
+
opts = validateOpts.requireObject(opts, "mail.crypto.pgp.experimental.encrypt",
|
|
944
|
+
MailCryptoError, "mail-crypto/pgp/bad-opts");
|
|
945
|
+
validateOpts(opts, ["message", "recipients", "audit"], "mail.crypto.pgp.experimental.encrypt");
|
|
946
|
+
if (!opts.message || (!Buffer.isBuffer(opts.message) && typeof opts.message !== "string")) {
|
|
947
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-message",
|
|
948
|
+
"encrypt: opts.message must be a Buffer or string");
|
|
949
|
+
}
|
|
950
|
+
if (!Array.isArray(opts.recipients) || opts.recipients.length === 0) {
|
|
951
|
+
throw new MailCryptoError("mail-crypto/pgp/no-recipients",
|
|
952
|
+
"encrypt: opts.recipients must be a non-empty array");
|
|
953
|
+
}
|
|
954
|
+
var plaintext = Buffer.isBuffer(opts.message) ? opts.message : Buffer.from(opts.message, "utf8");
|
|
955
|
+
var sessionKey = bCrypto.generateBytes(32); // allow:raw-byte-literal — 256-bit session key
|
|
956
|
+
var ciphertext = bCrypto.encryptPacked(plaintext, sessionKey);
|
|
957
|
+
var recipientBlobs = [];
|
|
958
|
+
for (var i = 0; i < opts.recipients.length; i += 1) {
|
|
959
|
+
var r = opts.recipients[i];
|
|
960
|
+
if (!Buffer.isBuffer(r.recipientId)) {
|
|
961
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-recipient",
|
|
962
|
+
"encrypt: recipients[" + i + "].recipientId must be a Buffer");
|
|
963
|
+
}
|
|
964
|
+
if (!(r.publicKey instanceof Uint8Array)) {
|
|
965
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-recipient",
|
|
966
|
+
"encrypt: recipients[" + i + "].publicKey must be a Uint8Array (ML-KEM-1024)");
|
|
967
|
+
}
|
|
968
|
+
if (r.recipientId.length > 255) { // allow:raw-byte-literal — u8 length cap
|
|
969
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-recipient",
|
|
970
|
+
"encrypt: recipients[" + i + "].recipientId must be <= 255 bytes");
|
|
971
|
+
}
|
|
972
|
+
var encap = pqcSoftware.ml_kem_1024.encapsulate(r.publicKey);
|
|
973
|
+
var kek = bCrypto.kdf(Buffer.concat([
|
|
974
|
+
Buffer.from(encap.sharedSecret),
|
|
975
|
+
Buffer.from("pgp/experimental/chacha20-poly1305", "ascii"),
|
|
976
|
+
]), 32); // allow:raw-byte-literal — 256-bit KEK
|
|
977
|
+
var wrappedKey = bCrypto.encryptPacked(sessionKey, kek);
|
|
978
|
+
var ct = Buffer.from(encap.cipherText);
|
|
979
|
+
recipientBlobs.push(Buffer.concat([
|
|
980
|
+
Buffer.from([r.recipientId.length]),
|
|
981
|
+
r.recipientId,
|
|
982
|
+
_u16be(ct.length),
|
|
983
|
+
ct,
|
|
984
|
+
_u16be(wrappedKey.length),
|
|
985
|
+
wrappedKey,
|
|
986
|
+
]));
|
|
987
|
+
}
|
|
988
|
+
var envelope = Buffer.concat([
|
|
989
|
+
PGP_PQ_MAGIC,
|
|
990
|
+
Buffer.from([PGP_PQ_VERSION]),
|
|
991
|
+
Buffer.from([opts.recipients.length]), // allow:raw-byte-literal — u8 recipient count
|
|
992
|
+
Buffer.concat(recipientBlobs),
|
|
993
|
+
_u32be(ciphertext.length),
|
|
994
|
+
ciphertext,
|
|
995
|
+
]);
|
|
996
|
+
var armored = _armorMessage(envelope);
|
|
997
|
+
_audit(opts.audit, "mail.crypto.pgp.experimental.encrypt", "success", {
|
|
998
|
+
recipients: opts.recipients.length,
|
|
999
|
+
});
|
|
1000
|
+
return { armored: armored, envelope: envelope };
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function experimentalDecrypt(opts) {
|
|
1004
|
+
opts = validateOpts.requireObject(opts, "mail.crypto.pgp.experimental.decrypt",
|
|
1005
|
+
MailCryptoError, "mail-crypto/pgp/bad-opts");
|
|
1006
|
+
validateOpts(opts, ["armored", "envelope", "recipientId", "secretKey", "audit"],
|
|
1007
|
+
"mail.crypto.pgp.experimental.decrypt");
|
|
1008
|
+
if (!Buffer.isBuffer(opts.recipientId)) {
|
|
1009
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-opts",
|
|
1010
|
+
"decrypt: opts.recipientId must be a Buffer");
|
|
1011
|
+
}
|
|
1012
|
+
if (!(opts.secretKey instanceof Uint8Array)) {
|
|
1013
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-opts",
|
|
1014
|
+
"decrypt: opts.secretKey must be a Uint8Array");
|
|
1015
|
+
}
|
|
1016
|
+
var envelope;
|
|
1017
|
+
if (Buffer.isBuffer(opts.envelope)) {
|
|
1018
|
+
envelope = opts.envelope;
|
|
1019
|
+
} else if (typeof opts.armored === "string" && opts.armored.length > 0) {
|
|
1020
|
+
envelope = _dearmorMessage(opts.armored);
|
|
1021
|
+
} else {
|
|
1022
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-opts",
|
|
1023
|
+
"decrypt: opts.envelope OR opts.armored required");
|
|
1024
|
+
}
|
|
1025
|
+
if (envelope.length < PGP_PQ_MAGIC.length + 2 ||
|
|
1026
|
+
!envelope.slice(0, PGP_PQ_MAGIC.length).equals(PGP_PQ_MAGIC)) {
|
|
1027
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-magic",
|
|
1028
|
+
"decrypt: envelope magic mismatch (not a blamejs-pgp-pq-v1 envelope)");
|
|
1029
|
+
}
|
|
1030
|
+
var off = PGP_PQ_MAGIC.length;
|
|
1031
|
+
var version = envelope[off]; off += 1;
|
|
1032
|
+
if (version !== PGP_PQ_VERSION) {
|
|
1033
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-version",
|
|
1034
|
+
"decrypt: envelope version " + version + " unsupported (expected " + PGP_PQ_VERSION + ")");
|
|
1035
|
+
}
|
|
1036
|
+
var nRecips = envelope[off]; off += 1;
|
|
1037
|
+
var matchedSessionKey = null;
|
|
1038
|
+
for (var i = 0; i < nRecips; i += 1) {
|
|
1039
|
+
if (off >= envelope.length) {
|
|
1040
|
+
throw new MailCryptoError("mail-crypto/pgp/truncated",
|
|
1041
|
+
"decrypt: envelope truncated at recipient " + i);
|
|
1042
|
+
}
|
|
1043
|
+
var ridLen = envelope[off]; off += 1;
|
|
1044
|
+
var rid = envelope.slice(off, off + ridLen); off += ridLen;
|
|
1045
|
+
var ctLen = envelope.readUInt16BE(off); off += 2; // allow:raw-byte-literal — u16-be width
|
|
1046
|
+
var ct = envelope.slice(off, off + ctLen); off += ctLen;
|
|
1047
|
+
var wkLen = envelope.readUInt16BE(off); off += 2; // allow:raw-byte-literal — u16-be width
|
|
1048
|
+
var wrappedKey = envelope.slice(off, off + wkLen); off += wkLen;
|
|
1049
|
+
if (matchedSessionKey) continue;
|
|
1050
|
+
if (!rid.equals(opts.recipientId)) continue;
|
|
1051
|
+
var shared;
|
|
1052
|
+
try { shared = pqcSoftware.ml_kem_1024.decapsulate(new Uint8Array(ct), opts.secretKey); }
|
|
1053
|
+
catch (e) {
|
|
1054
|
+
throw new MailCryptoError("mail-crypto/pgp/decap-failed",
|
|
1055
|
+
"decrypt: ML-KEM-1024 decapsulate failed: " + ((e && e.message) || String(e)));
|
|
1056
|
+
}
|
|
1057
|
+
var kek = bCrypto.kdf(Buffer.concat([
|
|
1058
|
+
Buffer.from(shared),
|
|
1059
|
+
Buffer.from("pgp/experimental/chacha20-poly1305", "ascii"),
|
|
1060
|
+
]), 32); // allow:raw-byte-literal — 256-bit KEK
|
|
1061
|
+
try { matchedSessionKey = bCrypto.decryptPacked(wrappedKey, kek); }
|
|
1062
|
+
catch (e2) {
|
|
1063
|
+
throw new MailCryptoError("mail-crypto/pgp/unwrap-failed",
|
|
1064
|
+
"decrypt: session-key unwrap failed: " + ((e2 && e2.message) || String(e2)));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (!matchedSessionKey) {
|
|
1068
|
+
throw new MailCryptoError("mail-crypto/pgp/no-matching-recipient",
|
|
1069
|
+
"decrypt: no recipient in envelope matches opts.recipientId");
|
|
1070
|
+
}
|
|
1071
|
+
var bodyLen = envelope.readUInt32BE(off); off += 4; // allow:raw-byte-literal — u32-be width
|
|
1072
|
+
var body = envelope.slice(off, off + bodyLen);
|
|
1073
|
+
var plaintext;
|
|
1074
|
+
try { plaintext = bCrypto.decryptPacked(body, matchedSessionKey); }
|
|
1075
|
+
catch (e3) {
|
|
1076
|
+
throw new MailCryptoError("mail-crypto/pgp/body-decrypt-failed",
|
|
1077
|
+
"decrypt: body AEAD verify failed: " + ((e3 && e3.message) || String(e3)));
|
|
1078
|
+
}
|
|
1079
|
+
_audit(opts.audit, "mail.crypto.pgp.experimental.decrypt", "success", {});
|
|
1080
|
+
return { plaintext: plaintext, recipientId: opts.recipientId };
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function _armorMessage(bytes) {
|
|
1084
|
+
var b64 = bytes.toString("base64");
|
|
1085
|
+
var lines = [];
|
|
1086
|
+
for (var i = 0; i < b64.length; i += 64) { // allow:raw-byte-literal — RFC 2045 base64 line length
|
|
1087
|
+
lines.push(b64.slice(i, i + 64)); // allow:raw-byte-literal — RFC 2045 base64 line length
|
|
1088
|
+
}
|
|
1089
|
+
return "-----BEGIN PGP MESSAGE-----\r\nVersion: blamejs-pgp-pq-v1\r\n\r\n" +
|
|
1090
|
+
lines.join("\r\n") + "\r\n-----END PGP MESSAGE-----\r\n";
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function _dearmorMessage(armored) {
|
|
1094
|
+
// Line-by-line parser — avoids the polynomial-time backtracking of
|
|
1095
|
+
// the prior regex (CodeQL "Polynomial regular expression on
|
|
1096
|
+
// uncontrolled data"). The previous shape
|
|
1097
|
+
// /-----BEGIN PGP MESSAGE-----\r?\n(?:[^\r\n]+\r?\n)*\r?\n.../
|
|
1098
|
+
// backtracks pathologically on inputs starting with many repeated
|
|
1099
|
+
// BEGIN lines. Split + walk in linear time instead.
|
|
1100
|
+
if (typeof armored !== "string") {
|
|
1101
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-armor",
|
|
1102
|
+
"dearmor: envelope must be a string");
|
|
1103
|
+
}
|
|
1104
|
+
var lines = armored.split(/\r?\n/);
|
|
1105
|
+
var begin = -1;
|
|
1106
|
+
var end = -1;
|
|
1107
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
1108
|
+
if (begin === -1 && lines[i] === "-----BEGIN PGP MESSAGE-----") begin = i;
|
|
1109
|
+
else if (begin !== -1 && lines[i] === "-----END PGP MESSAGE-----") { end = i; break; }
|
|
1110
|
+
}
|
|
1111
|
+
if (begin === -1 || end === -1) {
|
|
1112
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-armor",
|
|
1113
|
+
"dearmor: envelope is not BEGIN PGP MESSAGE armored");
|
|
1114
|
+
}
|
|
1115
|
+
// Skip header lines until the blank-line separator (RFC 9580 §6.2),
|
|
1116
|
+
// then collect base64 body lines until the END marker.
|
|
1117
|
+
var bodyStart = begin + 1;
|
|
1118
|
+
while (bodyStart < end && lines[bodyStart] !== "") bodyStart += 1;
|
|
1119
|
+
if (bodyStart >= end) {
|
|
1120
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-armor",
|
|
1121
|
+
"dearmor: armor header has no blank-line separator before body");
|
|
1122
|
+
}
|
|
1123
|
+
var bodyChunks = [];
|
|
1124
|
+
for (var j = bodyStart + 1; j < end; j += 1) bodyChunks.push(lines[j]);
|
|
1125
|
+
return Buffer.from(bodyChunks.join(""), "base64");
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* @primitive b.mail.crypto.pgp.experimental.wkd.fetch
|
|
1130
|
+
* @signature b.mail.crypto.pgp.experimental.wkd.fetch(email, opts)
|
|
1131
|
+
* @since 0.10.16
|
|
1132
|
+
* @status experimental
|
|
1133
|
+
*
|
|
1134
|
+
* Fetch a WKD key for `email` per draft-koch-openpgp-webkey-service.
|
|
1135
|
+
* Tries the direct URL first; on 404 / network failure falls back
|
|
1136
|
+
* to the advanced URL. `opts.httpsGet(url) → Promise<{ status,
|
|
1137
|
+
* body: Buffer }>` is operator-supplied so the framework doesn't
|
|
1138
|
+
* couple to a specific HTTP client. Returns
|
|
1139
|
+
* `{ keyBytes, source: "direct" | "advanced", url }` or throws
|
|
1140
|
+
* `mail-crypto/pgp/wkd-not-found` when both URLs fail.
|
|
1141
|
+
*
|
|
1142
|
+
* @opts
|
|
1143
|
+
* httpsGet: Function, // (url) → Promise<{ status, body }>; REQUIRED
|
|
1144
|
+
* advancedHost: string, // passed through to computeUrl
|
|
1145
|
+
* maxKeyBytes: number, // default 256 KiB
|
|
1146
|
+
*
|
|
1147
|
+
* @example
|
|
1148
|
+
* var key = await b.mail.crypto.pgp.experimental.wkd.fetch("alice@example.com", {
|
|
1149
|
+
* httpsGet: function (url) {
|
|
1150
|
+
* return b.httpClient.request({ url: url, method: "GET" });
|
|
1151
|
+
* },
|
|
1152
|
+
* });
|
|
1153
|
+
*/
|
|
1154
|
+
function wkdFetch(email, opts) {
|
|
1155
|
+
opts = validateOpts.requireObject(opts, "mail.crypto.pgp.experimental.wkd.fetch",
|
|
1156
|
+
MailCryptoError, "mail-crypto/pgp/bad-opts");
|
|
1157
|
+
if (typeof opts.httpsGet !== "function") {
|
|
1158
|
+
throw new MailCryptoError("mail-crypto/pgp/no-https-get",
|
|
1159
|
+
"wkd.fetch: opts.httpsGet must be a function (url) => Promise<{status, body}>");
|
|
1160
|
+
}
|
|
1161
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.maxKeyBytes, "maxKeyBytes",
|
|
1162
|
+
MailCryptoError, "mail-crypto/pgp/bad-max-key-bytes");
|
|
1163
|
+
var maxBytes = typeof opts.maxKeyBytes === "number" ? opts.maxKeyBytes : (256 * 1024); // allow:raw-byte-literal — 256 KiB default key cap
|
|
1164
|
+
var urls = wkdComputeUrl(email, { advancedHost: opts.advancedHost });
|
|
1165
|
+
return Promise.resolve(opts.httpsGet(urls.direct)).then(function (resp) {
|
|
1166
|
+
if (resp && resp.status === 200 && Buffer.isBuffer(resp.body) && resp.body.length > 0) { // allow:raw-byte-literal — HTTP 200
|
|
1167
|
+
if (resp.body.length > maxBytes) {
|
|
1168
|
+
throw new MailCryptoError("mail-crypto/pgp/wkd-too-large",
|
|
1169
|
+
"wkd.fetch: key bytes " + resp.body.length + " exceed maxKeyBytes=" + maxBytes);
|
|
1170
|
+
}
|
|
1171
|
+
return { keyBytes: resp.body, source: "direct", url: urls.direct };
|
|
1172
|
+
}
|
|
1173
|
+
return Promise.resolve(opts.httpsGet(urls.advanced)).then(function (resp2) {
|
|
1174
|
+
if (resp2 && resp2.status === 200 && Buffer.isBuffer(resp2.body) && resp2.body.length > 0) { // allow:raw-byte-literal — HTTP 200
|
|
1175
|
+
if (resp2.body.length > maxBytes) {
|
|
1176
|
+
throw new MailCryptoError("mail-crypto/pgp/wkd-too-large",
|
|
1177
|
+
"wkd.fetch: key bytes " + resp2.body.length + " exceed maxKeyBytes=" + maxBytes);
|
|
1178
|
+
}
|
|
1179
|
+
return { keyBytes: resp2.body, source: "advanced", url: urls.advanced };
|
|
1180
|
+
}
|
|
1181
|
+
throw new MailCryptoError("mail-crypto/pgp/wkd-not-found",
|
|
1182
|
+
"wkd.fetch: neither direct nor advanced URL returned a key for " + email);
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function wkdComputeUrl(email, opts) {
|
|
1188
|
+
opts = opts || {};
|
|
1189
|
+
if (typeof email !== "string" || email.indexOf("@") <= 0 || email.indexOf("@") === email.length - 1) {
|
|
1190
|
+
throw new MailCryptoError("mail-crypto/pgp/bad-email",
|
|
1191
|
+
"wkd.computeUrl: email must be a 'local@domain' string");
|
|
1192
|
+
}
|
|
1193
|
+
var at = email.indexOf("@");
|
|
1194
|
+
var localRaw = email.slice(0, at);
|
|
1195
|
+
var localLower = localRaw.toLowerCase();
|
|
1196
|
+
var domain = email.slice(at + 1).toLowerCase();
|
|
1197
|
+
var hashed = bCrypto.kdf(Buffer.from(localLower, "utf8"), 20); // allow:raw-byte-literal — 20-byte hash per draft-koch §3.1
|
|
1198
|
+
var encoded = _zbase32Encode(hashed);
|
|
1199
|
+
var advancedHost = opts.advancedHost || ("openpgpkey." + domain);
|
|
1200
|
+
var encodedLocal = encodeURIComponent(localRaw);
|
|
1201
|
+
return {
|
|
1202
|
+
direct: "https://" + domain + "/.well-known/openpgpkey/hu/" + encoded + "?l=" + encodedLocal,
|
|
1203
|
+
advanced: "https://" + advancedHost + "/.well-known/openpgpkey/" + domain + "/hu/" + encoded + "?l=" + encodedLocal,
|
|
1204
|
+
hashed: encoded,
|
|
1205
|
+
localLower: localLower,
|
|
1206
|
+
domain: domain,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
var ZBASE32_ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769";
|
|
1211
|
+
|
|
1212
|
+
function _zbase32Encode(buf) {
|
|
1213
|
+
var bits = 0;
|
|
1214
|
+
var bitCount = 0;
|
|
1215
|
+
var out = "";
|
|
1216
|
+
for (var i = 0; i < buf.length; i += 1) {
|
|
1217
|
+
bits = (bits << 8) | buf[i]; // allow:raw-byte-literal — 8 bits per input byte
|
|
1218
|
+
bitCount += 8; // allow:raw-byte-literal — 8 bits per input byte
|
|
1219
|
+
while (bitCount >= 5) { // allow:raw-byte-literal — 5 bits per zbase32 char
|
|
1220
|
+
bitCount -= 5; // allow:raw-byte-literal — 5 bits per zbase32 char
|
|
1221
|
+
out += ZBASE32_ALPHABET.charAt((bits >> bitCount) & 0x1f); // allow:raw-byte-literal — 5-bit mask
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (bitCount > 0) {
|
|
1225
|
+
out += ZBASE32_ALPHABET.charAt((bits << (5 - bitCount)) & 0x1f); // allow:raw-byte-literal — final partial char
|
|
1226
|
+
}
|
|
1227
|
+
return out;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
926
1230
|
module.exports = {
|
|
927
1231
|
sign: sign,
|
|
928
1232
|
verify: verify,
|
|
1233
|
+
experimental: {
|
|
1234
|
+
encrypt: experimentalEncrypt,
|
|
1235
|
+
decrypt: experimentalDecrypt,
|
|
1236
|
+
wkd: {
|
|
1237
|
+
computeUrl: wkdComputeUrl,
|
|
1238
|
+
fetch: wkdFetch,
|
|
1239
|
+
},
|
|
1240
|
+
},
|
|
929
1241
|
MailCryptoError: MailCryptoError,
|
|
930
|
-
// Test-only exports — operators don't call these. Stable for v1 so
|
|
931
|
-
// the codebase-patterns gate sees them as named.
|
|
932
1242
|
_v4FingerprintForTest: _v4Fingerprint,
|
|
933
1243
|
_armorForTest: _armor,
|
|
934
1244
|
_dearmorForTest: _dearmor,
|