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
package/client/README.md
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
# 🦍 api-ape Client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
The client module provides the browser-side WebSocket client for api-ape. It enables seamless communication with api-ape servers through a proxy-based API that converts method calls like `api.users.list()` into WebSocket messages.
|
|
8
|
+
|
|
9
|
+
**Key capabilities:**
|
|
10
|
+
|
|
11
|
+
- **Proxy-based API** — Call server endpoints like local methods (`api.path.method(data)`)
|
|
12
|
+
- **Auto-reconnection** — Automatically reconnects with exponential backoff on disconnection
|
|
13
|
+
- **Call buffering** — Queues calls made before connection is established
|
|
14
|
+
- **JSS encoding** — Supports Date, RegExp, Error, Set, Map, and undefined over the wire
|
|
15
|
+
- **HTTP fallback** — Falls back to long-polling when WebSocket is blocked
|
|
16
|
+
- **Binary transfers** — Transparent file upload/download handling
|
|
17
|
+
- **Connection state** — Track connection status (offline, connecting, connected, disconnected)
|
|
18
|
+
- **Pub/sub subscriptions** — Subscribe to channels and receive targeted updates
|
|
19
|
+
|
|
20
|
+
The client works in both browser environments (via `<script>` tag) and bundled applications (React, Vue, etc.).
|
|
21
|
+
|
|
22
|
+
> **Contributing?** See [`files.md`](./files.md) for directory structure and file descriptions.
|
|
12
23
|
|
|
13
24
|
## Usage
|
|
14
25
|
|
|
@@ -17,11 +28,14 @@ WebSocket client library with auto-reconnection and proxy-based API calls.
|
|
|
17
28
|
```html
|
|
18
29
|
<script src="/api/ape.js"></script>
|
|
19
30
|
<script>
|
|
20
|
-
// Call server functions
|
|
31
|
+
// Call server functions (RPC)
|
|
21
32
|
api.hello('World').then(result => console.log(result))
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
api.
|
|
33
|
+
|
|
34
|
+
// Subscribe to channels (pass a callback function)
|
|
35
|
+
const unsub = api.message(data => console.log(data))
|
|
36
|
+
|
|
37
|
+
// Unsubscribe when done
|
|
38
|
+
unsub()
|
|
25
39
|
</script>
|
|
26
40
|
```
|
|
27
41
|
|
|
@@ -37,8 +51,13 @@ import api from 'api-ape'
|
|
|
37
51
|
// Just use it! Calls are buffered until connected.
|
|
38
52
|
api.users.list().then(users => console.log(users))
|
|
39
53
|
|
|
40
|
-
//
|
|
41
|
-
api.
|
|
54
|
+
// Subscribe to channels (pass a callback function)
|
|
55
|
+
const unsub = api.news.banking(data => {
|
|
56
|
+
console.log('Received:', data)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Unsubscribe when done
|
|
60
|
+
unsub()
|
|
42
61
|
|
|
43
62
|
// Track connection state
|
|
44
63
|
api.onConnectionChange((state) => {
|
|
@@ -53,6 +72,7 @@ api.onConnectionChange((state) => {
|
|
|
53
72
|
- `disconnected` — Had connection, lost it
|
|
54
73
|
- `connecting` — Actively connecting
|
|
55
74
|
- `connected` — Ready to use
|
|
75
|
+
- `closing` — Connection is closing
|
|
56
76
|
|
|
57
77
|
**No async setup needed!** The client auto-initializes and buffers calls until connected.
|
|
58
78
|
|
|
@@ -61,7 +81,7 @@ api.onConnectionChange((state) => {
|
|
|
61
81
|
- **Proxy-based API** — `ape.path.method(data)` converts to WebSocket calls
|
|
62
82
|
- **Auto-reconnect** — Reconnects on disconnect with queued messages
|
|
63
83
|
- **Promise-based** — All calls return promises with matched responses via queryId
|
|
64
|
-
- **
|
|
84
|
+
- **JSS encoding** — Supports Date, RegExp, Error, Set, Map, undefined over the wire
|
|
65
85
|
- **Request timeout** — Configurable timeout (default: 10s)
|
|
66
86
|
|
|
67
87
|
## File Transfers
|
|
@@ -95,6 +115,45 @@ await api.files.upload({
|
|
|
95
115
|
|
|
96
116
|
Binary transfers use `/api/ape/data/:hash` endpoints with session verification.
|
|
97
117
|
|
|
118
|
+
## Pub/Sub Subscriptions
|
|
119
|
+
|
|
120
|
+
Subscribe to channels using the same chaining syntax as RPC calls. The key difference: **pass a callback function to subscribe, pass data to make an RPC call**.
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
// RPC call - passing data
|
|
124
|
+
await api.news.banking({ category: 'stocks' }) // Returns Promise<response>
|
|
125
|
+
|
|
126
|
+
// Subscribe - passing a callback function
|
|
127
|
+
const unsub = api.news.banking(data => {
|
|
128
|
+
console.log('Received:', data)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Unsubscribe when done
|
|
132
|
+
unsub()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Chained Subscriptions
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
// Subscribe to nested channels
|
|
139
|
+
const unsub1 = api.stock.AAPL(data => {
|
|
140
|
+
console.log('AAPL:', data.price)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const unsub2 = api.health(data => {
|
|
144
|
+
console.log('Health update:', data)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Clean up
|
|
148
|
+
unsub1()
|
|
149
|
+
unsub2()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Behavior:**
|
|
153
|
+
- On subscribe, you receive the last published message immediately (if any)
|
|
154
|
+
- Subscriptions are automatically restored on reconnect
|
|
155
|
+
- Subscriptions are automatically cleaned up on disconnect
|
|
156
|
+
|
|
98
157
|
## Security
|
|
99
158
|
|
|
100
159
|
### CSRF Protection
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AES-256-GCM AEAD encryption for browser
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
CryptoError,
|
|
8
|
+
AES_KEY_LENGTH,
|
|
9
|
+
GCM_NONCE_LENGTH,
|
|
10
|
+
GCM_TAG_LENGTH,
|
|
11
|
+
} = require('./constants');
|
|
12
|
+
const {
|
|
13
|
+
isCryptoAvailable,
|
|
14
|
+
bufferToUint8Array,
|
|
15
|
+
stringToUint8Array,
|
|
16
|
+
randomBytes,
|
|
17
|
+
} = require('./encoding');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Import raw key for AES-GCM
|
|
21
|
+
* @param {Uint8Array} keyData - Key bytes
|
|
22
|
+
* @returns {Promise<CryptoKey>}
|
|
23
|
+
*/
|
|
24
|
+
async function importKey(keyData) {
|
|
25
|
+
return crypto.subtle.importKey('raw', keyData, { name: 'AES-GCM' }, false, [
|
|
26
|
+
'encrypt',
|
|
27
|
+
'decrypt',
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Encrypt data using AES-256-GCM with AEAD
|
|
33
|
+
* @param {Uint8Array} key - 32-byte encryption key
|
|
34
|
+
* @param {Uint8Array|string} plaintext - Data to encrypt
|
|
35
|
+
* @param {Uint8Array|string} [aad] - Additional authenticated data
|
|
36
|
+
* @returns {Promise<Object>} Encrypted data components
|
|
37
|
+
*/
|
|
38
|
+
async function aeadEncrypt(key, plaintext, aad = '') {
|
|
39
|
+
if (!isCryptoAvailable()) {
|
|
40
|
+
const err = new Error('Web Crypto API not available');
|
|
41
|
+
err.code = CryptoError.CRYPTO_NOT_AVAILABLE;
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!(key instanceof Uint8Array) || key.length !== AES_KEY_LENGTH) {
|
|
46
|
+
const err = new Error(`Key must be a ${AES_KEY_LENGTH}-byte Uint8Array`);
|
|
47
|
+
err.code = CryptoError.INVALID_KEY_LENGTH;
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const plaintextArray =
|
|
52
|
+
typeof plaintext === 'string' ? stringToUint8Array(plaintext) : plaintext;
|
|
53
|
+
const aadArray =
|
|
54
|
+
typeof aad === 'string' ? stringToUint8Array(aad) : aad || new Uint8Array(0);
|
|
55
|
+
|
|
56
|
+
const nonce = randomBytes(GCM_NONCE_LENGTH);
|
|
57
|
+
const cryptoKey = await importKey(key);
|
|
58
|
+
|
|
59
|
+
const encryptedBuffer = await crypto.subtle.encrypt(
|
|
60
|
+
{
|
|
61
|
+
name: 'AES-GCM',
|
|
62
|
+
iv: nonce,
|
|
63
|
+
additionalData: aadArray,
|
|
64
|
+
tagLength: GCM_TAG_LENGTH * 8,
|
|
65
|
+
},
|
|
66
|
+
cryptoKey,
|
|
67
|
+
plaintextArray
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const encrypted = bufferToUint8Array(encryptedBuffer);
|
|
71
|
+
const ciphertext = encrypted.slice(0, encrypted.length - GCM_TAG_LENGTH);
|
|
72
|
+
const tag = encrypted.slice(encrypted.length - GCM_TAG_LENGTH);
|
|
73
|
+
|
|
74
|
+
return { ciphertext, nonce, tag };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Decrypt data using AES-256-GCM with AEAD
|
|
79
|
+
* @param {Uint8Array} key - 32-byte decryption key
|
|
80
|
+
* @param {Uint8Array} ciphertext - Data to decrypt
|
|
81
|
+
* @param {Uint8Array} nonce - 12-byte nonce
|
|
82
|
+
* @param {Uint8Array} tag - 16-byte auth tag
|
|
83
|
+
* @param {Uint8Array|string} [aad] - Additional authenticated data
|
|
84
|
+
* @returns {Promise<Uint8Array>} Decrypted plaintext
|
|
85
|
+
*/
|
|
86
|
+
async function aeadDecrypt(key, ciphertext, nonce, tag, aad = '') {
|
|
87
|
+
if (!isCryptoAvailable()) {
|
|
88
|
+
const err = new Error('Web Crypto API not available');
|
|
89
|
+
err.code = CryptoError.CRYPTO_NOT_AVAILABLE;
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!(key instanceof Uint8Array) || key.length !== AES_KEY_LENGTH) {
|
|
94
|
+
const err = new Error(`Key must be a ${AES_KEY_LENGTH}-byte Uint8Array`);
|
|
95
|
+
err.code = CryptoError.INVALID_KEY_LENGTH;
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!(nonce instanceof Uint8Array) || nonce.length !== GCM_NONCE_LENGTH) {
|
|
100
|
+
const err = new Error(`Nonce must be a ${GCM_NONCE_LENGTH}-byte Uint8Array`);
|
|
101
|
+
err.code = CryptoError.INVALID_NONCE;
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!(tag instanceof Uint8Array) || tag.length !== GCM_TAG_LENGTH) {
|
|
106
|
+
const err = new Error(`Tag must be a ${GCM_TAG_LENGTH}-byte Uint8Array`);
|
|
107
|
+
err.code = CryptoError.INVALID_TAG;
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const aadArray =
|
|
112
|
+
typeof aad === 'string' ? stringToUint8Array(aad) : aad || new Uint8Array(0);
|
|
113
|
+
|
|
114
|
+
const combined = new Uint8Array(ciphertext.length + tag.length);
|
|
115
|
+
combined.set(ciphertext);
|
|
116
|
+
combined.set(tag, ciphertext.length);
|
|
117
|
+
|
|
118
|
+
const cryptoKey = await importKey(key);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
122
|
+
{
|
|
123
|
+
name: 'AES-GCM',
|
|
124
|
+
iv: nonce,
|
|
125
|
+
additionalData: aadArray,
|
|
126
|
+
tagLength: GCM_TAG_LENGTH * 8,
|
|
127
|
+
},
|
|
128
|
+
cryptoKey,
|
|
129
|
+
combined
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return bufferToUint8Array(decryptedBuffer);
|
|
133
|
+
} catch {
|
|
134
|
+
const err = new Error('Decryption failed: authentication tag mismatch');
|
|
135
|
+
err.code = CryptoError.DECRYPTION_FAILED;
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Pack encrypted data into single Uint8Array for storage
|
|
142
|
+
* Format: [nonce:12][tag:16][ciphertext:N]
|
|
143
|
+
* @param {Object} encrypted - Encrypted data components
|
|
144
|
+
* @param {Uint8Array} encrypted.ciphertext - The encrypted data
|
|
145
|
+
* @param {Uint8Array} encrypted.nonce - The nonce
|
|
146
|
+
* @param {Uint8Array} encrypted.tag - The auth tag
|
|
147
|
+
* @returns {Uint8Array}
|
|
148
|
+
*/
|
|
149
|
+
function packEncrypted(encrypted) {
|
|
150
|
+
const { ciphertext, nonce, tag } = encrypted;
|
|
151
|
+
const packed = new Uint8Array(nonce.length + tag.length + ciphertext.length);
|
|
152
|
+
packed.set(nonce);
|
|
153
|
+
packed.set(tag, nonce.length);
|
|
154
|
+
packed.set(ciphertext, nonce.length + tag.length);
|
|
155
|
+
return packed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Unpack encrypted data from storage format
|
|
160
|
+
* @param {Uint8Array} packed - Packed encrypted data
|
|
161
|
+
* @returns {Object} Unpacked components
|
|
162
|
+
*/
|
|
163
|
+
function unpackEncrypted(packed) {
|
|
164
|
+
if (!(packed instanceof Uint8Array)) {
|
|
165
|
+
const err = new Error('Packed data must be a Uint8Array');
|
|
166
|
+
err.code = CryptoError.INVALID_CIPHERTEXT;
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const minLength = GCM_NONCE_LENGTH + GCM_TAG_LENGTH;
|
|
171
|
+
if (packed.length < minLength) {
|
|
172
|
+
const err = new Error(`Packed data too short: minimum ${minLength} bytes`);
|
|
173
|
+
err.code = CryptoError.INVALID_CIPHERTEXT;
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const nonce = packed.slice(0, GCM_NONCE_LENGTH);
|
|
178
|
+
const tag = packed.slice(GCM_NONCE_LENGTH, GCM_NONCE_LENGTH + GCM_TAG_LENGTH);
|
|
179
|
+
const ciphertext = packed.slice(GCM_NONCE_LENGTH + GCM_TAG_LENGTH);
|
|
180
|
+
|
|
181
|
+
return { ciphertext, nonce, tag };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Encrypt and pack in one step
|
|
186
|
+
* @param {Uint8Array} key - Encryption key
|
|
187
|
+
* @param {Uint8Array|string} plaintext - Data to encrypt
|
|
188
|
+
* @param {Uint8Array|string} [aad] - Additional authenticated data
|
|
189
|
+
* @returns {Promise<Uint8Array>}
|
|
190
|
+
*/
|
|
191
|
+
async function encryptAndPack(key, plaintext, aad = '') {
|
|
192
|
+
return packEncrypted(await aeadEncrypt(key, plaintext, aad));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Unpack and decrypt in one step
|
|
197
|
+
* @param {Uint8Array} key - Decryption key
|
|
198
|
+
* @param {Uint8Array} packed - Packed encrypted data
|
|
199
|
+
* @param {Uint8Array|string} [aad] - Additional authenticated data
|
|
200
|
+
* @returns {Promise<Uint8Array>}
|
|
201
|
+
*/
|
|
202
|
+
async function unpackAndDecrypt(key, packed, aad = '') {
|
|
203
|
+
const { ciphertext, nonce, tag } = unpackEncrypted(packed);
|
|
204
|
+
return aeadDecrypt(key, ciphertext, nonce, tag, aad);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
aeadEncrypt,
|
|
209
|
+
aeadDecrypt,
|
|
210
|
+
packEncrypted,
|
|
211
|
+
unpackEncrypted,
|
|
212
|
+
encryptAndPack,
|
|
213
|
+
unpackAndDecrypt,
|
|
214
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Cryptographic constants and error codes
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error codes for crypto operations
|
|
8
|
+
* @enum {string}
|
|
9
|
+
*/
|
|
10
|
+
const CryptoError = {
|
|
11
|
+
DECRYPTION_FAILED: 'DECRYPTION_FAILED',
|
|
12
|
+
INVALID_KEY_LENGTH: 'INVALID_KEY_LENGTH',
|
|
13
|
+
INVALID_CIPHERTEXT: 'INVALID_CIPHERTEXT',
|
|
14
|
+
INVALID_NONCE: 'INVALID_NONCE',
|
|
15
|
+
INVALID_TAG: 'INVALID_TAG',
|
|
16
|
+
ARGON2_NOT_AVAILABLE: 'ARGON2_NOT_AVAILABLE',
|
|
17
|
+
KDF_FAILED: 'KDF_FAILED',
|
|
18
|
+
CRYPTO_NOT_AVAILABLE: 'CRYPTO_NOT_AVAILABLE',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const AES_KEY_LENGTH = 32; // 256 bits for AES-256-GCM
|
|
22
|
+
const GCM_NONCE_LENGTH = 12; // 96 bits for GCM
|
|
23
|
+
const GCM_TAG_LENGTH = 16; // 128 bits auth tag
|
|
24
|
+
const PBKDF2_ITERATIONS = 600000; // OWASP recommended minimum
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
CryptoError,
|
|
28
|
+
AES_KEY_LENGTH,
|
|
29
|
+
GCM_NONCE_LENGTH,
|
|
30
|
+
GCM_TAG_LENGTH,
|
|
31
|
+
PBKDF2_ITERATIONS,
|
|
32
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Data encoding utilities for browser crypto
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if Web Crypto API is available
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
function isCryptoAvailable() {
|
|
11
|
+
return typeof crypto !== 'undefined' && crypto.subtle;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert ArrayBuffer to Uint8Array
|
|
16
|
+
* @param {ArrayBuffer} buffer - Buffer to convert
|
|
17
|
+
* @returns {Uint8Array}
|
|
18
|
+
*/
|
|
19
|
+
function bufferToUint8Array(buffer) {
|
|
20
|
+
return new Uint8Array(buffer);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert string to Uint8Array
|
|
25
|
+
* @param {string} str - String to convert
|
|
26
|
+
* @returns {Uint8Array}
|
|
27
|
+
*/
|
|
28
|
+
function stringToUint8Array(str) {
|
|
29
|
+
return new TextEncoder().encode(str);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert Uint8Array to string
|
|
34
|
+
* @param {Uint8Array} array - Array to convert
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function uint8ArrayToString(array) {
|
|
38
|
+
return new TextDecoder().decode(array);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert Uint8Array to base64
|
|
43
|
+
* @param {Uint8Array} array - Array to convert
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function uint8ArrayToBase64(array) {
|
|
47
|
+
let binary = '';
|
|
48
|
+
for (let i = 0; i < array.length; i++) {
|
|
49
|
+
binary += String.fromCharCode(array[i]);
|
|
50
|
+
}
|
|
51
|
+
return btoa(binary);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert base64 to Uint8Array
|
|
56
|
+
* @param {string} base64 - Base64 string to convert
|
|
57
|
+
* @returns {Uint8Array}
|
|
58
|
+
*/
|
|
59
|
+
function base64ToUint8Array(base64) {
|
|
60
|
+
const binary = atob(base64);
|
|
61
|
+
const array = new Uint8Array(binary.length);
|
|
62
|
+
for (let i = 0; i < binary.length; i++) {
|
|
63
|
+
array[i] = binary.charCodeAt(i);
|
|
64
|
+
}
|
|
65
|
+
return array;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate random bytes
|
|
70
|
+
* @param {number} length - Number of bytes
|
|
71
|
+
* @returns {Uint8Array}
|
|
72
|
+
*/
|
|
73
|
+
function randomBytes(length) {
|
|
74
|
+
const array = new Uint8Array(length);
|
|
75
|
+
crypto.getRandomValues(array);
|
|
76
|
+
return array;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Timing-safe comparison (best effort in browser)
|
|
81
|
+
* @param {Uint8Array} a - First array
|
|
82
|
+
* @param {Uint8Array} b - Second array
|
|
83
|
+
* @returns {boolean}
|
|
84
|
+
*/
|
|
85
|
+
function timingSafeEqual(a, b) {
|
|
86
|
+
if (a.length !== b.length) return false;
|
|
87
|
+
|
|
88
|
+
let result = 0;
|
|
89
|
+
for (let i = 0; i < a.length; i++) {
|
|
90
|
+
result |= a[i] ^ b[i];
|
|
91
|
+
}
|
|
92
|
+
return result === 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
isCryptoAvailable,
|
|
97
|
+
bufferToUint8Array,
|
|
98
|
+
stringToUint8Array,
|
|
99
|
+
uint8ArrayToString,
|
|
100
|
+
uint8ArrayToBase64,
|
|
101
|
+
base64ToUint8Array,
|
|
102
|
+
randomBytes,
|
|
103
|
+
timingSafeEqual,
|
|
104
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Client Auth Crypto Module
|
|
2
|
+
|
|
3
|
+
Browser-side cryptographic utilities using Web Crypto API.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
crypto/
|
|
9
|
+
├── aead.js - AES-GCM encryption/decryption
|
|
10
|
+
├── constants.js - Crypto constants and error codes
|
|
11
|
+
├── encoding.js - Base64/hex encoding utilities
|
|
12
|
+
└── kdf.js - Key derivation functions (PBKDF2)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Files
|
|
16
|
+
|
|
17
|
+
### `constants.js`
|
|
18
|
+
Defines AES key lengths, nonce lengths, and error codes for crypto operations.
|
|
19
|
+
|
|
20
|
+
### `encoding.js`
|
|
21
|
+
Provides base64url and hex encoding/decoding utilities for browser environments.
|
|
22
|
+
|
|
23
|
+
### `aead.js`
|
|
24
|
+
AES-256-GCM authenticated encryption using Web Crypto API.
|
|
25
|
+
|
|
26
|
+
### `kdf.js`
|
|
27
|
+
PBKDF2 key derivation for encrypting shares client-side.
|