api-ape 3.0.1 → 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 +58 -570
- 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 -202
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +32 -7
- package/server/README.md +287 -53
- package/server/adapters/README.md +28 -19
- 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 +332 -27
- 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 -221
- 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 -225
- 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 -308
- package/server/lib/broadcast.js +0 -146
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Key derivation functions for browser (HKDF, Argon2id, PBKDF2)
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
CryptoError,
|
|
8
|
+
AES_KEY_LENGTH,
|
|
9
|
+
PBKDF2_ITERATIONS,
|
|
10
|
+
} = require('./constants');
|
|
11
|
+
const {
|
|
12
|
+
isCryptoAvailable,
|
|
13
|
+
bufferToUint8Array,
|
|
14
|
+
stringToUint8Array,
|
|
15
|
+
} = require('./encoding');
|
|
16
|
+
|
|
17
|
+
// Argon2 WASM module (lazy loaded)
|
|
18
|
+
let _argon2Module = null;
|
|
19
|
+
let _argon2Loading = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load argon2-browser module
|
|
23
|
+
* @returns {Promise<Object|null>}
|
|
24
|
+
*/
|
|
25
|
+
async function loadArgon2() {
|
|
26
|
+
if (_argon2Module) return _argon2Module;
|
|
27
|
+
if (_argon2Loading) return _argon2Loading;
|
|
28
|
+
|
|
29
|
+
_argon2Loading = (async () => {
|
|
30
|
+
try {
|
|
31
|
+
if (typeof window !== 'undefined' && window.argon2) {
|
|
32
|
+
_argon2Module = window.argon2;
|
|
33
|
+
return _argon2Module;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const module = await import('argon2-browser');
|
|
38
|
+
_argon2Module = module.default || module;
|
|
39
|
+
return _argon2Module;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
} finally {
|
|
46
|
+
_argon2Loading = null;
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
|
|
50
|
+
return _argon2Loading;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if Argon2 is available
|
|
55
|
+
* @returns {Promise<boolean>}
|
|
56
|
+
*/
|
|
57
|
+
async function isArgon2Available() {
|
|
58
|
+
const argon2 = await loadArgon2();
|
|
59
|
+
return argon2 !== null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* HKDF key derivation using SHA-256
|
|
64
|
+
* @param {Uint8Array|string} inputKeyMaterial - Input key material
|
|
65
|
+
* @param {Uint8Array|string} salt - Salt value
|
|
66
|
+
* @param {Uint8Array|string} info - Context info
|
|
67
|
+
* @param {number} [length] - Output length
|
|
68
|
+
* @returns {Promise<Uint8Array>}
|
|
69
|
+
*/
|
|
70
|
+
async function hkdf(inputKeyMaterial, salt, info, length = AES_KEY_LENGTH) {
|
|
71
|
+
if (!isCryptoAvailable()) {
|
|
72
|
+
const err = new Error('Web Crypto API not available');
|
|
73
|
+
err.code = CryptoError.CRYPTO_NOT_AVAILABLE;
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ikmArray =
|
|
78
|
+
typeof inputKeyMaterial === 'string'
|
|
79
|
+
? stringToUint8Array(inputKeyMaterial)
|
|
80
|
+
: inputKeyMaterial;
|
|
81
|
+
|
|
82
|
+
const saltArray =
|
|
83
|
+
typeof salt === 'string'
|
|
84
|
+
? stringToUint8Array(salt)
|
|
85
|
+
: salt || new Uint8Array(0);
|
|
86
|
+
|
|
87
|
+
const infoArray =
|
|
88
|
+
typeof info === 'string' ? stringToUint8Array(info) : info;
|
|
89
|
+
|
|
90
|
+
const ikmKey = await crypto.subtle.importKey(
|
|
91
|
+
'raw',
|
|
92
|
+
ikmArray,
|
|
93
|
+
'HKDF',
|
|
94
|
+
false,
|
|
95
|
+
['deriveBits']
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const derivedBits = await crypto.subtle.deriveBits(
|
|
99
|
+
{
|
|
100
|
+
name: 'HKDF',
|
|
101
|
+
hash: 'SHA-256',
|
|
102
|
+
salt: saltArray,
|
|
103
|
+
info: infoArray,
|
|
104
|
+
},
|
|
105
|
+
ikmKey,
|
|
106
|
+
length * 8
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return bufferToUint8Array(derivedBits);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Derive key using Argon2id
|
|
114
|
+
* Falls back to PBKDF2 if argon2-browser not available.
|
|
115
|
+
* @param {Uint8Array|string} password - Password input
|
|
116
|
+
* @param {Uint8Array} salt - 16+ byte salt
|
|
117
|
+
* @param {Object} [options] - Argon2 options
|
|
118
|
+
* @param {number} [options.memoryCost] - Memory in KB
|
|
119
|
+
* @param {number} [options.timeCost] - Iterations
|
|
120
|
+
* @param {number} [options.parallelism] - Threads
|
|
121
|
+
* @param {number} [options.hashLength] - Output length
|
|
122
|
+
* @returns {Promise<Uint8Array>}
|
|
123
|
+
*/
|
|
124
|
+
async function argon2id(password, salt, options = {}) {
|
|
125
|
+
const {
|
|
126
|
+
memoryCost = 65536,
|
|
127
|
+
timeCost = 3,
|
|
128
|
+
parallelism = 4,
|
|
129
|
+
hashLength = AES_KEY_LENGTH,
|
|
130
|
+
} = options;
|
|
131
|
+
|
|
132
|
+
const passwordArray =
|
|
133
|
+
typeof password === 'string' ? stringToUint8Array(password) : password;
|
|
134
|
+
|
|
135
|
+
if (!(salt instanceof Uint8Array) || salt.length < 16) {
|
|
136
|
+
const err = new Error('Salt must be a Uint8Array of at least 16 bytes');
|
|
137
|
+
err.code = CryptoError.KDF_FAILED;
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const argon2 = await loadArgon2();
|
|
142
|
+
|
|
143
|
+
if (argon2) {
|
|
144
|
+
try {
|
|
145
|
+
const result = await argon2.hash({
|
|
146
|
+
pass: passwordArray,
|
|
147
|
+
salt,
|
|
148
|
+
type: argon2.ArgonType.Argon2id,
|
|
149
|
+
mem: memoryCost,
|
|
150
|
+
time: timeCost,
|
|
151
|
+
parallelism,
|
|
152
|
+
hashLen: hashLength,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return new Uint8Array(result.hash);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
const newErr = new Error(`Argon2id failed: ${err.message}`);
|
|
158
|
+
newErr.code = CryptoError.KDF_FAILED;
|
|
159
|
+
throw newErr;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return pbkdf2Fallback(passwordArray, salt, PBKDF2_ITERATIONS, hashLength);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* PBKDF2 fallback for environments without Argon2
|
|
168
|
+
* @param {Uint8Array|string} password - Password input
|
|
169
|
+
* @param {Uint8Array} salt - Salt value
|
|
170
|
+
* @param {number} [iterations] - PBKDF2 iterations
|
|
171
|
+
* @param {number} [keyLength] - Output key length
|
|
172
|
+
* @returns {Promise<Uint8Array>}
|
|
173
|
+
*/
|
|
174
|
+
async function pbkdf2Fallback(password, salt, iterations = PBKDF2_ITERATIONS, keyLength = AES_KEY_LENGTH) {
|
|
175
|
+
if (!isCryptoAvailable()) {
|
|
176
|
+
const err = new Error('Web Crypto API not available');
|
|
177
|
+
err.code = CryptoError.CRYPTO_NOT_AVAILABLE;
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const passwordArray =
|
|
182
|
+
typeof password === 'string' ? stringToUint8Array(password) : password;
|
|
183
|
+
|
|
184
|
+
if (!(salt instanceof Uint8Array)) {
|
|
185
|
+
const err = new Error('Salt must be a Uint8Array');
|
|
186
|
+
err.code = CryptoError.KDF_FAILED;
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const passwordKey = await crypto.subtle.importKey(
|
|
191
|
+
'raw',
|
|
192
|
+
passwordArray,
|
|
193
|
+
'PBKDF2',
|
|
194
|
+
false,
|
|
195
|
+
['deriveBits']
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const derivedBits = await crypto.subtle.deriveBits(
|
|
199
|
+
{
|
|
200
|
+
name: 'PBKDF2',
|
|
201
|
+
salt,
|
|
202
|
+
iterations,
|
|
203
|
+
hash: 'SHA-512',
|
|
204
|
+
},
|
|
205
|
+
passwordKey,
|
|
206
|
+
keyLength * 8
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return bufferToUint8Array(derivedBits);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
hkdf,
|
|
214
|
+
argon2id,
|
|
215
|
+
pbkdf2Fallback,
|
|
216
|
+
isArgon2Available,
|
|
217
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Browser Cryptographic Utilities for Key Recovery
|
|
3
|
+
*
|
|
4
|
+
* Provides AEAD encryption, key derivation (HKDF), and password-based
|
|
5
|
+
* key derivation (Argon2id) using Web Crypto API and argon2-browser.
|
|
6
|
+
*
|
|
7
|
+
* API compatible with server/security/auth/mfa/crypto-utils.js
|
|
8
|
+
*
|
|
9
|
+
* @module client/auth/crypto-utils
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
CryptoError,
|
|
15
|
+
AES_KEY_LENGTH,
|
|
16
|
+
GCM_NONCE_LENGTH,
|
|
17
|
+
GCM_TAG_LENGTH,
|
|
18
|
+
PBKDF2_ITERATIONS,
|
|
19
|
+
} = require('./crypto/constants');
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
isCryptoAvailable,
|
|
23
|
+
stringToUint8Array,
|
|
24
|
+
uint8ArrayToString,
|
|
25
|
+
uint8ArrayToBase64,
|
|
26
|
+
base64ToUint8Array,
|
|
27
|
+
randomBytes,
|
|
28
|
+
timingSafeEqual,
|
|
29
|
+
} = require('./crypto/encoding');
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
aeadEncrypt,
|
|
33
|
+
aeadDecrypt,
|
|
34
|
+
packEncrypted,
|
|
35
|
+
unpackEncrypted,
|
|
36
|
+
encryptAndPack,
|
|
37
|
+
unpackAndDecrypt,
|
|
38
|
+
} = require('./crypto/aead');
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
hkdf,
|
|
42
|
+
argon2id,
|
|
43
|
+
pbkdf2Fallback,
|
|
44
|
+
isArgon2Available,
|
|
45
|
+
} = require('./crypto/kdf');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate a random salt for KDF
|
|
49
|
+
* @param {number} [length] - Salt length in bytes
|
|
50
|
+
* @returns {Uint8Array}
|
|
51
|
+
*/
|
|
52
|
+
function generateSalt(length = 16) {
|
|
53
|
+
return randomBytes(length);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generate a random encryption key
|
|
58
|
+
* @param {number} [length] - Key length in bytes
|
|
59
|
+
* @returns {Uint8Array}
|
|
60
|
+
*/
|
|
61
|
+
function generateKey(length = AES_KEY_LENGTH) {
|
|
62
|
+
return randomBytes(length);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Derive a key from a master key for a specific purpose
|
|
67
|
+
* @param {Uint8Array} masterKey - Master key material
|
|
68
|
+
* @param {string} purpose - Key purpose identifier
|
|
69
|
+
* @param {number} [version] - Key version
|
|
70
|
+
* @returns {Promise<Uint8Array>}
|
|
71
|
+
*/
|
|
72
|
+
async function deriveKeyForPurpose(masterKey, purpose, version = 1) {
|
|
73
|
+
const info = `api-ape:key-recovery:${purpose}:v${version}`;
|
|
74
|
+
return hkdf(masterKey, '', info, AES_KEY_LENGTH);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
// AEAD
|
|
79
|
+
aeadEncrypt,
|
|
80
|
+
aeadDecrypt,
|
|
81
|
+
|
|
82
|
+
// Packing
|
|
83
|
+
packEncrypted,
|
|
84
|
+
unpackEncrypted,
|
|
85
|
+
encryptAndPack,
|
|
86
|
+
unpackAndDecrypt,
|
|
87
|
+
|
|
88
|
+
// KDF
|
|
89
|
+
hkdf,
|
|
90
|
+
argon2id,
|
|
91
|
+
pbkdf2Fallback,
|
|
92
|
+
isArgon2Available,
|
|
93
|
+
|
|
94
|
+
// Utilities
|
|
95
|
+
generateSalt,
|
|
96
|
+
generateKey,
|
|
97
|
+
randomBytes,
|
|
98
|
+
timingSafeEqual,
|
|
99
|
+
deriveKeyForPurpose,
|
|
100
|
+
|
|
101
|
+
// Encoding utilities
|
|
102
|
+
stringToUint8Array,
|
|
103
|
+
uint8ArrayToString,
|
|
104
|
+
uint8ArrayToBase64,
|
|
105
|
+
base64ToUint8Array,
|
|
106
|
+
|
|
107
|
+
// Constants
|
|
108
|
+
AES_KEY_LENGTH,
|
|
109
|
+
GCM_NONCE_LENGTH,
|
|
110
|
+
GCM_TAG_LENGTH,
|
|
111
|
+
PBKDF2_ITERATIONS,
|
|
112
|
+
|
|
113
|
+
// Errors
|
|
114
|
+
CryptoError,
|
|
115
|
+
|
|
116
|
+
// Environment checks
|
|
117
|
+
isCryptoAvailable,
|
|
118
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Client Authentication SDK Files
|
|
2
|
+
|
|
3
|
+
This directory contains the client-side SDK for key recovery and authentication.
|
|
4
|
+
|
|
5
|
+
## Guidelines
|
|
6
|
+
|
|
7
|
+
- **Browser-compatible** - Uses Web Crypto API and IndexedDB
|
|
8
|
+
- **Secure storage** - S2 share encrypted with WebAuthn-derived key
|
|
9
|
+
- **API compatible** - Crypto utilities match server-side interface
|
|
10
|
+
|
|
11
|
+
## Directory Structure
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
auth/
|
|
15
|
+
├── crypto-utils.js # Browser crypto utilities (Web Crypto API)
|
|
16
|
+
├── key-recovery.js # Key recovery client SDK
|
|
17
|
+
├── key-recovery.test.js # Client SDK tests
|
|
18
|
+
├── share-storage.js # IndexedDB share storage
|
|
19
|
+
└── share-storage.test.js # Storage tests
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Files
|
|
23
|
+
|
|
24
|
+
### `crypto-utils.js`
|
|
25
|
+
|
|
26
|
+
Browser-compatible cryptographic utilities:
|
|
27
|
+
|
|
28
|
+
- `aeadEncrypt/aeadDecrypt` - AES-256-GCM encryption with AEAD
|
|
29
|
+
- `hkdf` - RFC 5869 key derivation using Web Crypto API
|
|
30
|
+
- `argon2id` - Password-based KDF with PBKDF2 fallback
|
|
31
|
+
- `packEncrypted/unpackEncrypted` - Pack/unpack for storage
|
|
32
|
+
- API compatible with `server/security/auth/mfa/crypto-utils.js`
|
|
33
|
+
|
|
34
|
+
### `key-recovery.js`
|
|
35
|
+
|
|
36
|
+
Client SDK for 2-of-3 key recovery (Tier 3):
|
|
37
|
+
|
|
38
|
+
- `KeyRecoveryClient` - Main client class
|
|
39
|
+
- `enroll()` - Generate K_user, split shares, encrypt, store S2 locally
|
|
40
|
+
- `recover()` - Fetch encrypted shares, decrypt, combine to reconstruct
|
|
41
|
+
- `rotateShare()` - Handle share rotation after device loss
|
|
42
|
+
- GF(256) Shamir Secret Sharing implementation for browser
|
|
43
|
+
|
|
44
|
+
### `share-storage.js`
|
|
45
|
+
|
|
46
|
+
IndexedDB storage for S2 share (WebAuthn-gated):
|
|
47
|
+
|
|
48
|
+
- `saveShare/getShare` - Store/retrieve encrypted shares
|
|
49
|
+
- `saveWrappedKey/getWrappedKey` - Store/retrieve wrapped L_keys
|
|
50
|
+
- `saveMetadata/getMetadata` - Store enrollment metadata
|
|
51
|
+
- `createUserStorage()` - Convenience factory with bound userId
|
|
52
|
+
- Database schema: shares, keys, metadata stores
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Client SDK for 2-of-3 Key Recovery
|
|
3
|
+
*
|
|
4
|
+
* Provides client-side key reconstruction using Shamir Secret Sharing.
|
|
5
|
+
* Works with server-side two-of-three adapter for HIGH_SECURITY (Tier 3).
|
|
6
|
+
*
|
|
7
|
+
* @module client/auth/key-recovery
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const cryptoUtils = require('./crypto-utils');
|
|
12
|
+
const shareStorage = require('./share-storage');
|
|
13
|
+
const { KeyRecoveryError, FactorType } = require('./recovery/constants');
|
|
14
|
+
const { combineShares, deserializeShare, serializeShare, evaluatePolynomial } = require('./recovery/sss-browser');
|
|
15
|
+
const { deriveS1Key, deriveS2Key, deriveS3Key } = require('./recovery/key-derivation');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Client SDK for 2-of-3 key recovery
|
|
19
|
+
*/
|
|
20
|
+
class KeyRecoveryClient {
|
|
21
|
+
/**
|
|
22
|
+
* Create a KeyRecoveryClient
|
|
23
|
+
* @param {Object} options - Client options
|
|
24
|
+
* @param {Function} options.sendMessage - Server message function
|
|
25
|
+
* @param {string} [options.rpId] - WebAuthn relying party ID
|
|
26
|
+
*/
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
if (!options.sendMessage || typeof options.sendMessage !== 'function') {
|
|
29
|
+
throw new Error('sendMessage function is required');
|
|
30
|
+
}
|
|
31
|
+
this.sendMessage = options.sendMessage;
|
|
32
|
+
this.rpId = options.rpId || (typeof location !== 'undefined' ? location.hostname : 'localhost');
|
|
33
|
+
this._pendingEnrollment = null;
|
|
34
|
+
this._pendingRecovery = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if user is enrolled
|
|
39
|
+
* @param {string} userId - User identifier
|
|
40
|
+
* @returns {Promise<boolean>}
|
|
41
|
+
*/
|
|
42
|
+
async isEnrolled(userId) {
|
|
43
|
+
return shareStorage.isEnrolled(userId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Enroll in key recovery
|
|
48
|
+
* @param {Object} params - Enrollment parameters
|
|
49
|
+
* @param {string} params.userId - User identifier
|
|
50
|
+
* @param {string} params.oauthToken - OAuth token
|
|
51
|
+
* @param {string} params.totpSeed - TOTP seed
|
|
52
|
+
* @param {Uint8Array} params.webauthnAuthData - WebAuthn data
|
|
53
|
+
* @param {string} params.webauthnCredentialId - Credential ID
|
|
54
|
+
* @returns {Promise<Object>}
|
|
55
|
+
*/
|
|
56
|
+
async enroll(params) {
|
|
57
|
+
const { userId, oauthToken, totpSeed, webauthnAuthData, webauthnCredentialId } = params;
|
|
58
|
+
|
|
59
|
+
if (!userId) throw new Error('userId is required');
|
|
60
|
+
if (!oauthToken) throw new Error('oauthToken is required');
|
|
61
|
+
if (!totpSeed) throw new Error('totpSeed is required');
|
|
62
|
+
if (!webauthnAuthData) throw new Error('webauthnAuthData is required');
|
|
63
|
+
if (!webauthnCredentialId) throw new Error('webauthnCredentialId is required');
|
|
64
|
+
|
|
65
|
+
const enrolled = await this.isEnrolled(userId);
|
|
66
|
+
if (enrolled) {
|
|
67
|
+
const err = new Error('User already enrolled');
|
|
68
|
+
err.code = KeyRecoveryError.ALREADY_ENROLLED;
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const kUser = cryptoUtils.generateKey(32);
|
|
74
|
+
const shares = this._splitSecret(kUser, 2, 3);
|
|
75
|
+
|
|
76
|
+
const s1Salt = cryptoUtils.generateSalt(16);
|
|
77
|
+
const s3Salt = cryptoUtils.generateSalt(16);
|
|
78
|
+
|
|
79
|
+
const [s1Key, s2Key, s3Key] = await Promise.all([
|
|
80
|
+
deriveS1Key(oauthToken, userId),
|
|
81
|
+
deriveS2Key(webauthnAuthData, webauthnCredentialId),
|
|
82
|
+
deriveS3Key(totpSeed, s3Salt)
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const [encS1, encS2, encS3] = await Promise.all([
|
|
86
|
+
cryptoUtils.encryptAndPack(s1Key, shares[0].data, `s1:${userId}`),
|
|
87
|
+
cryptoUtils.encryptAndPack(s2Key, shares[1].data, `s2:${userId}`),
|
|
88
|
+
cryptoUtils.encryptAndPack(s3Key, shares[2].data, `s3:${userId}`)
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const storage = shareStorage.createUserStorage(userId);
|
|
92
|
+
await storage.saveShare('S2', cryptoUtils.uint8ArrayToBase64(encS2), 1);
|
|
93
|
+
await storage.saveWrappedKey(webauthnCredentialId, cryptoUtils.uint8ArrayToBase64(s2Key));
|
|
94
|
+
await storage.saveMetadata({
|
|
95
|
+
enrolledAt: Date.now(),
|
|
96
|
+
shareCount: 3,
|
|
97
|
+
s3Salt: cryptoUtils.uint8ArrayToBase64(s3Salt)
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const proof = await this._generateProof(kUser, userId);
|
|
101
|
+
|
|
102
|
+
const response = await this.sendMessage({
|
|
103
|
+
type: 'key_recovery_enrollment_finish',
|
|
104
|
+
userId,
|
|
105
|
+
encShares: {
|
|
106
|
+
S1: cryptoUtils.uint8ArrayToBase64(encS1),
|
|
107
|
+
S1_salt: cryptoUtils.uint8ArrayToBase64(s1Salt),
|
|
108
|
+
S3: cryptoUtils.uint8ArrayToBase64(encS3),
|
|
109
|
+
S3_salt: cryptoUtils.uint8ArrayToBase64(s3Salt)
|
|
110
|
+
},
|
|
111
|
+
shareIndices: { S1: shares[0].index, S2: shares[1].index, S3: shares[2].index },
|
|
112
|
+
proof
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (response.type === 'error') {
|
|
116
|
+
await storage.deleteAll();
|
|
117
|
+
const err = new Error(response.message || 'Enrollment failed');
|
|
118
|
+
err.code = KeyRecoveryError.SERVER_ERROR;
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { kUser, proof };
|
|
123
|
+
} catch (err) {
|
|
124
|
+
try { await shareStorage.deleteAllUserData(userId); } catch { /* ignore */ }
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Recover K_user using 2 of 3 factors
|
|
131
|
+
* @param {Object} params - Recovery parameters
|
|
132
|
+
* @param {string} params.userId - User identifier
|
|
133
|
+
* @param {Array<string>} params.factors - Factor types to use
|
|
134
|
+
* @param {Object} params.factorData - Data for each factor
|
|
135
|
+
* @returns {Promise<Uint8Array>}
|
|
136
|
+
*/
|
|
137
|
+
async recover(params) {
|
|
138
|
+
const { userId, factors, factorData = {} } = params;
|
|
139
|
+
|
|
140
|
+
if (!userId) throw new Error('userId is required');
|
|
141
|
+
|
|
142
|
+
// Check enrollment
|
|
143
|
+
const enrolled = await this.isEnrolled(userId);
|
|
144
|
+
if (!enrolled) {
|
|
145
|
+
const err = new Error('User not enrolled');
|
|
146
|
+
err.code = KeyRecoveryError.NOT_ENROLLED;
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Validate factors array
|
|
151
|
+
if (!Array.isArray(factors) || factors.length !== 2) {
|
|
152
|
+
const err = new Error('Exactly 2 factors required');
|
|
153
|
+
err.code = KeyRecoveryError.INSUFFICIENT_FACTORS;
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Validate factor types
|
|
158
|
+
const validFactors = Object.values(FactorType);
|
|
159
|
+
for (const factor of factors) {
|
|
160
|
+
if (!validFactors.includes(factor)) {
|
|
161
|
+
const err = new Error(`Unknown factor: ${factor}`);
|
|
162
|
+
err.code = KeyRecoveryError.INVALID_FACTOR;
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const response = await this.sendMessage({ type: 'key_recovery_start', userId });
|
|
168
|
+
|
|
169
|
+
if (response.type !== 'key_recovery_shares') {
|
|
170
|
+
const err = new Error(response.message || 'Recovery failed');
|
|
171
|
+
err.code = KeyRecoveryError.SERVER_ERROR;
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const decryptedShares = [];
|
|
176
|
+
|
|
177
|
+
if (factors.includes(FactorType.OAUTH) && response.encShares?.S1) {
|
|
178
|
+
const s1Key = await deriveS1Key(factorData.oauthToken, userId);
|
|
179
|
+
const encData = cryptoUtils.base64ToUint8Array(response.encShares.S1);
|
|
180
|
+
const shareData = await cryptoUtils.unpackAndDecrypt(s1Key, encData, `s1:${userId}`);
|
|
181
|
+
decryptedShares.push({ index: response.encShares.S1_index || 1, data: shareData });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (factors.includes(FactorType.WEBAUTHN)) {
|
|
185
|
+
const storage = shareStorage.createUserStorage(userId);
|
|
186
|
+
const share = await storage.getShare('S2');
|
|
187
|
+
if (share) {
|
|
188
|
+
const s2Key = await deriveS2Key(factorData.webauthnAuthData, factorData.webauthnCredentialId);
|
|
189
|
+
const encData = cryptoUtils.base64ToUint8Array(share.encryptedShare);
|
|
190
|
+
const shareData = await cryptoUtils.unpackAndDecrypt(s2Key, encData, `s2:${userId}`);
|
|
191
|
+
decryptedShares.push({ index: response.encShares?.S2_index || 2, data: shareData });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (factors.includes(FactorType.TOTP) && response.encShares?.S3) {
|
|
196
|
+
const metadata = await shareStorage.getMetadata(userId);
|
|
197
|
+
const s3Salt = metadata?.s3Salt ? cryptoUtils.base64ToUint8Array(metadata.s3Salt) : cryptoUtils.generateSalt(16);
|
|
198
|
+
const s3Key = await deriveS3Key(factorData.totpSeed, s3Salt);
|
|
199
|
+
const encData = cryptoUtils.base64ToUint8Array(response.encShares.S3);
|
|
200
|
+
const shareData = await cryptoUtils.unpackAndDecrypt(s3Key, encData, `s3:${userId}`);
|
|
201
|
+
decryptedShares.push({ index: response.encShares.S3_index || 3, data: shareData });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (decryptedShares.length < 2) {
|
|
205
|
+
const err = new Error('Could not decrypt enough shares');
|
|
206
|
+
err.code = KeyRecoveryError.DECRYPTION_FAILED;
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const kUser = combineShares(decryptedShares.slice(0, 2));
|
|
211
|
+
const proof = await this._generateProof(kUser, userId);
|
|
212
|
+
|
|
213
|
+
await this.sendMessage({ type: 'key_recovery_complete', userId, proof });
|
|
214
|
+
return kUser;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Unenroll from key recovery
|
|
219
|
+
* @param {string} userId - User identifier
|
|
220
|
+
* @returns {Promise<void>}
|
|
221
|
+
*/
|
|
222
|
+
async unenroll(userId) {
|
|
223
|
+
await shareStorage.deleteAllUserData(userId);
|
|
224
|
+
await this.sendMessage({ type: 'key_recovery_unenroll', userId });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Split secret using SSS
|
|
229
|
+
* @private
|
|
230
|
+
* @param {Uint8Array} secret - Secret to split
|
|
231
|
+
* @param {number} threshold - Threshold
|
|
232
|
+
* @param {number} totalShares - Total shares
|
|
233
|
+
* @returns {Array}
|
|
234
|
+
*/
|
|
235
|
+
_splitSecret(secret, threshold, totalShares) {
|
|
236
|
+
const shares = [];
|
|
237
|
+
for (let i = 0; i < totalShares; i++) {
|
|
238
|
+
shares.push({ index: i + 1, data: new Uint8Array(secret.length) });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (let byteIdx = 0; byteIdx < secret.length; byteIdx++) {
|
|
242
|
+
const coefficients = new Uint8Array(threshold);
|
|
243
|
+
coefficients[0] = secret[byteIdx];
|
|
244
|
+
for (let c = 1; c < threshold; c++) {
|
|
245
|
+
coefficients[c] = cryptoUtils.randomBytes(1)[0];
|
|
246
|
+
}
|
|
247
|
+
for (let shareIdx = 0; shareIdx < totalShares; shareIdx++) {
|
|
248
|
+
shares[shareIdx].data[byteIdx] = evaluatePolynomial(coefficients, shares[shareIdx].index);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return shares;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Generate K_user proof
|
|
257
|
+
* @private
|
|
258
|
+
* @param {Uint8Array} kUser - User key
|
|
259
|
+
* @param {string} userId - User identifier
|
|
260
|
+
* @returns {Promise<string>}
|
|
261
|
+
*/
|
|
262
|
+
async _generateProof(kUser, userId) {
|
|
263
|
+
const key = await crypto.subtle.importKey('raw', kUser, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
264
|
+
const data = cryptoUtils.stringToUint8Array(`api-ape:key-recovery:proof:${userId}`);
|
|
265
|
+
const signature = await crypto.subtle.sign('HMAC', key, data);
|
|
266
|
+
return cryptoUtils.uint8ArrayToBase64(new Uint8Array(signature));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const { gfMul, gfDiv, gfAdd, lagrangeInterpolate } = require('./recovery/sss-browser');
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
KeyRecoveryClient,
|
|
274
|
+
KeyRecoveryError,
|
|
275
|
+
FactorType,
|
|
276
|
+
deriveS1Key,
|
|
277
|
+
deriveS2Key,
|
|
278
|
+
deriveS3Key,
|
|
279
|
+
combineShares,
|
|
280
|
+
deserializeShare,
|
|
281
|
+
serializeShare,
|
|
282
|
+
|
|
283
|
+
// Expose for testing
|
|
284
|
+
_gfMul: gfMul,
|
|
285
|
+
_gfDiv: gfDiv,
|
|
286
|
+
_gfAdd: gfAdd,
|
|
287
|
+
_lagrangeInterpolate: lagrangeInterpolate,
|
|
288
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Key recovery error codes and factor types
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error codes for key recovery operations
|
|
8
|
+
* @enum {string}
|
|
9
|
+
*/
|
|
10
|
+
const KeyRecoveryError = {
|
|
11
|
+
NOT_ENROLLED: 'NOT_ENROLLED',
|
|
12
|
+
ALREADY_ENROLLED: 'ALREADY_ENROLLED',
|
|
13
|
+
INSUFFICIENT_FACTORS: 'INSUFFICIENT_FACTORS',
|
|
14
|
+
INVALID_FACTOR: 'INVALID_FACTOR',
|
|
15
|
+
DECRYPTION_FAILED: 'DECRYPTION_FAILED',
|
|
16
|
+
RECONSTRUCTION_FAILED: 'RECONSTRUCTION_FAILED',
|
|
17
|
+
SERVER_ERROR: 'SERVER_ERROR',
|
|
18
|
+
WEBAUTHN_FAILED: 'WEBAUTHN_FAILED',
|
|
19
|
+
SHARE_MISMATCH: 'SHARE_MISMATCH',
|
|
20
|
+
PROOF_FAILED: 'PROOF_FAILED',
|
|
21
|
+
STORAGE_ERROR: 'STORAGE_ERROR',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Factor types for authentication
|
|
26
|
+
* @enum {string}
|
|
27
|
+
*/
|
|
28
|
+
const FactorType = {
|
|
29
|
+
OAUTH: 'oauth',
|
|
30
|
+
WEBAUTHN: 'webauthn',
|
|
31
|
+
TOTP: 'totp',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
KeyRecoveryError,
|
|
36
|
+
FactorType,
|
|
37
|
+
};
|