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,23 @@
|
|
|
1
|
+
# Client Auth Recovery Module
|
|
2
|
+
|
|
3
|
+
Browser-side key recovery using Shamir Secret Sharing.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
recovery/
|
|
9
|
+
├── constants.js - Recovery error codes and factor types
|
|
10
|
+
├── key-derivation.js - Per-factor key derivation (S1, S2, S3)
|
|
11
|
+
└── sss-browser.js - Shamir Secret Sharing for browser (GF256)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Files
|
|
15
|
+
|
|
16
|
+
### `constants.js`
|
|
17
|
+
Defines KeyRecoveryError codes and FactorType enum (OAUTH, WEBAUTHN, TOTP).
|
|
18
|
+
|
|
19
|
+
### `key-derivation.js`
|
|
20
|
+
Derives encryption keys for each share: deriveS1Key (OAuth), deriveS2Key (WebAuthn), deriveS3Key (TOTP).
|
|
21
|
+
|
|
22
|
+
### `sss-browser.js`
|
|
23
|
+
Implements Shamir Secret Sharing over GF(256) for browsers. Includes Lagrange interpolation, share serialization, and Galois field arithmetic.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Factor-specific key derivation functions
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const cryptoUtils = require('../crypto-utils');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Derive S1 encryption key from OAuth token
|
|
10
|
+
* @param {string} oauthToken - OAuth access token
|
|
11
|
+
* @param {string} userId - User identifier
|
|
12
|
+
* @returns {Promise<Uint8Array>}
|
|
13
|
+
*/
|
|
14
|
+
async function deriveS1Key(oauthToken, userId) {
|
|
15
|
+
return cryptoUtils.hkdf(
|
|
16
|
+
cryptoUtils.stringToUint8Array(oauthToken),
|
|
17
|
+
cryptoUtils.stringToUint8Array(`api-ape:s1:${userId}`),
|
|
18
|
+
cryptoUtils.stringToUint8Array('S1_key'),
|
|
19
|
+
32
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Derive S2 encryption key from WebAuthn data
|
|
25
|
+
* @param {Uint8Array} authenticatorData - WebAuthn data
|
|
26
|
+
* @param {string} credentialId - Credential ID
|
|
27
|
+
* @returns {Promise<Uint8Array>}
|
|
28
|
+
*/
|
|
29
|
+
async function deriveS2Key(authenticatorData, credentialId) {
|
|
30
|
+
return cryptoUtils.hkdf(
|
|
31
|
+
authenticatorData,
|
|
32
|
+
cryptoUtils.stringToUint8Array(credentialId),
|
|
33
|
+
cryptoUtils.stringToUint8Array('S2_key'),
|
|
34
|
+
32
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Derive S3 encryption key from TOTP seed
|
|
40
|
+
* @param {string} totpSeed - TOTP seed
|
|
41
|
+
* @param {Uint8Array} salt - Salt value
|
|
42
|
+
* @returns {Promise<Uint8Array>}
|
|
43
|
+
*/
|
|
44
|
+
async function deriveS3Key(totpSeed, salt) {
|
|
45
|
+
return cryptoUtils.argon2id(
|
|
46
|
+
cryptoUtils.stringToUint8Array(totpSeed),
|
|
47
|
+
salt,
|
|
48
|
+
{
|
|
49
|
+
memoryCost: 65536,
|
|
50
|
+
timeCost: 3,
|
|
51
|
+
parallelism: 4,
|
|
52
|
+
hashLength: 32
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
deriveS1Key,
|
|
59
|
+
deriveS2Key,
|
|
60
|
+
deriveS3Key,
|
|
61
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Shamir Secret Sharing for browser (GF(256))
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const { KeyRecoveryError } = require('./constants');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pre-computed log and exp tables for GF(256)
|
|
10
|
+
* @type {Object}
|
|
11
|
+
*/
|
|
12
|
+
const GF256 = (function initGF256() {
|
|
13
|
+
const exp = new Uint8Array(512);
|
|
14
|
+
const log = new Uint8Array(256);
|
|
15
|
+
|
|
16
|
+
let x = 1;
|
|
17
|
+
for (let i = 0; i < 255; i++) {
|
|
18
|
+
exp[i] = x;
|
|
19
|
+
exp[i + 255] = x;
|
|
20
|
+
log[x] = i;
|
|
21
|
+
x = x ^ ((x << 1) ^ (x & 0x80 ? 0x11b : 0));
|
|
22
|
+
}
|
|
23
|
+
log[0] = 0;
|
|
24
|
+
exp[510] = exp[0];
|
|
25
|
+
|
|
26
|
+
return { exp, log };
|
|
27
|
+
})();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Multiply in GF(256)
|
|
31
|
+
* @param {number} a - First operand
|
|
32
|
+
* @param {number} b - Second operand
|
|
33
|
+
* @returns {number}
|
|
34
|
+
*/
|
|
35
|
+
function gfMul(a, b) {
|
|
36
|
+
if (a === 0 || b === 0) return 0;
|
|
37
|
+
return GF256.exp[GF256.log[a] + GF256.log[b]];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Divide in GF(256)
|
|
42
|
+
* @param {number} a - Dividend
|
|
43
|
+
* @param {number} b - Divisor
|
|
44
|
+
* @returns {number}
|
|
45
|
+
*/
|
|
46
|
+
function gfDiv(a, b) {
|
|
47
|
+
if (b === 0) throw new Error('Division by zero in GF(256)');
|
|
48
|
+
if (a === 0) return 0;
|
|
49
|
+
return GF256.exp[GF256.log[a] + 255 - GF256.log[b]];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add in GF(256) (XOR)
|
|
54
|
+
* @param {number} a - First operand
|
|
55
|
+
* @param {number} b - Second operand
|
|
56
|
+
* @returns {number}
|
|
57
|
+
*/
|
|
58
|
+
function gfAdd(a, b) {
|
|
59
|
+
return a ^ b;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Lagrange interpolation to find f(0)
|
|
64
|
+
* @param {Array} points - Array of point objects
|
|
65
|
+
* @returns {number}
|
|
66
|
+
*/
|
|
67
|
+
function lagrangeInterpolate(points) {
|
|
68
|
+
let result = 0;
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < points.length; i++) {
|
|
71
|
+
let numerator = 1;
|
|
72
|
+
let denominator = 1;
|
|
73
|
+
|
|
74
|
+
for (let j = 0; j < points.length; j++) {
|
|
75
|
+
if (i !== j) {
|
|
76
|
+
numerator = gfMul(numerator, points[j].x);
|
|
77
|
+
denominator = gfMul(denominator, gfAdd(points[i].x, points[j].x));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const basis = gfMul(points[i].y, gfDiv(numerator, denominator));
|
|
82
|
+
result = gfAdd(result, basis);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Combine shares to reconstruct secret
|
|
90
|
+
* @param {Array} shares - Share objects
|
|
91
|
+
* @returns {Uint8Array}
|
|
92
|
+
*/
|
|
93
|
+
function combineShares(shares) {
|
|
94
|
+
if (!Array.isArray(shares) || shares.length < 2) {
|
|
95
|
+
const err = new Error('At least 2 shares required');
|
|
96
|
+
err.code = KeyRecoveryError.INSUFFICIENT_FACTORS;
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const secretLength = shares[0].data.length;
|
|
101
|
+
const secret = new Uint8Array(secretLength);
|
|
102
|
+
|
|
103
|
+
for (let byteIdx = 0; byteIdx < secretLength; byteIdx++) {
|
|
104
|
+
const points = shares.map(share => ({
|
|
105
|
+
x: share.index,
|
|
106
|
+
y: share.data[byteIdx]
|
|
107
|
+
}));
|
|
108
|
+
secret[byteIdx] = lagrangeInterpolate(points);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return secret;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Deserialize share from base64url
|
|
116
|
+
* @param {string} serialized - Serialized share
|
|
117
|
+
* @returns {Object}
|
|
118
|
+
*/
|
|
119
|
+
function deserializeShare(serialized) {
|
|
120
|
+
if (typeof serialized !== 'string' || serialized.length === 0) {
|
|
121
|
+
const err = new Error('Invalid serialized share');
|
|
122
|
+
err.code = KeyRecoveryError.INVALID_FACTOR;
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const base64 = serialized.replace(/-/g, '+').replace(/_/g, '/');
|
|
127
|
+
const binary = atob(base64);
|
|
128
|
+
const packed = new Uint8Array(binary.length);
|
|
129
|
+
for (let i = 0; i < binary.length; i++) {
|
|
130
|
+
packed[i] = binary.charCodeAt(i);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (packed.length < 2) {
|
|
134
|
+
const err = new Error('Serialized share too short');
|
|
135
|
+
err.code = KeyRecoveryError.INVALID_FACTOR;
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
index: packed[0],
|
|
141
|
+
data: packed.slice(1)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Serialize share to base64url
|
|
147
|
+
* @param {Object} share - Share to serialize
|
|
148
|
+
* @param {number} share.index - Share index
|
|
149
|
+
* @param {Uint8Array} share.data - Share data
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
function serializeShare(share) {
|
|
153
|
+
const packed = new Uint8Array(1 + share.data.length);
|
|
154
|
+
packed[0] = share.index;
|
|
155
|
+
packed.set(share.data, 1);
|
|
156
|
+
|
|
157
|
+
let binary = '';
|
|
158
|
+
for (let i = 0; i < packed.length; i++) {
|
|
159
|
+
binary += String.fromCharCode(packed[i]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Evaluate polynomial at x using Horner's method
|
|
167
|
+
* @param {Uint8Array} coefficients - Polynomial coefficients
|
|
168
|
+
* @param {number} x - Point to evaluate
|
|
169
|
+
* @returns {number}
|
|
170
|
+
*/
|
|
171
|
+
function evaluatePolynomial(coefficients, x) {
|
|
172
|
+
let result = 0;
|
|
173
|
+
for (let i = coefficients.length - 1; i >= 0; i--) {
|
|
174
|
+
result = gfAdd(gfMul(result, x), coefficients[i]);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
combineShares,
|
|
181
|
+
deserializeShare,
|
|
182
|
+
serializeShare,
|
|
183
|
+
evaluatePolynomial,
|
|
184
|
+
lagrangeInterpolate,
|
|
185
|
+
gfMul,
|
|
186
|
+
gfDiv,
|
|
187
|
+
gfAdd,
|
|
188
|
+
GF256,
|
|
189
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file IndexedDB Storage for S2 Share (WebAuthn-gated)
|
|
3
|
+
*
|
|
4
|
+
* Stores the encrypted S2 share and wrapped L_key locally in the browser.
|
|
5
|
+
* S2 is encrypted with L_key, which is derived from WebAuthn authenticator data.
|
|
6
|
+
*
|
|
7
|
+
* @module client/auth/share-storage
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
DB_NAME,
|
|
13
|
+
DB_VERSION,
|
|
14
|
+
STORE_SHARES,
|
|
15
|
+
STORE_KEYS,
|
|
16
|
+
STORE_METADATA,
|
|
17
|
+
} = require('./storage/constants');
|
|
18
|
+
|
|
19
|
+
const { openDatabase, clearDatabase } = require('./storage/db');
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
saveShare,
|
|
23
|
+
getShare,
|
|
24
|
+
getAllShares,
|
|
25
|
+
deleteShare,
|
|
26
|
+
getShareVersion,
|
|
27
|
+
} = require('./storage/shares');
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
saveWrappedKey,
|
|
31
|
+
getWrappedKey,
|
|
32
|
+
getAllWrappedKeys,
|
|
33
|
+
deleteWrappedKey,
|
|
34
|
+
} = require('./storage/keys');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Save enrollment metadata for a user
|
|
38
|
+
* @param {string} userId - User identifier
|
|
39
|
+
* @param {Object} [metadata] - Enrollment metadata
|
|
40
|
+
* @returns {Promise<void>}
|
|
41
|
+
*/
|
|
42
|
+
async function saveMetadata(userId, metadata = {}) {
|
|
43
|
+
if (!userId || typeof userId !== 'string') {
|
|
44
|
+
throw new Error('userId is required and must be a string');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const db = await openDatabase();
|
|
48
|
+
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const tx = db.transaction(STORE_METADATA, 'readwrite');
|
|
51
|
+
const store = tx.objectStore(STORE_METADATA);
|
|
52
|
+
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const record = {
|
|
55
|
+
userId,
|
|
56
|
+
enrolledAt: metadata.enrolledAt || now,
|
|
57
|
+
lastRecoveryAt: metadata.lastRecoveryAt || null,
|
|
58
|
+
shareCount: metadata.shareCount || 0,
|
|
59
|
+
...metadata,
|
|
60
|
+
updatedAt: now
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
store.put(record);
|
|
64
|
+
|
|
65
|
+
tx.oncomplete = () => { db.close(); resolve(); };
|
|
66
|
+
tx.onerror = () => { db.close(); reject(tx.error); };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get enrollment metadata for a user
|
|
72
|
+
* @param {string} userId - User identifier
|
|
73
|
+
* @returns {Promise<Object|null>}
|
|
74
|
+
*/
|
|
75
|
+
async function getMetadata(userId) {
|
|
76
|
+
if (!userId || typeof userId !== 'string') {
|
|
77
|
+
throw new Error('userId is required and must be a string');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const db = await openDatabase();
|
|
81
|
+
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const tx = db.transaction(STORE_METADATA, 'readonly');
|
|
84
|
+
const store = tx.objectStore(STORE_METADATA);
|
|
85
|
+
|
|
86
|
+
const request = store.get(userId);
|
|
87
|
+
|
|
88
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
89
|
+
request.onerror = () => reject(request.error);
|
|
90
|
+
|
|
91
|
+
tx.oncomplete = () => db.close();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Delete all data for a user
|
|
97
|
+
* @param {string} userId - User identifier
|
|
98
|
+
* @returns {Promise<Object>} Count of deleted items
|
|
99
|
+
*/
|
|
100
|
+
async function deleteAllUserData(userId) {
|
|
101
|
+
if (!userId || typeof userId !== 'string') {
|
|
102
|
+
throw new Error('userId is required and must be a string');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const db = await openDatabase();
|
|
106
|
+
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const tx = db.transaction([STORE_SHARES, STORE_KEYS, STORE_METADATA], 'readwrite');
|
|
109
|
+
|
|
110
|
+
let sharesDeleted = 0;
|
|
111
|
+
let keysDeleted = 0;
|
|
112
|
+
|
|
113
|
+
const sharesStore = tx.objectStore(STORE_SHARES);
|
|
114
|
+
const sharesIndex = sharesStore.index('userId');
|
|
115
|
+
const sharesCursor = sharesIndex.openCursor(userId);
|
|
116
|
+
|
|
117
|
+
sharesCursor.onsuccess = (event) => {
|
|
118
|
+
const cursor = event.target.result;
|
|
119
|
+
if (cursor) {
|
|
120
|
+
sharesStore.delete(cursor.primaryKey);
|
|
121
|
+
sharesDeleted++;
|
|
122
|
+
cursor.continue();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const keysStore = tx.objectStore(STORE_KEYS);
|
|
127
|
+
const keysIndex = keysStore.index('userId');
|
|
128
|
+
const keysCursor = keysIndex.openCursor(userId);
|
|
129
|
+
|
|
130
|
+
keysCursor.onsuccess = (event) => {
|
|
131
|
+
const cursor = event.target.result;
|
|
132
|
+
if (cursor) {
|
|
133
|
+
keysStore.delete(cursor.primaryKey);
|
|
134
|
+
keysDeleted++;
|
|
135
|
+
cursor.continue();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const metadataStore = tx.objectStore(STORE_METADATA);
|
|
140
|
+
metadataStore.delete(userId);
|
|
141
|
+
|
|
142
|
+
tx.oncomplete = () => { db.close(); resolve({ shares: sharesDeleted, keys: keysDeleted }); };
|
|
143
|
+
tx.onerror = () => { db.close(); reject(tx.error); };
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if a user has enrolled
|
|
149
|
+
* @param {string} userId - User identifier
|
|
150
|
+
* @returns {Promise<boolean>}
|
|
151
|
+
*/
|
|
152
|
+
async function isEnrolled(userId) {
|
|
153
|
+
const metadata = await getMetadata(userId);
|
|
154
|
+
return metadata !== null && metadata.enrolledAt !== undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Create a storage instance with bound userId
|
|
159
|
+
* @param {string} userId - User identifier
|
|
160
|
+
* @returns {Object} Storage API bound to userId
|
|
161
|
+
*/
|
|
162
|
+
function createUserStorage(userId) {
|
|
163
|
+
if (!userId || typeof userId !== 'string') {
|
|
164
|
+
throw new Error('userId is required and must be a string');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
userId,
|
|
169
|
+
saveShare: (shareId, encryptedShare, version) => saveShare(userId, shareId, encryptedShare, version),
|
|
170
|
+
getShare: (shareId) => getShare(userId, shareId),
|
|
171
|
+
getAllShares: () => getAllShares(userId),
|
|
172
|
+
deleteShare: (shareId) => deleteShare(userId, shareId),
|
|
173
|
+
getShareVersion: (shareId) => getShareVersion(userId, shareId),
|
|
174
|
+
saveWrappedKey: (keyId, wrappedKey) => saveWrappedKey(userId, keyId, wrappedKey),
|
|
175
|
+
getAllWrappedKeys: () => getAllWrappedKeys(userId),
|
|
176
|
+
saveMetadata: (metadata) => saveMetadata(userId, metadata),
|
|
177
|
+
getMetadata: () => getMetadata(userId),
|
|
178
|
+
isEnrolled: () => isEnrolled(userId),
|
|
179
|
+
deleteAll: () => deleteAllUserData(userId)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
openDatabase,
|
|
185
|
+
clearDatabase,
|
|
186
|
+
saveShare,
|
|
187
|
+
getShare,
|
|
188
|
+
getAllShares,
|
|
189
|
+
deleteShare,
|
|
190
|
+
getShareVersion,
|
|
191
|
+
saveWrappedKey,
|
|
192
|
+
getWrappedKey,
|
|
193
|
+
getAllWrappedKeys,
|
|
194
|
+
deleteWrappedKey,
|
|
195
|
+
saveMetadata,
|
|
196
|
+
getMetadata,
|
|
197
|
+
deleteAllUserData,
|
|
198
|
+
isEnrolled,
|
|
199
|
+
createUserStorage,
|
|
200
|
+
DB_NAME,
|
|
201
|
+
DB_VERSION,
|
|
202
|
+
STORE_SHARES,
|
|
203
|
+
STORE_KEYS,
|
|
204
|
+
STORE_METADATA
|
|
205
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file IndexedDB storage constants
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const DB_NAME = 'KeyRecoveryStore';
|
|
7
|
+
const DB_VERSION = 1;
|
|
8
|
+
const STORE_SHARES = 'shares';
|
|
9
|
+
const STORE_KEYS = 'keys';
|
|
10
|
+
const STORE_METADATA = 'metadata';
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
DB_NAME,
|
|
14
|
+
DB_VERSION,
|
|
15
|
+
STORE_SHARES,
|
|
16
|
+
STORE_KEYS,
|
|
17
|
+
STORE_METADATA,
|
|
18
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file IndexedDB database initialization and transaction helpers
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
DB_NAME,
|
|
8
|
+
DB_VERSION,
|
|
9
|
+
STORE_SHARES,
|
|
10
|
+
STORE_KEYS,
|
|
11
|
+
STORE_METADATA,
|
|
12
|
+
} = require('./constants');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Open the IndexedDB database
|
|
16
|
+
* @returns {Promise<IDBDatabase>}
|
|
17
|
+
*/
|
|
18
|
+
function openDatabase() {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
if (typeof indexedDB === 'undefined') {
|
|
21
|
+
reject(new Error('IndexedDB not available'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
26
|
+
|
|
27
|
+
request.onerror = () => {
|
|
28
|
+
reject(new Error(`Failed to open database: ${request.error?.message || 'Unknown error'}`));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
request.onsuccess = () => {
|
|
32
|
+
resolve(request.result);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
request.onupgradeneeded = (event) => {
|
|
36
|
+
const db = event.target.result;
|
|
37
|
+
|
|
38
|
+
if (!db.objectStoreNames.contains(STORE_SHARES)) {
|
|
39
|
+
const sharesStore = db.createObjectStore(STORE_SHARES, { keyPath: 'id', autoIncrement: true });
|
|
40
|
+
sharesStore.createIndex('userId', 'userId', { unique: false });
|
|
41
|
+
sharesStore.createIndex('shareId', 'shareId', { unique: true });
|
|
42
|
+
sharesStore.createIndex('userShare', ['userId', 'shareId'], { unique: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!db.objectStoreNames.contains(STORE_KEYS)) {
|
|
46
|
+
const keysStore = db.createObjectStore(STORE_KEYS, { keyPath: 'id', autoIncrement: true });
|
|
47
|
+
keysStore.createIndex('userId', 'userId', { unique: false });
|
|
48
|
+
keysStore.createIndex('keyId', 'keyId', { unique: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!db.objectStoreNames.contains(STORE_METADATA)) {
|
|
52
|
+
db.createObjectStore(STORE_METADATA, { keyPath: 'userId' });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Execute a transaction on the database
|
|
60
|
+
* @param {string|Array} storeNames - Store name(s)
|
|
61
|
+
* @param {string} mode - Transaction mode
|
|
62
|
+
* @param {Function} operation - Operation function
|
|
63
|
+
* @returns {Promise<*>}
|
|
64
|
+
*/
|
|
65
|
+
async function withTransaction(storeNames, mode, operation) {
|
|
66
|
+
const db = await openDatabase();
|
|
67
|
+
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const tx = db.transaction(storeNames, mode);
|
|
70
|
+
|
|
71
|
+
tx.onerror = () => {
|
|
72
|
+
reject(new Error(`Transaction failed: ${tx.error?.message || 'Unknown error'}`));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
tx.oncomplete = () => {
|
|
76
|
+
db.close();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = operation(tx);
|
|
81
|
+
|
|
82
|
+
if (result && typeof result.then === 'function') {
|
|
83
|
+
result.then(resolve).catch(reject);
|
|
84
|
+
} else {
|
|
85
|
+
tx.oncomplete = () => {
|
|
86
|
+
db.close();
|
|
87
|
+
resolve(result);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
reject(err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Wrap an IDBRequest in a Promise
|
|
98
|
+
* @param {IDBRequest} request - Request to wrap
|
|
99
|
+
* @returns {Promise<*>}
|
|
100
|
+
*/
|
|
101
|
+
function promisifyRequest(request) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
request.onsuccess = () => resolve(request.result);
|
|
104
|
+
request.onerror = () => reject(request.error);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Clear the entire database
|
|
110
|
+
* @returns {Promise<void>}
|
|
111
|
+
*/
|
|
112
|
+
function clearDatabase() {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
if (typeof indexedDB === 'undefined') {
|
|
115
|
+
reject(new Error('IndexedDB not available'));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const request = indexedDB.deleteDatabase(DB_NAME);
|
|
120
|
+
|
|
121
|
+
request.onsuccess = () => resolve();
|
|
122
|
+
request.onerror = () => reject(request.error);
|
|
123
|
+
request.onblocked = () => reject(new Error('Database deletion blocked'));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
openDatabase,
|
|
129
|
+
withTransaction,
|
|
130
|
+
promisifyRequest,
|
|
131
|
+
clearDatabase,
|
|
132
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Client Auth Storage Module
|
|
2
|
+
|
|
3
|
+
IndexedDB storage for encrypted shares and keys.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
storage/
|
|
9
|
+
├── constants.js - Storage constants (DB name, store names)
|
|
10
|
+
├── db.js - IndexedDB connection management
|
|
11
|
+
├── keys.js - Wrapped key storage operations
|
|
12
|
+
└── shares.js - Encrypted share storage operations
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Files
|
|
16
|
+
|
|
17
|
+
### `constants.js`
|
|
18
|
+
Defines StorageError codes, database name, and object store names.
|
|
19
|
+
|
|
20
|
+
### `db.js`
|
|
21
|
+
Manages IndexedDB connection lifecycle with version upgrades and cleanup.
|
|
22
|
+
|
|
23
|
+
### `shares.js`
|
|
24
|
+
CRUD operations for encrypted shares in IndexedDB with versioning.
|
|
25
|
+
|
|
26
|
+
### `keys.js`
|
|
27
|
+
Storage for WebAuthn-wrapped keys to decrypt S2 share.
|