api-ape 3.0.2 → 4.1.0
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/README.md +59 -572
- package/client/README.md +73 -14
- package/client/auth/crypto/aead.js +214 -0
- package/client/auth/crypto/constants.js +32 -0
- package/client/auth/crypto/encoding.js +104 -0
- package/client/auth/crypto/files.md +27 -0
- package/client/auth/crypto/kdf.js +217 -0
- package/client/auth/crypto-utils.js +118 -0
- package/client/auth/files.md +52 -0
- package/client/auth/key-recovery.js +288 -0
- package/client/auth/recovery/constants.js +37 -0
- package/client/auth/recovery/files.md +23 -0
- package/client/auth/recovery/key-derivation.js +61 -0
- package/client/auth/recovery/sss-browser.js +189 -0
- package/client/auth/share-storage.js +205 -0
- package/client/auth/storage/constants.js +18 -0
- package/client/auth/storage/db.js +132 -0
- package/client/auth/storage/files.md +27 -0
- package/client/auth/storage/keys.js +173 -0
- package/client/auth/storage/shares.js +200 -0
- package/client/browser.js +190 -23
- package/client/connectSocket.js +418 -988
- package/client/connection/README.md +23 -0
- package/client/connection/fileDownload.js +256 -0
- package/client/connection/fileHandling.js +450 -0
- package/client/connection/fileUtils.js +346 -0
- package/client/connection/files.md +71 -0
- package/client/connection/messageHandler.js +105 -0
- package/client/connection/network.js +350 -0
- package/client/connection/proxy.js +233 -0
- package/client/connection/sender.js +333 -0
- package/client/connection/state.js +321 -0
- package/client/connection/subscriptions.js +151 -0
- package/client/files.md +53 -0
- package/client/index.js +298 -142
- package/client/transports/README.md +50 -0
- package/client/transports/files.md +41 -0
- package/client/transports/streamParser.js +195 -0
- package/client/transports/streaming.js +555 -203
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +31 -6
- package/server/README.md +272 -67
- package/server/adapters/README.md +23 -14
- package/server/adapters/files.md +68 -0
- package/server/adapters/firebase.js +543 -160
- package/server/adapters/index.js +362 -112
- package/server/adapters/mongo.js +530 -140
- package/server/adapters/postgres.js +534 -155
- package/server/adapters/redis.js +508 -143
- package/server/adapters/supabase.js +555 -186
- package/server/client/README.md +43 -0
- package/server/client/connection.js +586 -0
- package/server/client/files.md +40 -0
- package/server/client/index.js +342 -0
- package/server/files.md +54 -0
- package/server/index.js +322 -71
- package/server/lib/README.md +26 -0
- package/server/lib/broadcast/clients.js +219 -0
- package/server/lib/broadcast/files.md +58 -0
- package/server/lib/broadcast/index.js +57 -0
- package/server/lib/broadcast/publishProxy.js +110 -0
- package/server/lib/broadcast/pubsub.js +137 -0
- package/server/lib/broadcast/sendProxy.js +103 -0
- package/server/lib/bun.js +315 -99
- package/server/lib/fileTransfer/README.md +63 -0
- package/server/lib/fileTransfer/files.md +30 -0
- package/server/lib/fileTransfer/streaming.js +435 -0
- package/server/lib/fileTransfer.js +710 -326
- package/server/lib/files.md +111 -0
- package/server/lib/httpUtils.js +283 -0
- package/server/lib/loader.js +208 -7
- package/server/lib/longPolling/README.md +63 -0
- package/server/lib/longPolling/files.md +44 -0
- package/server/lib/longPolling/getHandler.js +365 -0
- package/server/lib/longPolling/postHandler.js +327 -0
- package/server/lib/longPolling.js +174 -219
- package/server/lib/main.js +369 -532
- package/server/lib/runtimes/README.md +42 -0
- package/server/lib/runtimes/bun.js +586 -0
- package/server/lib/runtimes/files.md +56 -0
- package/server/lib/runtimes/node.js +511 -0
- package/server/lib/wiring.js +539 -98
- package/server/lib/ws/README.md +35 -0
- package/server/lib/ws/adapters/README.md +54 -0
- package/server/lib/ws/adapters/bun.js +538 -170
- package/server/lib/ws/adapters/deno.js +623 -149
- package/server/lib/ws/adapters/files.md +42 -0
- package/server/lib/ws/files.md +74 -0
- package/server/lib/ws/frames.js +532 -154
- package/server/lib/ws/index.js +207 -10
- package/server/lib/ws/server.js +385 -92
- package/server/lib/ws/socket.js +549 -181
- package/server/lib/wsProvider.js +363 -89
- package/server/plugins/binary.js +282 -0
- package/server/security/README.md +92 -0
- package/server/security/auth/README.md +319 -0
- package/server/security/auth/adapters/files.md +95 -0
- package/server/security/auth/adapters/ldap/constants.js +37 -0
- package/server/security/auth/adapters/ldap/files.md +19 -0
- package/server/security/auth/adapters/ldap/helpers.js +111 -0
- package/server/security/auth/adapters/ldap.js +353 -0
- package/server/security/auth/adapters/oauth2/constants.js +41 -0
- package/server/security/auth/adapters/oauth2/files.md +19 -0
- package/server/security/auth/adapters/oauth2/helpers.js +123 -0
- package/server/security/auth/adapters/oauth2.js +273 -0
- package/server/security/auth/adapters/opaque-handlers.js +314 -0
- package/server/security/auth/adapters/opaque.js +205 -0
- package/server/security/auth/adapters/saml/constants.js +52 -0
- package/server/security/auth/adapters/saml/files.md +19 -0
- package/server/security/auth/adapters/saml/helpers.js +74 -0
- package/server/security/auth/adapters/saml.js +173 -0
- package/server/security/auth/adapters/totp.js +703 -0
- package/server/security/auth/adapters/webauthn.js +625 -0
- package/server/security/auth/files.md +61 -0
- package/server/security/auth/framework/constants.js +27 -0
- package/server/security/auth/framework/files.md +23 -0
- package/server/security/auth/framework/handlers.js +272 -0
- package/server/security/auth/framework/socket-auth.js +177 -0
- package/server/security/auth/handlers/auth-messages.js +143 -0
- package/server/security/auth/handlers/files.md +28 -0
- package/server/security/auth/index.js +290 -0
- package/server/security/auth/mfa/crypto/aead.js +148 -0
- package/server/security/auth/mfa/crypto/constants.js +35 -0
- package/server/security/auth/mfa/crypto/files.md +27 -0
- package/server/security/auth/mfa/crypto/kdf.js +120 -0
- package/server/security/auth/mfa/crypto/utils.js +68 -0
- package/server/security/auth/mfa/crypto-utils.js +80 -0
- package/server/security/auth/mfa/files.md +77 -0
- package/server/security/auth/mfa/ledger/constants.js +75 -0
- package/server/security/auth/mfa/ledger/errors.js +73 -0
- package/server/security/auth/mfa/ledger/files.md +23 -0
- package/server/security/auth/mfa/ledger/share-record.js +32 -0
- package/server/security/auth/mfa/ledger.js +255 -0
- package/server/security/auth/mfa/recovery/constants.js +67 -0
- package/server/security/auth/mfa/recovery/files.md +19 -0
- package/server/security/auth/mfa/recovery/handlers.js +216 -0
- package/server/security/auth/mfa/recovery.js +191 -0
- package/server/security/auth/mfa/sss/constants.js +21 -0
- package/server/security/auth/mfa/sss/files.md +23 -0
- package/server/security/auth/mfa/sss/gf256.js +103 -0
- package/server/security/auth/mfa/sss/serialization.js +82 -0
- package/server/security/auth/mfa/sss.js +161 -0
- package/server/security/auth/mfa/two-of-three/constants.js +58 -0
- package/server/security/auth/mfa/two-of-three/files.md +23 -0
- package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
- package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
- package/server/security/auth/mfa/two-of-three.js +136 -0
- package/server/security/auth/nonce-manager.js +89 -0
- package/server/security/auth/state-machine-mfa.js +269 -0
- package/server/security/auth/state-machine.js +257 -0
- package/server/security/extractRootDomain.js +144 -16
- package/server/security/files.md +51 -0
- package/server/security/origin.js +197 -15
- package/server/security/reply.js +274 -16
- package/server/socket/README.md +119 -0
- package/server/socket/authMiddleware.js +299 -0
- package/server/socket/files.md +86 -0
- package/server/socket/open.js +154 -8
- package/server/socket/pluginHooks.js +334 -0
- package/server/socket/receive.js +184 -224
- package/server/socket/receiveContext.js +117 -0
- package/server/socket/send.js +416 -78
- package/server/socket/tagUtils.js +402 -0
- package/server/utils/README.md +19 -0
- package/server/utils/deepRequire.js +255 -30
- package/server/utils/files.md +57 -0
- package/server/utils/genId.js +182 -20
- package/server/utils/parseUserAgent.js +313 -251
- package/server/utils/userAgent/README.md +65 -0
- package/server/utils/userAgent/files.md +46 -0
- package/server/utils/userAgent/patterns.js +545 -0
- package/utils/README.md +21 -0
- package/utils/files.md +66 -0
- package/utils/jss/README.md +21 -0
- package/utils/jss/decode.js +471 -0
- package/utils/jss/encode.js +312 -0
- package/utils/jss/files.md +68 -0
- package/utils/jss/plugins.js +210 -0
- package/utils/jss.js +219 -273
- package/utils/messageHash.js +238 -35
- package/dist/api-ape.min.js +0 -2
- package/dist/api-ape.min.js.map +0 -7
- package/server/client.js +0 -311
- package/server/lib/broadcast.js +0 -146
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shamir Secret Sharing (SSS) Utilities
|
|
3
|
+
*
|
|
4
|
+
* Implements threshold secret sharing using GF(256) arithmetic.
|
|
5
|
+
*
|
|
6
|
+
* @module server/security/auth/mfa/sss
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const crypto = require("crypto");
|
|
12
|
+
const { SSSError } = require("./sss/constants");
|
|
13
|
+
const { gfMul, gfDiv, gfAdd, evaluatePolynomial, lagrangeInterpolate } = require("./sss/gf256");
|
|
14
|
+
const { serializeShare, deserializeShare, verifyShareFormat } = require("./sss/serialization");
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Split a secret into n shares with threshold k
|
|
18
|
+
*
|
|
19
|
+
* @param {Buffer|Uint8Array|string} secret - Secret to split
|
|
20
|
+
* @param {number} threshold - Minimum shares needed to reconstruct
|
|
21
|
+
* @param {number} totalShares - Total shares to generate
|
|
22
|
+
* @returns {Array} Array of shares with index and data
|
|
23
|
+
* @throws {Error} If parameters are invalid
|
|
24
|
+
*/
|
|
25
|
+
function split(secret, threshold, totalShares) {
|
|
26
|
+
if (threshold < 2) {
|
|
27
|
+
const err = new Error(`Threshold must be at least 2, got ${threshold}`);
|
|
28
|
+
err.code = SSSError.INVALID_THRESHOLD;
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
if (totalShares < threshold) {
|
|
32
|
+
const err = new Error(`Total shares (${totalShares}) must be >= threshold (${threshold})`);
|
|
33
|
+
err.code = SSSError.INVALID_SHARE_COUNT;
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
if (totalShares > 255) {
|
|
37
|
+
const err = new Error(`Total shares (${totalShares}) must be <= 255 for GF(256)`);
|
|
38
|
+
err.code = SSSError.INVALID_SHARE_COUNT;
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let secretBuffer;
|
|
43
|
+
if (typeof secret === "string") secretBuffer = Buffer.from(secret, "utf8");
|
|
44
|
+
else if (secret instanceof Uint8Array) secretBuffer = Buffer.from(secret);
|
|
45
|
+
else if (Buffer.isBuffer(secret)) secretBuffer = secret;
|
|
46
|
+
else {
|
|
47
|
+
const err = new Error("Secret must be a Buffer, Uint8Array, or string");
|
|
48
|
+
err.code = SSSError.INVALID_SECRET;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (secretBuffer.length === 0) {
|
|
53
|
+
const err = new Error("Secret cannot be empty");
|
|
54
|
+
err.code = SSSError.INVALID_SECRET;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const coeffCount = threshold - 1;
|
|
59
|
+
const randomBytes = crypto.randomBytes(secretBuffer.length * coeffCount);
|
|
60
|
+
|
|
61
|
+
const shares = [];
|
|
62
|
+
for (let i = 0; i < totalShares; i++) {
|
|
63
|
+
shares.push({ index: i + 1, data: Buffer.alloc(secretBuffer.length) });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (let byteIdx = 0; byteIdx < secretBuffer.length; byteIdx++) {
|
|
67
|
+
const coefficients = new Uint8Array(threshold);
|
|
68
|
+
coefficients[0] = secretBuffer[byteIdx];
|
|
69
|
+
for (let c = 1; c < threshold; c++) {
|
|
70
|
+
coefficients[c] = randomBytes[byteIdx * coeffCount + (c - 1)];
|
|
71
|
+
}
|
|
72
|
+
for (let shareIdx = 0; shareIdx < totalShares; shareIdx++) {
|
|
73
|
+
shares[shareIdx].data[byteIdx] = evaluatePolynomial(coefficients, shares[shareIdx].index);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return shares;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Combine shares to reconstruct the original secret
|
|
81
|
+
*
|
|
82
|
+
* @param {Array} shares - Array of share objects
|
|
83
|
+
* @returns {Buffer} Reconstructed secret
|
|
84
|
+
* @throws {Error} If shares are insufficient or malformed
|
|
85
|
+
*/
|
|
86
|
+
function combine(shares) {
|
|
87
|
+
if (!Array.isArray(shares) || shares.length === 0) {
|
|
88
|
+
const err = new Error("Shares must be a non-empty array");
|
|
89
|
+
err.code = SSSError.INSUFFICIENT_SHARES;
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
if (shares.length < 2) {
|
|
93
|
+
const err = new Error("At least 2 shares are required");
|
|
94
|
+
err.code = SSSError.INSUFFICIENT_SHARES;
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const secretLength = shares[0].data?.length;
|
|
99
|
+
if (!secretLength) {
|
|
100
|
+
const err = new Error("Invalid share format: missing data");
|
|
101
|
+
err.code = SSSError.INVALID_SHARE_FORMAT;
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const seenIndices = new Set();
|
|
106
|
+
for (const share of shares) {
|
|
107
|
+
if (typeof share.index !== "number" || share.index < 1 || share.index > 255) {
|
|
108
|
+
const err = new Error(`Invalid share index: ${share.index}. Must be 1-255`);
|
|
109
|
+
err.code = SSSError.INVALID_SHARE_FORMAT;
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
if (!Buffer.isBuffer(share.data) && !(share.data instanceof Uint8Array)) {
|
|
113
|
+
const err = new Error("Share data must be a Buffer or Uint8Array");
|
|
114
|
+
err.code = SSSError.INVALID_SHARE_FORMAT;
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
if (share.data.length !== secretLength) {
|
|
118
|
+
const err = new Error(`Share data length mismatch: expected ${secretLength}, got ${share.data.length}`);
|
|
119
|
+
err.code = SSSError.SHARE_INDEX_MISMATCH;
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
if (seenIndices.has(share.index)) {
|
|
123
|
+
const err = new Error(`Duplicate share index: ${share.index}`);
|
|
124
|
+
err.code = SSSError.DUPLICATE_SHARE_INDEX;
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
seenIndices.add(share.index);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const secret = Buffer.alloc(secretLength);
|
|
131
|
+
for (let byteIdx = 0; byteIdx < secretLength; byteIdx++) {
|
|
132
|
+
const points = shares.map((share) => ({ x: share.index, y: share.data[byteIdx] }));
|
|
133
|
+
secret[byteIdx] = lagrangeInterpolate(points);
|
|
134
|
+
}
|
|
135
|
+
return secret;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Generate a new random secret
|
|
140
|
+
*
|
|
141
|
+
* @param {number} length - Length in bytes (default: 32)
|
|
142
|
+
* @returns {Buffer} Random secret
|
|
143
|
+
*/
|
|
144
|
+
function generateSecret(length = 32) {
|
|
145
|
+
return crypto.randomBytes(length);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
split,
|
|
150
|
+
combine,
|
|
151
|
+
serializeShare,
|
|
152
|
+
deserializeShare,
|
|
153
|
+
verifyShareFormat,
|
|
154
|
+
generateSecret,
|
|
155
|
+
SSSError,
|
|
156
|
+
_gfMul: gfMul,
|
|
157
|
+
_gfDiv: gfDiv,
|
|
158
|
+
_gfAdd: gfAdd,
|
|
159
|
+
_evaluatePolynomial: evaluatePolynomial,
|
|
160
|
+
_lagrangeInterpolate: lagrangeInterpolate,
|
|
161
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Two-of-three message types, errors, and default configuration
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Message types for two-of-three protocol
|
|
8
|
+
* @enum {string}
|
|
9
|
+
*/
|
|
10
|
+
const TwoOfThreeMessageType = {
|
|
11
|
+
ENROLLMENT_START: "key_recovery_enrollment_start",
|
|
12
|
+
ENROLLMENT_CHALLENGE: "key_recovery_enrollment_challenge",
|
|
13
|
+
ENROLLMENT_FINISH: "key_recovery_enrollment_finish",
|
|
14
|
+
ENROLLMENT_OK: "key_recovery_enrollment_ok",
|
|
15
|
+
ENROLLMENT_FAIL: "key_recovery_enrollment_fail",
|
|
16
|
+
RECOVERY_START: "key_recovery_start",
|
|
17
|
+
RECOVERY_SHARES: "key_recovery_shares",
|
|
18
|
+
RECOVERY_COMPLETE: "key_recovery_complete",
|
|
19
|
+
RECOVERY_OK: "key_recovery_ok",
|
|
20
|
+
RECOVERY_FAIL: "key_recovery_fail",
|
|
21
|
+
ROTATION_START: "key_recovery_rotation_start",
|
|
22
|
+
ROTATION_OK: "key_recovery_rotation_ok",
|
|
23
|
+
ROTATION_FAIL: "key_recovery_rotation_fail",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Error codes for two-of-three operations
|
|
28
|
+
* @enum {string}
|
|
29
|
+
*/
|
|
30
|
+
const TwoOfThreeError = {
|
|
31
|
+
INSUFFICIENT_FACTORS: "INSUFFICIENT_FACTORS",
|
|
32
|
+
INVALID_FACTOR: "INVALID_FACTOR",
|
|
33
|
+
SHARE_DECRYPTION_FAILED: "SHARE_DECRYPTION_FAILED",
|
|
34
|
+
RECONSTRUCTION_FAILED: "RECONSTRUCTION_FAILED",
|
|
35
|
+
NOT_ENROLLED: "NOT_ENROLLED",
|
|
36
|
+
ALREADY_ENROLLED: "ALREADY_ENROLLED",
|
|
37
|
+
ENROLLMENT_EXPIRED: "ENROLLMENT_EXPIRED",
|
|
38
|
+
INVALID_FLOW: "INVALID_FLOW",
|
|
39
|
+
REVOKED_SHARE: "REVOKED_SHARE",
|
|
40
|
+
INVALID_PROOF: "INVALID_PROOF",
|
|
41
|
+
PENDING_ENROLLMENT: "PENDING_ENROLLMENT",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default configuration
|
|
46
|
+
*/
|
|
47
|
+
const DEFAULT_CONFIG = {
|
|
48
|
+
requiredFactors: 2,
|
|
49
|
+
allowedFlows: ["oauth+totp", "oauth+webauthn", "webauthn+totp"],
|
|
50
|
+
enrollmentTimeout: 300000,
|
|
51
|
+
secretLength: 32,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
TwoOfThreeMessageType,
|
|
56
|
+
TwoOfThreeError,
|
|
57
|
+
DEFAULT_CONFIG,
|
|
58
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Two-of-Three Module
|
|
2
|
+
|
|
3
|
+
2-of-3 key recovery adapter components.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
two-of-three/
|
|
9
|
+
├── constants.js - Message types, error codes, default config
|
|
10
|
+
├── handlers.js - Enrollment and recovery handler factories
|
|
11
|
+
└── helpers.js - Cleanup and utility functions
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Files
|
|
15
|
+
|
|
16
|
+
### `constants.js`
|
|
17
|
+
Defines TwoOfThreeMessageType, TwoOfThreeError, and DEFAULT_CONFIG.
|
|
18
|
+
|
|
19
|
+
### `handlers.js`
|
|
20
|
+
Factory functions for enrollment start/finish, recovery start/complete, and rotation handlers.
|
|
21
|
+
|
|
22
|
+
### `helpers.js`
|
|
23
|
+
Pending state cleanup and expiry management utilities.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Two-of-three enrollment, recovery, and rotation handlers
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const { serializeShare, generateSecret } = require("../sss");
|
|
8
|
+
const { generateSalt } = require("../crypto-utils");
|
|
9
|
+
const { ShareId, FactorType } = require("../ledger");
|
|
10
|
+
const { TwoOfThreeMessageType, TwoOfThreeError } = require("./constants");
|
|
11
|
+
const { generateChallenge, computeProofHash, getEnrollmentKey } = require("./helpers");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create enrollment handlers
|
|
15
|
+
* @param {Object} ctx - Context with ledger, maps, and config
|
|
16
|
+
* @returns {Object} Enrollment handler functions
|
|
17
|
+
*/
|
|
18
|
+
function createEnrollmentHandlers(ctx) {
|
|
19
|
+
const { ledger, pendingEnrollments, enrollmentTimeout, secretLength, cleanupExpired } = ctx;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handle enrollment start
|
|
23
|
+
* @param {Object} params - Parameters
|
|
24
|
+
* @param {string} params.clientId - Client ID
|
|
25
|
+
* @param {string} params.userId - User ID
|
|
26
|
+
* @returns {Promise<Object>} Enrollment challenge
|
|
27
|
+
*/
|
|
28
|
+
async function handleEnrollmentStart({ clientId, userId }) {
|
|
29
|
+
cleanupExpired();
|
|
30
|
+
|
|
31
|
+
if (await ledger.isEnrolled(userId)) {
|
|
32
|
+
const err = new Error(`User ${userId} is already enrolled`);
|
|
33
|
+
err.code = TwoOfThreeError.ALREADY_ENROLLED;
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const key = getEnrollmentKey(clientId, userId);
|
|
38
|
+
if (pendingEnrollments.has(key)) {
|
|
39
|
+
const err = new Error("Enrollment already in progress");
|
|
40
|
+
err.code = TwoOfThreeError.PENDING_ENROLLMENT;
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const kUser = generateSecret(secretLength);
|
|
45
|
+
const shares = require("../sss").split(kUser, 2, 3);
|
|
46
|
+
const challenge = generateChallenge();
|
|
47
|
+
const s3Salt = generateSalt(16);
|
|
48
|
+
|
|
49
|
+
pendingEnrollments.set(key, { userId, challenge, kUser, shares, s3Salt, expiresAt: Date.now() + enrollmentTimeout });
|
|
50
|
+
setTimeout(() => pendingEnrollments.delete(key), enrollmentTimeout + 1000);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
type: TwoOfThreeMessageType.ENROLLMENT_CHALLENGE,
|
|
54
|
+
challenge,
|
|
55
|
+
s3Salt: s3Salt.toString("base64"),
|
|
56
|
+
factorRequirements: {
|
|
57
|
+
S1: { factor: "oauth", description: "OAuth/OPAQUE authentication" },
|
|
58
|
+
S2: { factor: "webauthn", description: "WebAuthn credential" },
|
|
59
|
+
S3: { factor: "totp", description: "TOTP authenticator" },
|
|
60
|
+
},
|
|
61
|
+
shares: { S1: serializeShare(shares[0]), S2: serializeShare(shares[1]), S3: serializeShare(shares[2]) },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Handle enrollment finish
|
|
67
|
+
* @param {Object} params - Parameters
|
|
68
|
+
* @param {string} params.clientId - Client ID
|
|
69
|
+
* @param {string} params.userId - User ID
|
|
70
|
+
* @param {Object} params.encryptedShares - Encrypted shares
|
|
71
|
+
* @returns {Promise<Object>} Enrollment result
|
|
72
|
+
*/
|
|
73
|
+
async function handleEnrollmentFinish({ clientId, userId, encryptedShares }) {
|
|
74
|
+
cleanupExpired();
|
|
75
|
+
|
|
76
|
+
const key = getEnrollmentKey(clientId, userId);
|
|
77
|
+
const pending = pendingEnrollments.get(key);
|
|
78
|
+
|
|
79
|
+
if (!pending) {
|
|
80
|
+
const err = new Error("No pending enrollment found or enrollment expired");
|
|
81
|
+
err.code = TwoOfThreeError.ENROLLMENT_EXPIRED;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (pending.expiresAt < Date.now()) {
|
|
86
|
+
pendingEnrollments.delete(key);
|
|
87
|
+
const err = new Error("Enrollment has expired");
|
|
88
|
+
err.code = TwoOfThreeError.ENROLLMENT_EXPIRED;
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!encryptedShares?.S1 || !encryptedShares?.S3) {
|
|
93
|
+
const err = new Error("Missing encrypted shares (S1 and S3 required)");
|
|
94
|
+
err.code = TwoOfThreeError.INVALID_FACTOR;
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const proofHash = computeProofHash(pending.kUser);
|
|
99
|
+
|
|
100
|
+
await ledger.storeShares(userId, {
|
|
101
|
+
[ShareId.S1]: { factor: FactorType.OAUTH, data: Buffer.from(encryptedShares.S1, "base64") },
|
|
102
|
+
[ShareId.S3]: { factor: FactorType.TOTP, data: Buffer.from(encryptedShares.S3, "base64") },
|
|
103
|
+
}, { proofHash });
|
|
104
|
+
|
|
105
|
+
pendingEnrollments.delete(key);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
type: TwoOfThreeMessageType.ENROLLMENT_OK,
|
|
109
|
+
userId,
|
|
110
|
+
shares: {
|
|
111
|
+
S1: { stored: true, factor: FactorType.OAUTH },
|
|
112
|
+
S2: { stored: false, factor: FactorType.WEBAUTHN, note: "Client-stored" },
|
|
113
|
+
S3: { stored: true, factor: FactorType.TOTP },
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { handleEnrollmentStart, handleEnrollmentFinish };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create recovery handlers
|
|
123
|
+
* @param {Object} ctx - Context with ledger, maps, and config
|
|
124
|
+
* @returns {Object} Recovery handler functions
|
|
125
|
+
*/
|
|
126
|
+
function createRecoveryHandlers(ctx) {
|
|
127
|
+
const { ledger, pendingRecoveries, enrollmentTimeout, cleanupExpired } = ctx;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Handle recovery start
|
|
131
|
+
* @param {Object} params - Parameters
|
|
132
|
+
* @param {string} params.clientId - Client ID
|
|
133
|
+
* @param {string} params.userId - User ID
|
|
134
|
+
* @returns {Promise<Object>} Recovery shares and challenge
|
|
135
|
+
*/
|
|
136
|
+
async function handleRecoveryStart({ clientId, userId }) {
|
|
137
|
+
cleanupExpired();
|
|
138
|
+
|
|
139
|
+
if (!(await ledger.isEnrolled(userId))) {
|
|
140
|
+
const err = new Error(`User ${userId} is not enrolled in key recovery`);
|
|
141
|
+
err.code = TwoOfThreeError.NOT_ENROLLED;
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { shares, metadata } = await ledger.fetchShares(userId, [ShareId.S1, ShareId.S3]);
|
|
146
|
+
const challenge = generateChallenge();
|
|
147
|
+
const key = getEnrollmentKey(clientId, userId);
|
|
148
|
+
pendingRecoveries.set(key, { userId, challenge, expiresAt: Date.now() + enrollmentTimeout });
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
type: TwoOfThreeMessageType.RECOVERY_SHARES,
|
|
152
|
+
challenge,
|
|
153
|
+
encShares: { S1: shares[ShareId.S1]?.toString("base64") || null, S3: shares[ShareId.S3]?.toString("base64") || null },
|
|
154
|
+
metadata: { S1: metadata[ShareId.S1], S2: metadata[ShareId.S2], S3: metadata[ShareId.S3] },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handle recovery complete
|
|
160
|
+
* @param {Object} params - Parameters
|
|
161
|
+
* @param {string} params.clientId - Client ID
|
|
162
|
+
* @param {string} params.userId - User ID
|
|
163
|
+
* @param {string} params.proof - Recovery proof
|
|
164
|
+
* @returns {Promise<Object>} Recovery result
|
|
165
|
+
*/
|
|
166
|
+
async function handleRecoveryComplete({ clientId, userId, proof }) {
|
|
167
|
+
cleanupExpired();
|
|
168
|
+
|
|
169
|
+
const key = getEnrollmentKey(clientId, userId);
|
|
170
|
+
const pending = pendingRecoveries.get(key);
|
|
171
|
+
|
|
172
|
+
if (!pending) {
|
|
173
|
+
const err = new Error("No pending recovery found");
|
|
174
|
+
err.code = TwoOfThreeError.INVALID_FLOW;
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (proof) {
|
|
179
|
+
const storedProofHash = await ledger.getProofHash(userId);
|
|
180
|
+
if (storedProofHash) {
|
|
181
|
+
const expectedProof = crypto.createHmac("sha256", storedProofHash).update(pending.challenge).digest("base64");
|
|
182
|
+
if (proof !== expectedProof) {
|
|
183
|
+
const err = new Error("Invalid recovery proof");
|
|
184
|
+
err.code = TwoOfThreeError.INVALID_PROOF;
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
pendingRecoveries.delete(key);
|
|
191
|
+
return { type: TwoOfThreeMessageType.RECOVERY_OK, userId, tier: 3 };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { handleRecoveryStart, handleRecoveryComplete };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create rotation handler
|
|
199
|
+
* @param {Object} ctx - Context with ledger
|
|
200
|
+
* @returns {Object} Rotation handler function
|
|
201
|
+
*/
|
|
202
|
+
function createRotationHandler(ctx) {
|
|
203
|
+
const { ledger } = ctx;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Handle share rotation
|
|
207
|
+
* @param {Object} params - Parameters
|
|
208
|
+
* @param {string} params.userId - User ID
|
|
209
|
+
* @param {string} params.shareId - Share ID
|
|
210
|
+
* @param {string} params.encryptedShare - Encrypted share data
|
|
211
|
+
* @param {string} params.reason - Rotation reason
|
|
212
|
+
* @returns {Promise<Object>} Rotation result
|
|
213
|
+
*/
|
|
214
|
+
async function handleRotation({ userId, shareId, encryptedShare, reason = "rotation" }) {
|
|
215
|
+
if (!Object.values(ShareId).includes(shareId)) {
|
|
216
|
+
const err = new Error(`Invalid share ID: ${shareId}`);
|
|
217
|
+
err.code = TwoOfThreeError.INVALID_FACTOR;
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!(await ledger.isEnrolled(userId))) {
|
|
222
|
+
const err = new Error(`User ${userId} is not enrolled`);
|
|
223
|
+
err.code = TwoOfThreeError.NOT_ENROLLED;
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const metadata = await ledger.getShareMetadata(userId, shareId);
|
|
228
|
+
|
|
229
|
+
if (shareId === ShareId.S2) {
|
|
230
|
+
const result = await ledger.updateS2Metadata(userId, metadata.version + 1);
|
|
231
|
+
return { type: TwoOfThreeMessageType.ROTATION_OK, shareId, oldVersion: result.oldVersion, newVersion: result.newVersion };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const result = await ledger.rotateShare(userId, shareId, { factor: metadata.factor, data: Buffer.from(encryptedShare, "base64") }, reason);
|
|
235
|
+
return { type: TwoOfThreeMessageType.ROTATION_OK, shareId, oldVersion: result.oldVersion, newVersion: result.newVersion };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { handleRotation };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { createEnrollmentHandlers, createRecoveryHandlers, createRotationHandler };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Two-of-three helper functions
|
|
3
|
+
*/
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const { timingSafeEqual } = require("../crypto-utils");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate challenge nonce
|
|
11
|
+
* @returns {string} Base64url challenge
|
|
12
|
+
*/
|
|
13
|
+
function generateChallenge() {
|
|
14
|
+
return crypto.randomBytes(32).toString("base64url");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute proof hash for K_user verification
|
|
19
|
+
* @param {Buffer} kUser - The user's key
|
|
20
|
+
* @returns {Buffer} SHA-256 hash of K_user
|
|
21
|
+
*/
|
|
22
|
+
function computeProofHash(kUser) {
|
|
23
|
+
return crypto.createHash("sha256").update(kUser).digest();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Verify proof of K_user possession
|
|
28
|
+
* @param {Buffer} kUser - Claimed K_user
|
|
29
|
+
* @param {Buffer} storedProofHash - Stored proof hash
|
|
30
|
+
* @returns {boolean} True if proof is valid
|
|
31
|
+
*/
|
|
32
|
+
function verifyProof(kUser, storedProofHash) {
|
|
33
|
+
const computedHash = computeProofHash(kUser);
|
|
34
|
+
return timingSafeEqual(computedHash, storedProofHash);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get enrollment key for pending operations
|
|
39
|
+
* @param {string} clientId - Client identifier
|
|
40
|
+
* @param {string} userId - User identifier
|
|
41
|
+
* @returns {string} Combined key
|
|
42
|
+
*/
|
|
43
|
+
function getEnrollmentKey(clientId, userId) {
|
|
44
|
+
return `${clientId}:${userId}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a pending operation cleanup function
|
|
49
|
+
* @param {Map} pendingEnrollments - Pending enrollments map
|
|
50
|
+
* @param {Map} pendingRecoveries - Pending recoveries map
|
|
51
|
+
* @returns {Function} Cleanup function
|
|
52
|
+
*/
|
|
53
|
+
function createCleanupExpired(pendingEnrollments, pendingRecoveries) {
|
|
54
|
+
return function cleanupExpired() {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
for (const [key, pending] of pendingEnrollments) {
|
|
57
|
+
if (pending.expiresAt < now) pendingEnrollments.delete(key);
|
|
58
|
+
}
|
|
59
|
+
for (const [key, pending] of pendingRecoveries) {
|
|
60
|
+
if (pending.expiresAt < now) pendingRecoveries.delete(key);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
generateChallenge,
|
|
67
|
+
computeProofHash,
|
|
68
|
+
verifyProof,
|
|
69
|
+
getEnrollmentKey,
|
|
70
|
+
createCleanupExpired,
|
|
71
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Two-of-Three Authentication Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements 2-of-3 SSS key recovery for Tier 3 (HIGH_SECURITY).
|
|
5
|
+
* Compatible with Passport.js strategy interface.
|
|
6
|
+
*
|
|
7
|
+
* @module server/security/auth/mfa/two-of-three
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const { createLedger } = require("./ledger");
|
|
13
|
+
const { TwoOfThreeMessageType, TwoOfThreeError, DEFAULT_CONFIG } = require("./two-of-three/constants");
|
|
14
|
+
const { createCleanupExpired } = require("./two-of-three/helpers");
|
|
15
|
+
const { createEnrollmentHandlers, createRecoveryHandlers, createRotationHandler } = require("./two-of-three/handlers");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a Two-of-Three Strategy
|
|
19
|
+
*
|
|
20
|
+
* @param {Object|Function} options - Config or verify callback
|
|
21
|
+
* @param {Function} verify - Verify callback (Passport.js style)
|
|
22
|
+
* @returns {Object} Two-of-three adapter/strategy
|
|
23
|
+
*/
|
|
24
|
+
function createTwoOfThreeStrategy(options, verify) {
|
|
25
|
+
let config = {};
|
|
26
|
+
let verifyCallback = null;
|
|
27
|
+
|
|
28
|
+
if (typeof options === "function") {
|
|
29
|
+
verifyCallback = options;
|
|
30
|
+
} else {
|
|
31
|
+
config = options || {};
|
|
32
|
+
verifyCallback = verify;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
requiredFactors = DEFAULT_CONFIG.requiredFactors,
|
|
37
|
+
allowedFlows = DEFAULT_CONFIG.allowedFlows,
|
|
38
|
+
enrollmentTimeout = DEFAULT_CONFIG.enrollmentTimeout,
|
|
39
|
+
secretLength = DEFAULT_CONFIG.secretLength,
|
|
40
|
+
passReqToCallback = false,
|
|
41
|
+
getRecord, saveRecord, deleteRecord,
|
|
42
|
+
auditEnabled = true, onAuditEvent,
|
|
43
|
+
} = config;
|
|
44
|
+
|
|
45
|
+
const ledger = createLedger({ getRecord, saveRecord, deleteRecord, auditEnabled, onAuditEvent });
|
|
46
|
+
const pendingEnrollments = new Map();
|
|
47
|
+
const pendingRecoveries = new Map();
|
|
48
|
+
const cleanupExpired = createCleanupExpired(pendingEnrollments, pendingRecoveries);
|
|
49
|
+
|
|
50
|
+
const ctx = { ledger, pendingEnrollments, pendingRecoveries, enrollmentTimeout, secretLength, cleanupExpired };
|
|
51
|
+
const { handleEnrollmentStart, handleEnrollmentFinish } = createEnrollmentHandlers(ctx);
|
|
52
|
+
const { handleRecoveryStart, handleRecoveryComplete } = createRecoveryHandlers(ctx);
|
|
53
|
+
const { handleRotation } = createRotationHandler(ctx);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Passport.js authenticate method
|
|
57
|
+
* @param {Object} req - Request object with recovery data
|
|
58
|
+
* @param {Object} authOptions - Authentication options
|
|
59
|
+
*/
|
|
60
|
+
function authenticate(req, authOptions = {}) {
|
|
61
|
+
const self = this;
|
|
62
|
+
const { userId, factors, proof } = req.body || req;
|
|
63
|
+
|
|
64
|
+
if (!factors || Object.keys(factors).length < requiredFactors) {
|
|
65
|
+
return self.fail({ message: `At least ${requiredFactors} factors required`, code: TwoOfThreeError.INSUFFICIENT_FACTORS });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const factorKeys = Object.keys(factors).sort().join("+");
|
|
69
|
+
const normalizedFlow = factorKeys.toLowerCase();
|
|
70
|
+
if (!allowedFlows.some((f) => f === normalizedFlow || f.split("+").sort().join("+") === normalizedFlow)) {
|
|
71
|
+
return self.fail({ message: `Flow ${normalizedFlow} not allowed`, code: TwoOfThreeError.INVALID_FLOW });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (verifyCallback) {
|
|
75
|
+
/**
|
|
76
|
+
* Passport.js verify callback handler
|
|
77
|
+
* @param {Error|null} err - Error if verification failed
|
|
78
|
+
* @param {Object|false} user - User object if verified, false if not
|
|
79
|
+
* @param {Object} info - Additional info about verification
|
|
80
|
+
* @returns {void}
|
|
81
|
+
*/
|
|
82
|
+
const verified = (err, user, info) => {
|
|
83
|
+
if (err) return self.error(err);
|
|
84
|
+
if (!user) return self.fail(info);
|
|
85
|
+
return self.success(user, info);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (passReqToCallback) return verifyCallback(req, { userId, factors, proof }, verified);
|
|
89
|
+
return verifyCallback({ userId, factors, proof }, verified);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
self.success({ userId, tier: 3 }, { factors: Object.keys(factors) });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Clean up pending operations for a client
|
|
97
|
+
* @param {string} clientId - Client ID
|
|
98
|
+
* @returns {void}
|
|
99
|
+
*/
|
|
100
|
+
function cleanupClient(clientId) {
|
|
101
|
+
for (const key of pendingEnrollments.keys()) {
|
|
102
|
+
if (key.startsWith(`${clientId}:`)) pendingEnrollments.delete(key);
|
|
103
|
+
}
|
|
104
|
+
for (const key of pendingRecoveries.keys()) {
|
|
105
|
+
if (key.startsWith(`${clientId}:`)) pendingRecoveries.delete(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
name: "two-of-three",
|
|
111
|
+
authenticate,
|
|
112
|
+
type: "two-of-three",
|
|
113
|
+
tier: 3,
|
|
114
|
+
MessageType: TwoOfThreeMessageType,
|
|
115
|
+
Error: TwoOfThreeError,
|
|
116
|
+
handleEnrollmentStart,
|
|
117
|
+
handleEnrollmentFinish,
|
|
118
|
+
handleRecoveryStart,
|
|
119
|
+
handleRecoveryComplete,
|
|
120
|
+
handleRotation,
|
|
121
|
+
isEnrolled: (userId) => ledger.isEnrolled(userId),
|
|
122
|
+
cleanupClient,
|
|
123
|
+
getShareVersions: (userId) => ledger.getVersions(userId),
|
|
124
|
+
ledger,
|
|
125
|
+
_pendingEnrollments: pendingEnrollments,
|
|
126
|
+
_pendingRecoveries: pendingRecoveries,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
createTwoOfThreeStrategy,
|
|
132
|
+
Strategy: createTwoOfThreeStrategy,
|
|
133
|
+
TwoOfThreeMessageType,
|
|
134
|
+
TwoOfThreeError,
|
|
135
|
+
DEFAULT_CONFIG,
|
|
136
|
+
};
|