ephem 0.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,481 @@
1
+ # Ephem
2
+
3
+ **Robust, Ephemeral End-to-End Encryption for the Application Layer.**
4
+
5
+ > [!IMPORTANT]
6
+ > **Not a TLS replacement.** Ephem is a defense-in-depth security layer designed to protect high-value sensitive data (passwords, PII, financial info) even if TLS is terminated, inspected, or compromised.
7
+
8
+ Ephem allows your server to issue disposable "Capsules": short-lived, single-use cryptographic containers. Clients seal data into these capsules using hybrid encryption (RSA + AES). Only the server that issued the capsule can open it, and only within the defined restrictions (expiration time, maximum open count).
9
+
10
+ Once a capsule is used or expires, its private key is destroyed forever.
11
+
12
+ ---
13
+
14
+ ## Why Ephem? (Intent)
15
+
16
+ Ephem is designed for **Application-Layer Encryption**.
17
+
18
+ In modern infrastructure, TLS (Transport Layer Security) often terminates at the perimeter, at your Load Balancer (AWS ALB, Nginx, Cloudflare). From that point onward, traffic often travels unencrypted across your internal network (VPC) to reach your application server.
19
+
20
+ **The Risk:** If an attacker compromises your internal network, a proxy, or your logging infrastructure, they can see sensitive data in plain text.
21
+
22
+ **The Solution:** Ephem ensures that sensitive fields (like Credit Card numbers, SSNs, Password changes) are encrypted **at the source** (the user's browser) and remain encrypted until they reach your **application logic**. Even if the TLS termination point is compromised, the attacker only sees Ephem capsules, which they cannot open.
23
+
24
+ Ephem is also suitable for **Server-to-Server** communication for high-security microservices that need to pass secrets over untrusted internal pipes.
25
+
26
+ Ephem is also suitable for **Server-to-Server** communication for high-security microservices that need to pass secrets over untrusted internal pipes.
27
+
28
+ **Transport Agnostic:** Since Ephem operates at the application layer, it works independently of the transport protocol. You can send capsules over HTTP, WebSockets, FTP, email, or even sneakernet.
29
+
30
+ **Horizontal Scaling:** Ephem supports high-availability clusters. By using `inMemory: false` and implementing persistence hooks (backed by Redis, Postgres, etc.), any server in your fleet can open a capsule, regardless of which server issued it. State is shared, not siloed.
31
+
32
+ ## Features
33
+
34
+ - **Zero-Trust Architecture**: Key material never leaves the server (private keys) or client (ephemeral symmetric keys) inappropriately.
35
+ - **Transport Agnostic**: Works over any protocol (HTTP, FTP, WebSockets, etc.) as the encryption happens before transmission.
36
+ - **Forward Secrecy**: Each request uses a unique, disposable key pair. Past captures of traffic cannot be decrypted even if the server is later compromised.
37
+ - **Hybrid Encryption**: Combines the convenience of RSA-2048 (for key exchange) with the speed of AES-256-GCM (for payload encryption).
38
+ - **Strict Lifecycle Management**: Capsules have hard expiration times and maximum usage counts.
39
+ - **Persistence Ready**: hooks for Redis, SQL, or other storage engines to support horizontal scaling.
40
+ - **Zero-Dependency Client**: The client library is extremely lightweight and uses the native WebCrypto API.
41
+ - **Typescript First**: Written in TypeScript with full type definitions.
42
+
43
+ ---
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ npm install ephem
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Quick Start
54
+
55
+ ### 1. Server Setup (Node.js)
56
+
57
+ Initialize Ephem and create an endpoint to issue capsules, and another to receive sealed data.
58
+
59
+ ```typescript
60
+ import Ephem from "ephem";
61
+
62
+ // Initialize with default in-memory storage
63
+ const ephem = new Ephem({
64
+ inMemory: true,
65
+ logging: true,
66
+ });
67
+
68
+ // --- In your framework (Express, Fastify, etc.) ---
69
+
70
+ // GET /api/capsule
71
+ app.get('/api/capsule', async (req, res) => {
72
+ // Create a capsule that lives for 1 minute and can be opened once
73
+ const capsule = await ephem.createCapsulePromise({
74
+ maxOpens: 1,
75
+ lifetimeDurationMS: 60 * 1000
76
+ });
77
+
78
+ if (!capsule) return res.status(500).send("Failed to create capsule");
79
+
80
+ // Send the PUBLIC key and Capsule ID (CID) to the client
81
+ res.json({
82
+ cid: capsule.cid,
83
+ publicKey: capsule.publicKey
84
+ });
85
+ });
86
+
87
+ // POST /api/submit
88
+ app.post('/api/submit', async (req, res) => {
89
+ const { sealedPayload } = req.body;
90
+
91
+ // Attempt to open the capsule
92
+ const decryptedData = await ephem.open(sealedPayload);
93
+
94
+ if (!decryptedData) {
95
+ return res.status(400).send("Invalid, expired, or exhausted capsule.");
96
+ }
97
+
98
+ console.log("Received sensitive data:", decryptedData);
99
+ res.send("Data received securely.");
100
+ });
101
+ ```
102
+
103
+ ### 1a. Server Setup (CommonJS)
104
+
105
+ If you are using `require()` instead of `import`:
106
+
107
+ ```javascript
108
+ const Ephem = require("ephem");
109
+
110
+ // Initialize
111
+ const ephem = new Ephem({
112
+ inMemory: true,
113
+ logging: true
114
+ });
115
+
116
+ // ... (Rest of usage is identical)
117
+ ```
118
+
119
+ ### 2. Client Setup (Browser)
120
+
121
+ The client fetches a capsule and uses it to "seal" data before sending it.
122
+
123
+ ```typescript
124
+ import { seal } from "ephem/client";
125
+
126
+ async function submitSensitiveData(secretData: string) {
127
+ // 1. Get a fresh capsule from the server
128
+ const response = await fetch('/api/capsule');
129
+ const { cid, publicKey } = await response.json();
130
+
131
+ // 2. Seal the data locally (Browser WebCrypto)
132
+ // This generates an AES key, encrypts data, then wraps the AES key with the RSA public key
133
+ const sealedPayload = await seal(secretData, publicKey, cid);
134
+
135
+ // 3. Send securely
136
+ await fetch('/api/submit', {
137
+ method: 'POST',
138
+ headers: { 'Content-Type': 'application/json' },
139
+ body: JSON.stringify({ sealedPayload })
140
+ });
141
+ }
142
+ ```
143
+
144
+ ### 3. Browser Usage (via CDN)
145
+
146
+ For projects without a bundler, you can use the pre-built unpkg/CDN version. This exposes the global `EphemClient` object.
147
+
148
+ ```html
149
+ <!-- Load Ephem client from CDN -->
150
+ <script src="https://unpkg.com/ephem/dist/client/index.global.js"></script>
151
+
152
+ <script>
153
+ "use strict";
154
+ // Example usage
155
+ async function runDemo() {
156
+ try {
157
+ // In a real app, fetch these from your server
158
+ const cid = "5a6d4d70-620a-472d-9edc-c9490ff77c2b";
159
+ const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
160
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
161
+ -----END PUBLIC KEY-----`;
162
+
163
+ const message = "Hello from browser 👋";
164
+
165
+ // Encrypt using the global EphemClient
166
+ const cipher = await EphemClient.seal(
167
+ message,
168
+ PUBLIC_KEY_PEM,
169
+ cid,
170
+ // allowInsecureFallback: Set to true ONLY for local dev over HTTP
171
+ // In production (HTTPS), this should be false or omitted.
172
+ true,
173
+ );
174
+
175
+ console.log("Cipher:", cipher);
176
+ } catch (err) {
177
+ console.error(err);
178
+ }
179
+ };
180
+ </script>
181
+ ```
182
+
183
+ ---
184
+
185
+ ## How it Works
186
+
187
+ Ephem uses a **Hybrid Encryption** scheme encapsulated in a "Capsule" paradigm.
188
+
189
+ ### Flow Diagram
190
+
191
+ 1. **Handshake**:
192
+ * **Server** generates an RSA-2048 KeyPair.
193
+ * **Server** stores `PrivateKey` + `UsageLimits` (expiresAt, maxOpens) mapped to a `CID` (Capsule ID).
194
+ * **Server** sends `PublicKey` + `CID` to Client.
195
+ 2. **Sealing (Client)**:
196
+ * **Client** generates a random AES-256-GCM key (`SymKey`).
197
+ * **Client** encrypts the payload with `SymKey`.
198
+ * **Client** encrypts `SymKey` with the Server's `PublicKey` (RSA-OAEP).
199
+ * **Client** bundles `CID` + `IV` + `EncryptedPayload` + `EncryptedSymKey` into a single string.
200
+ 3. **Opening (Server)**:
201
+ * **Server** looks up internal state using `CID`.
202
+ * **Server** checks: Is it expired? Has it been used too many times?
203
+ * **Server** decrypts `SymKey` using `PrivateKey`.
204
+ * **Server** decrypts payload using `SymKey`.
205
+ * **Server** increments usage count / destroys capsule if exhausted.
206
+
207
+ ---
208
+
209
+
210
+
211
+ ## Use Cases
212
+
213
+ ### 1. Secure Form Submission (Standard)
214
+
215
+ * **Scenario:** User changes their password.
216
+ * **Flow:**
217
+ 1. Client fetches a capsule (`maxOpens: 1`).
218
+ 2. Client seals the new password.
219
+ 3. Client sends `sealedPassword` to server.
220
+ 4. Server opens it, hashes the password, and updates DB.
221
+
222
+ ### 2. Multi-Field Forms (Advanced)
223
+
224
+ * **Scenario:** A Checkout Form with `CreditCard`, `CVV`, and `SSN`.
225
+ * **Problem:** Requesting 3 separate capsules is slow.
226
+ * **Solution:** Request **one** capsule with `maxOpens: 3`.
227
+
228
+ ```typescript
229
+ // Server: Issue a multi-use capsule
230
+ const capsule = await ephem.createCapsulePromise({ maxOpens: 3, lifetimeDurationMS: 60000 });
231
+ ```
232
+
233
+ ```typescript
234
+ // Client: Seal each field with the SAME capsule details
235
+ const sealedCC = await seal(ccNumber, pubKey, cid);
236
+ const sealedCVV = await seal(cvv, pubKey, cid);
237
+ const sealedSSN = await seal(ssn, pubKey, cid);
238
+
239
+ // Send all three
240
+ await fetch('/checkout', { body: JSON.stringify({ sealedCC, sealedCVV, sealedSSN }) });
241
+ ```
242
+
243
+ > [!CAUTION]
244
+ > **Concurrency Warning: Serial Unsealing**
245
+ >
246
+ > When opening multiple payloads from the same capsule, **you must unseal them serially** (one by one), especially if using a database like Redis for persistence.
247
+ >
248
+ > If you `Promise.all([open(a), open(b), open(c)])`, the parallel requests might race against the database's `openCount` check. One of them might read the "old" count before another has written the "new" count, causing you to accidentally exceed the limit or fail unpredictably.
249
+ >
250
+ > **Correct:**
251
+ > ```typescript
252
+ > // Server Code
253
+ > const cc = await ephem.open(req.body.sealedCC); // count becomes 1
254
+ > const cvv = await ephem.open(req.body.sealedCVV); // count becomes 2
255
+ > const ssn = await ephem.open(req.body.sealedSSN); // count becomes 3
256
+ > ```
257
+
258
+ ---
259
+
260
+ ## Strengths & Weaknesses
261
+
262
+ | Feature | Ephem Strength | Limitation / Weakness |
263
+ | :--- | :--- | :--- |
264
+ | **Security Model** | **Perfect Forward Secrecy.** Every request has a unique key. Compromising the server now does not compromise past traffic. | **Not a TLS Replacement.** Does not verify server identity (no Certificates). Vulnerable to active Man-in-the-Middle if TLS is missing. |
265
+ | **Architecture** | **Zero-Trust.** Keys never leave their respective environments. | **Stateful.** Requires the server to "remember" the capsule (RAM or DB). Harder to scale than stateless JWTs. |
266
+ | **Performance** | **Hybrid Encryption.** Fast AES-256 for payloads. | **RSA Overhead.** Generating 2048-bit RSA keys is CPU intensive. Not suitable for high-frequency "chat" messages. |
267
+ | **Usability** | **Strict Types.** Full TypeScript support. Simple `seal()` / `open()` API. | **Payload Size.** Sealed strings are larger than plaintext due to base64 encoding and key wrapping. |
268
+ | **Compliance** | **Auditable.** You define exactly where and when decryption happens. | **Key Management.** You are responsible for the persistence layer if scaling beyond one server. |
269
+ | **Data Suitability** | **Optimized for Secrets.** Perfect for PII, API Keys, CC numbers, and passwords. | **Small Payloads Only.** Do **not** use for database dumps or file uploads. For >1MB files, use a streaming encryption library. |
270
+
271
+ ---
272
+
273
+ ## Threat Model
274
+
275
+ ### What Ephem PROTECTS Against
276
+
277
+ * **Compromised TLS Termination**: If your load balancer or CDN terminates TLS and passes unencrypted traffic to your backend, that traffic is vulnerable to internal snoopers. Ephem keeps it encrypted until it reaches your application logic.
278
+ * **Man-in-the-Middle (MITM)**: If a corporate proxy or malicious actor intercepts the request (even with a trusted root CA), they cannot read the payload because they lack the ephemeral private key, which never leaves your app server's memory.
279
+ * **Replay Attacks**: Because capsules have `maxOpens` (default: 1), a captured valid request cannot be replayed to trigger a second action (e.g., duplicate payment).
280
+ * **Accidental Logging**: If you accidentally log the raw request body, you are only logging ciphertext.
281
+
282
+ ### What Ephem Does NOT Protect Against
283
+
284
+ * **XSS (Cross-Site Scripting)**: If an attacker can run JS on your page, they can hook the `seal()` function or read the input before it is sealed.
285
+ * **Compromised Server**: If the attacker controls your server, they can access the memory where private keys are stored (temporarily).
286
+ * **Compromised Client Device**: Keyloggers or malware on the user's machine.
287
+
288
+ ---
289
+
290
+ ## API Reference
291
+
292
+ ### `new Ephem(config)`
293
+
294
+ Creates a new Ephem instance.
295
+
296
+ ```typescript
297
+ const ephem = new Ephem({
298
+ inMemory: true,
299
+ maxConcurrentCapsules: 1000,
300
+ logging: true
301
+ });
302
+ ```
303
+
304
+ **Configuration Options:**
305
+
306
+ | Option | Type | Default | Description |
307
+ | :--- | :--- | :--- | :--- |
308
+ | **`inMemory`** | `boolean` | `true` | If `true`, store capsules in a JS Map. If `false`, you **must** provide the persistence hooks. |
309
+ | **`maxConcurrentCapsules`** | `number` | `Infinity` | **DoS Protection.** Limits the maximum number of active capsules in memory. If creating a new capsule would exceed this, `createCapsule` returns `null`. |
310
+ | **`defaultCaptsuleLifetimeMS`** | `number` | `Infinity` | Default duration before a capsule expires. Prevents stale keys from lingering forever. |
311
+ | **`capsuleCleanupIntervalMS`** | `number` | `60_000` | How often the internal "reaper" runs to delete expired/exhausted capsules from memory. |
312
+ | **`logging`** | `boolean` | `false` | Enable verbose internal logs. Useful for debugging but spammy in production. |
313
+ | **`onCapsuleCreation`** | `Function` | `undefined` | **Persistence Hook.** Called when a capsule is created. Signature: `(cid, privateKey, openCount, maxOpens, expiresAt)`. Return `true` to confirm storage. |
314
+ | **`onCapsuleOpen`** | `Function` | `undefined` | **Persistence Hook.** Called after a successful decryption. Signature: `(cid, openCount)`. Return `true` on success. |
315
+ | **`OnGetCapsuleByCID`** | `Function` | `undefined` | **Persistence Hook.** Retrieve capsule. Signature: `(cid)`. Returns `{ privateKey, maxOpens, openCount, expiresAt }` or `null`. |
316
+ | **`onCapsuleDelete`** | `Function` | `undefined` | **Persistence Hook.** Called to delete/expire. Signature: `(cid, reason)`. Reason is `'expired'` or `'max_opened'`. |
317
+
318
+ ---
319
+
320
+ ### `ephem.createCapsulePromise(config)`
321
+
322
+ Creates a new capsule and returns a Promise resolving to the public details.
323
+
324
+ **Parameters:**
325
+ * `config.maxOpens` (number, default `0`): The number of times this capsule can be decrypted. `0` usually means infinite (depending on your logic), but `1` is recommended for strict one-time use.
326
+ * `config.lifetimeDurationMS` (number): Milliseconds until the capsule expires.
327
+
328
+ **Returns:**
329
+ * `Promise<{ cid: string, publicKey: string } | null>`
330
+ * Returns `null` if the capsule could not be created (e.g., `maxConcurrentCapsules` limit reached or storage failure).
331
+
332
+ ---
333
+
334
+ ### `ephem.open(sealedPayload)`
335
+
336
+ Attempts to decrypt a sealed payload.
337
+
338
+ **Parameters:**
339
+ * `sealedPayload` (string): The dot-separated string generated by the client.
340
+
341
+ **Returns:**
342
+ * `Promise<string | null>`
343
+ * Returns the **decrypted plaintext** string if successful.
344
+ * Returns `null` if:
345
+ * The capsule ID (CID) is not found.
346
+ * The capsule has expired.
347
+ * The capsule has exceeded its `maxOpens` count.
348
+ * The decryption fails (wrong key, tampering detected).
349
+
350
+ * The decryption fails (wrong key, tampering detected).
351
+
352
+ ---
353
+
354
+ ### `ephem.getAnalytics()`
355
+
356
+ Returns a snapshot of the usage stats since the instance started.
357
+
358
+ **Returns:**
359
+ * `{ totalCreatedCapsules: number, totalUsedCapsules: number, totalExpiredCapsules: number }`
360
+
361
+ ---
362
+
363
+ ### `seal(text, publicKey, cid, allowInsecureFallback)` (Client)
364
+
365
+ The core client-side encryption function. Available in `ephem/client` (Node/Bundlers) or `EphemClient.seal` (CDN).
366
+
367
+ **Parameters:**
368
+ * **`text`** (string): The sensitive data to encrypt.
369
+ * **`publicKey`** (string): The PEM-formatted public key string received from the server.
370
+ * **`cid`** (string): The Capsule ID to attach to this payload.
371
+ * **`allowInsecureFallback`** (boolean, default `false`):
372
+ * **Crucial for Development.** WebCrypto is **only** available in Secure Contexts (HTTPS or `localhost`).
373
+ * If you are testing on HTTP (e.g., a local network IP `http://192.168.x.x`), WebCrypto will throw an error and the client will crash.
374
+ * **Effect:** Setting this to `true` causes the client to bypass encryption entirely. It sends the plaintext data prefixed with `##INSECURE##`.
375
+ * **SECURITY WARNING:** **Data is NOT encrypted in this mode.** This is strictly for debugging connectivity in non-HTTPS development environments. **NEVER** enable this in production.
376
+
377
+ ---
378
+
379
+ ### Full Redis Implementation
380
+
381
+ To scale Ephem across multiple server instances, you must use an external store like Redis.
382
+
383
+ You must implement **all 4 hooks**.
384
+
385
+ ```typescript
386
+ import Ephem from "ephem";
387
+ import Redis from "ioredis";
388
+
389
+ const redis = new Redis();
390
+ const getKey = (cid: string) => `ephem:capsule:${cid}`;
391
+
392
+ const ephem = new Ephem({
393
+ inMemory: false,
394
+
395
+ /**
396
+ * Hook 1: Creation
397
+ * Store the new capsule, including the PRIVATE KEY.
398
+ */
399
+ onCapsuleCreation: async (cid, privateKey, openCount, maxOpens, expiresAt) => {
400
+ try {
401
+ await redis.hset(getKey(cid), {
402
+ privateKey, // <--- CRITICAL: Store this securely!
403
+ openCount,
404
+ maxOpens,
405
+ expiresAt
406
+ });
407
+
408
+ // Set Redis TTL so it auto-deletes roughly when the capsule expires
409
+ if (expiresAt !== Infinity) {
410
+ const ttlSeconds = Math.ceil((expiresAt - Date.now()) / 1000);
411
+ if (ttlSeconds > 0) {
412
+ await redis.expire(getKey(cid), ttlSeconds);
413
+ }
414
+ }
415
+ return true;
416
+ } catch (err) {
417
+ console.error("Redis error on creation:", err);
418
+ return false; // Returning false will cause createCapsule to return null
419
+ }
420
+ },
421
+
422
+ /**
423
+ * Hook 2: Retrieval
424
+ * Fetch the capsule state so Ephem can decrypt the payload.
425
+ */
426
+ OnGetCapsuleByCID: async (cid) => {
427
+ try {
428
+ const data = await redis.hgetall(getKey(cid));
429
+
430
+ // If empty or missing private key, return null
431
+ if (!data || !data.privateKey) return null;
432
+
433
+ return {
434
+ privateKey: data.privateKey,
435
+ maxOpens: Number(data.maxOpens),
436
+ openCount: Number(data.openCount),
437
+ expiresAt: Number(data.expiresAt)
438
+ };
439
+ } catch (err) {
440
+ console.error("Redis error on get:", err);
441
+ return null;
442
+ }
443
+ },
444
+
445
+ /**
446
+ * Hook 3: Open (Usage Update)
447
+ * Called AFTER a successful decryption. We must increment the usage count in DB.
448
+ */
449
+ onCapsuleOpen: async (cid, openCount) => {
450
+ try {
451
+ // Ephem tracks the new 'openCount' in memory and passes it here.
452
+ // We just need to persist it.
453
+ await redis.hset(getKey(cid), "openCount", openCount);
454
+ return true;
455
+ } catch (err) {
456
+ console.error("Redis error on update:", err);
457
+ return false;
458
+ }
459
+ },
460
+
461
+ /**
462
+ * Hook 4: Deletion
463
+ * Explicit cleanup (e.g., if maxOpens reached).
464
+ */
465
+ onCapsuleDelete: async (cid, reason) => {
466
+ // Reason is 'expired' or 'max_opened'
467
+ try {
468
+ await redis.del(getKey(cid));
469
+ console.log(`Deleted capsule ${cid} due to ${reason}`);
470
+ } catch (err) {
471
+ console.error("Redis error on delete:", err);
472
+ }
473
+ }
474
+ });
475
+ ```
476
+
477
+ ## Contributing
478
+ Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
479
+
480
+ ## License
481
+ [ISC](/LICENSE)
@@ -0,0 +1,3 @@
1
+ declare function seal(text: string, publicKeyPem: string, cid: string, allowInsecureFallback?: boolean): Promise<string>;
2
+
3
+ export { seal };
@@ -0,0 +1,3 @@
1
+ declare function seal(text: string, publicKeyPem: string, cid: string, allowInsecureFallback?: boolean): Promise<string>;
2
+
3
+ export { seal };
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/client/index.ts
21
+ var client_exports = {};
22
+ __export(client_exports, {
23
+ seal: () => seal
24
+ });
25
+ module.exports = __toCommonJS(client_exports);
26
+ function pemToArrayBuffer(pem) {
27
+ const b64 = pem.replace(/-----BEGIN PUBLIC KEY-----/, "").replace(/-----END PUBLIC KEY-----/, "").replace(/\s+/g, "");
28
+ const binary = atob(b64);
29
+ const bytes = new Uint8Array(binary.length);
30
+ for (let i = 0; i < binary.length; i++) {
31
+ bytes[i] = binary.charCodeAt(i);
32
+ }
33
+ return bytes.buffer;
34
+ }
35
+ function base64Encode(data) {
36
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
37
+ let binary = "";
38
+ for (let i = 0; i < bytes.length; i++) {
39
+ binary += String.fromCharCode(bytes[i] || 0);
40
+ }
41
+ return btoa(binary);
42
+ }
43
+ async function seal(text, publicKeyPem, cid, allowInsecureFallback = false) {
44
+ if (!globalThis.crypto?.subtle) {
45
+ if (allowInsecureFallback) {
46
+ return `##INSECURE##${text}`;
47
+ } else {
48
+ throw new Error("EphemClient requires a secure context (HTTPS or localhost).");
49
+ }
50
+ }
51
+ const subtle = globalThis.crypto.subtle;
52
+ const enc = new TextEncoder();
53
+ const symKey = await subtle.generateKey(
54
+ { name: "AES-GCM", length: 256 },
55
+ true,
56
+ ["encrypt"]
57
+ );
58
+ const iv = crypto.getRandomValues(new Uint8Array(12));
59
+ const ciphertext = await subtle.encrypt(
60
+ { name: "AES-GCM", iv, additionalData: enc.encode(cid) },
61
+ symKey,
62
+ enc.encode(text)
63
+ );
64
+ const publicKey = await subtle.importKey(
65
+ "spki",
66
+ pemToArrayBuffer(publicKeyPem),
67
+ { name: "RSA-OAEP", hash: "SHA-256" },
68
+ false,
69
+ ["encrypt"]
70
+ );
71
+ const rawSymKey = await subtle.exportKey("raw", symKey);
72
+ const encryptedSymKey = await subtle.encrypt(
73
+ { name: "RSA-OAEP" },
74
+ publicKey,
75
+ rawSymKey
76
+ );
77
+ return [
78
+ cid,
79
+ base64Encode(iv),
80
+ base64Encode(ciphertext),
81
+ base64Encode(encryptedSymKey)
82
+ ].join(".");
83
+ }
84
+ // Annotate the CommonJS export names for ESM import in node:
85
+ 0 && (module.exports = {
86
+ seal
87
+ });
88
+ if (module.exports.default) { module.exports = module.exports.default; }
@@ -0,0 +1,63 @@
1
+ // src/client/index.ts
2
+ function pemToArrayBuffer(pem) {
3
+ const b64 = pem.replace(/-----BEGIN PUBLIC KEY-----/, "").replace(/-----END PUBLIC KEY-----/, "").replace(/\s+/g, "");
4
+ const binary = atob(b64);
5
+ const bytes = new Uint8Array(binary.length);
6
+ for (let i = 0; i < binary.length; i++) {
7
+ bytes[i] = binary.charCodeAt(i);
8
+ }
9
+ return bytes.buffer;
10
+ }
11
+ function base64Encode(data) {
12
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
13
+ let binary = "";
14
+ for (let i = 0; i < bytes.length; i++) {
15
+ binary += String.fromCharCode(bytes[i] || 0);
16
+ }
17
+ return btoa(binary);
18
+ }
19
+ async function seal(text, publicKeyPem, cid, allowInsecureFallback = false) {
20
+ if (!globalThis.crypto?.subtle) {
21
+ if (allowInsecureFallback) {
22
+ return `##INSECURE##${text}`;
23
+ } else {
24
+ throw new Error("EphemClient requires a secure context (HTTPS or localhost).");
25
+ }
26
+ }
27
+ const subtle = globalThis.crypto.subtle;
28
+ const enc = new TextEncoder();
29
+ const symKey = await subtle.generateKey(
30
+ { name: "AES-GCM", length: 256 },
31
+ true,
32
+ ["encrypt"]
33
+ );
34
+ const iv = crypto.getRandomValues(new Uint8Array(12));
35
+ const ciphertext = await subtle.encrypt(
36
+ { name: "AES-GCM", iv, additionalData: enc.encode(cid) },
37
+ symKey,
38
+ enc.encode(text)
39
+ );
40
+ const publicKey = await subtle.importKey(
41
+ "spki",
42
+ pemToArrayBuffer(publicKeyPem),
43
+ { name: "RSA-OAEP", hash: "SHA-256" },
44
+ false,
45
+ ["encrypt"]
46
+ );
47
+ const rawSymKey = await subtle.exportKey("raw", symKey);
48
+ const encryptedSymKey = await subtle.encrypt(
49
+ { name: "RSA-OAEP" },
50
+ publicKey,
51
+ rawSymKey
52
+ );
53
+ return [
54
+ cid,
55
+ base64Encode(iv),
56
+ base64Encode(ciphertext),
57
+ base64Encode(encryptedSymKey)
58
+ ].join(".");
59
+ }
60
+ export {
61
+ seal
62
+ };
63
+ if (module.exports.default) { module.exports = module.exports.default; }