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.
Files changed (186) hide show
  1. package/README.md +58 -570
  2. package/client/README.md +73 -14
  3. package/client/auth/crypto/aead.js +214 -0
  4. package/client/auth/crypto/constants.js +32 -0
  5. package/client/auth/crypto/encoding.js +104 -0
  6. package/client/auth/crypto/files.md +27 -0
  7. package/client/auth/crypto/kdf.js +217 -0
  8. package/client/auth/crypto-utils.js +118 -0
  9. package/client/auth/files.md +52 -0
  10. package/client/auth/key-recovery.js +288 -0
  11. package/client/auth/recovery/constants.js +37 -0
  12. package/client/auth/recovery/files.md +23 -0
  13. package/client/auth/recovery/key-derivation.js +61 -0
  14. package/client/auth/recovery/sss-browser.js +189 -0
  15. package/client/auth/share-storage.js +205 -0
  16. package/client/auth/storage/constants.js +18 -0
  17. package/client/auth/storage/db.js +132 -0
  18. package/client/auth/storage/files.md +27 -0
  19. package/client/auth/storage/keys.js +173 -0
  20. package/client/auth/storage/shares.js +200 -0
  21. package/client/browser.js +190 -23
  22. package/client/connectSocket.js +418 -988
  23. package/client/connection/README.md +23 -0
  24. package/client/connection/fileDownload.js +256 -0
  25. package/client/connection/fileHandling.js +450 -0
  26. package/client/connection/fileUtils.js +346 -0
  27. package/client/connection/files.md +71 -0
  28. package/client/connection/messageHandler.js +105 -0
  29. package/client/connection/network.js +350 -0
  30. package/client/connection/proxy.js +233 -0
  31. package/client/connection/sender.js +333 -0
  32. package/client/connection/state.js +321 -0
  33. package/client/connection/subscriptions.js +151 -0
  34. package/client/files.md +53 -0
  35. package/client/index.js +298 -142
  36. package/client/transports/README.md +50 -0
  37. package/client/transports/files.md +41 -0
  38. package/client/transports/streamParser.js +195 -0
  39. package/client/transports/streaming.js +555 -202
  40. package/dist/ape.js +6 -1
  41. package/dist/ape.js.map +4 -4
  42. package/index.d.ts +38 -16
  43. package/package.json +32 -7
  44. package/server/README.md +287 -53
  45. package/server/adapters/README.md +28 -19
  46. package/server/adapters/files.md +68 -0
  47. package/server/adapters/firebase.js +543 -160
  48. package/server/adapters/index.js +362 -112
  49. package/server/adapters/mongo.js +530 -140
  50. package/server/adapters/postgres.js +534 -155
  51. package/server/adapters/redis.js +508 -143
  52. package/server/adapters/supabase.js +555 -186
  53. package/server/client/README.md +43 -0
  54. package/server/client/connection.js +586 -0
  55. package/server/client/files.md +40 -0
  56. package/server/client/index.js +342 -0
  57. package/server/files.md +54 -0
  58. package/server/index.js +332 -27
  59. package/server/lib/README.md +26 -0
  60. package/server/lib/broadcast/clients.js +219 -0
  61. package/server/lib/broadcast/files.md +58 -0
  62. package/server/lib/broadcast/index.js +57 -0
  63. package/server/lib/broadcast/publishProxy.js +110 -0
  64. package/server/lib/broadcast/pubsub.js +137 -0
  65. package/server/lib/broadcast/sendProxy.js +103 -0
  66. package/server/lib/bun.js +315 -99
  67. package/server/lib/fileTransfer/README.md +63 -0
  68. package/server/lib/fileTransfer/files.md +30 -0
  69. package/server/lib/fileTransfer/streaming.js +435 -0
  70. package/server/lib/fileTransfer.js +710 -326
  71. package/server/lib/files.md +111 -0
  72. package/server/lib/httpUtils.js +283 -0
  73. package/server/lib/loader.js +208 -7
  74. package/server/lib/longPolling/README.md +63 -0
  75. package/server/lib/longPolling/files.md +44 -0
  76. package/server/lib/longPolling/getHandler.js +365 -0
  77. package/server/lib/longPolling/postHandler.js +327 -0
  78. package/server/lib/longPolling.js +174 -221
  79. package/server/lib/main.js +369 -532
  80. package/server/lib/runtimes/README.md +42 -0
  81. package/server/lib/runtimes/bun.js +586 -0
  82. package/server/lib/runtimes/files.md +56 -0
  83. package/server/lib/runtimes/node.js +511 -0
  84. package/server/lib/wiring.js +539 -98
  85. package/server/lib/ws/README.md +35 -0
  86. package/server/lib/ws/adapters/README.md +54 -0
  87. package/server/lib/ws/adapters/bun.js +538 -170
  88. package/server/lib/ws/adapters/deno.js +623 -149
  89. package/server/lib/ws/adapters/files.md +42 -0
  90. package/server/lib/ws/files.md +74 -0
  91. package/server/lib/ws/frames.js +532 -154
  92. package/server/lib/ws/index.js +207 -10
  93. package/server/lib/ws/server.js +385 -92
  94. package/server/lib/ws/socket.js +549 -181
  95. package/server/lib/wsProvider.js +363 -89
  96. package/server/plugins/binary.js +282 -0
  97. package/server/security/README.md +92 -0
  98. package/server/security/auth/README.md +319 -0
  99. package/server/security/auth/adapters/files.md +95 -0
  100. package/server/security/auth/adapters/ldap/constants.js +37 -0
  101. package/server/security/auth/adapters/ldap/files.md +19 -0
  102. package/server/security/auth/adapters/ldap/helpers.js +111 -0
  103. package/server/security/auth/adapters/ldap.js +353 -0
  104. package/server/security/auth/adapters/oauth2/constants.js +41 -0
  105. package/server/security/auth/adapters/oauth2/files.md +19 -0
  106. package/server/security/auth/adapters/oauth2/helpers.js +123 -0
  107. package/server/security/auth/adapters/oauth2.js +273 -0
  108. package/server/security/auth/adapters/opaque-handlers.js +314 -0
  109. package/server/security/auth/adapters/opaque.js +205 -0
  110. package/server/security/auth/adapters/saml/constants.js +52 -0
  111. package/server/security/auth/adapters/saml/files.md +19 -0
  112. package/server/security/auth/adapters/saml/helpers.js +74 -0
  113. package/server/security/auth/adapters/saml.js +173 -0
  114. package/server/security/auth/adapters/totp.js +703 -0
  115. package/server/security/auth/adapters/webauthn.js +625 -0
  116. package/server/security/auth/files.md +61 -0
  117. package/server/security/auth/framework/constants.js +27 -0
  118. package/server/security/auth/framework/files.md +23 -0
  119. package/server/security/auth/framework/handlers.js +272 -0
  120. package/server/security/auth/framework/socket-auth.js +177 -0
  121. package/server/security/auth/handlers/auth-messages.js +143 -0
  122. package/server/security/auth/handlers/files.md +28 -0
  123. package/server/security/auth/index.js +290 -0
  124. package/server/security/auth/mfa/crypto/aead.js +148 -0
  125. package/server/security/auth/mfa/crypto/constants.js +35 -0
  126. package/server/security/auth/mfa/crypto/files.md +27 -0
  127. package/server/security/auth/mfa/crypto/kdf.js +120 -0
  128. package/server/security/auth/mfa/crypto/utils.js +68 -0
  129. package/server/security/auth/mfa/crypto-utils.js +80 -0
  130. package/server/security/auth/mfa/files.md +77 -0
  131. package/server/security/auth/mfa/ledger/constants.js +75 -0
  132. package/server/security/auth/mfa/ledger/errors.js +73 -0
  133. package/server/security/auth/mfa/ledger/files.md +23 -0
  134. package/server/security/auth/mfa/ledger/share-record.js +32 -0
  135. package/server/security/auth/mfa/ledger.js +255 -0
  136. package/server/security/auth/mfa/recovery/constants.js +67 -0
  137. package/server/security/auth/mfa/recovery/files.md +19 -0
  138. package/server/security/auth/mfa/recovery/handlers.js +216 -0
  139. package/server/security/auth/mfa/recovery.js +191 -0
  140. package/server/security/auth/mfa/sss/constants.js +21 -0
  141. package/server/security/auth/mfa/sss/files.md +23 -0
  142. package/server/security/auth/mfa/sss/gf256.js +103 -0
  143. package/server/security/auth/mfa/sss/serialization.js +82 -0
  144. package/server/security/auth/mfa/sss.js +161 -0
  145. package/server/security/auth/mfa/two-of-three/constants.js +58 -0
  146. package/server/security/auth/mfa/two-of-three/files.md +23 -0
  147. package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
  148. package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
  149. package/server/security/auth/mfa/two-of-three.js +136 -0
  150. package/server/security/auth/nonce-manager.js +89 -0
  151. package/server/security/auth/state-machine-mfa.js +269 -0
  152. package/server/security/auth/state-machine.js +257 -0
  153. package/server/security/extractRootDomain.js +144 -16
  154. package/server/security/files.md +51 -0
  155. package/server/security/origin.js +197 -15
  156. package/server/security/reply.js +274 -16
  157. package/server/socket/README.md +119 -0
  158. package/server/socket/authMiddleware.js +299 -0
  159. package/server/socket/files.md +86 -0
  160. package/server/socket/open.js +154 -8
  161. package/server/socket/pluginHooks.js +334 -0
  162. package/server/socket/receive.js +184 -225
  163. package/server/socket/receiveContext.js +117 -0
  164. package/server/socket/send.js +416 -78
  165. package/server/socket/tagUtils.js +402 -0
  166. package/server/utils/README.md +19 -0
  167. package/server/utils/deepRequire.js +255 -30
  168. package/server/utils/files.md +57 -0
  169. package/server/utils/genId.js +182 -20
  170. package/server/utils/parseUserAgent.js +313 -251
  171. package/server/utils/userAgent/README.md +65 -0
  172. package/server/utils/userAgent/files.md +46 -0
  173. package/server/utils/userAgent/patterns.js +545 -0
  174. package/utils/README.md +21 -0
  175. package/utils/files.md +66 -0
  176. package/utils/jss/README.md +21 -0
  177. package/utils/jss/decode.js +471 -0
  178. package/utils/jss/encode.js +312 -0
  179. package/utils/jss/files.md +68 -0
  180. package/utils/jss/plugins.js +210 -0
  181. package/utils/jss.js +219 -273
  182. package/utils/messageHash.js +238 -35
  183. package/dist/api-ape.min.js +0 -2
  184. package/dist/api-ape.min.js.map +0 -7
  185. package/server/client.js +0 -308
  186. 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
+ };