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.
@@ -0,0 +1,87 @@
1
+ type Key = string;
2
+ interface Capsule {
3
+ privateKey: Key;
4
+ maxOpens: number;
5
+ openCount: number;
6
+ expiresAt: number;
7
+ }
8
+ type OnCreationFunction = (cid: string, privateKey: string, openCount: number, maxOpens: number, expiresAt: number) => Promise<boolean>;
9
+ type OnOpenFunction = (cid: string, openCount: number) => Promise<boolean>;
10
+ type OnGetByCIDFunction = (cid: string) => Promise<{
11
+ privateKey: unknown;
12
+ maxOpens: unknown;
13
+ openCount: unknown;
14
+ expiresAt: unknown;
15
+ } | null>;
16
+ type CapsuleDeletionReason = 'expired' | 'max_opened';
17
+ type OnDeletionFuntion = (cid: string, deleteReason: CapsuleDeletionReason) => Promise<void>;
18
+ interface EphemConfigInMemory {
19
+ inMemory?: true;
20
+ maxConcurrentCapsules?: number;
21
+ defaultCaptsuleLifetimeMS?: number;
22
+ capsuleCleanupIntervalMS?: number;
23
+ onCapsuleCreation?: OnCreationFunction;
24
+ onCapsuleOpen?: OnOpenFunction;
25
+ OnGetCapsuleByCID?: OnGetByCIDFunction;
26
+ onCapsuleDelete?: OnDeletionFuntion;
27
+ logging?: boolean;
28
+ }
29
+ interface EphemConfigPersistent {
30
+ inMemory: false;
31
+ maxConcurrentCapsules?: number;
32
+ defaultCaptsuleLifetimeMS?: number;
33
+ capsuleCleanupIntervalMS?: number;
34
+ onCapsuleCreation: OnCreationFunction;
35
+ onCapsuleOpen: OnOpenFunction;
36
+ OnGetCapsuleByCID: OnGetByCIDFunction;
37
+ onCapsuleDelete: OnDeletionFuntion;
38
+ logging?: boolean;
39
+ }
40
+ type EphemConfig = EphemConfigInMemory | EphemConfigPersistent;
41
+ interface CreateCapsuleReturnObj {
42
+ cid: string;
43
+ publicKey: string;
44
+ }
45
+ interface CreateCapsuleConfig {
46
+ maxOpens?: number;
47
+ lifetimeDurationMS?: number;
48
+ }
49
+ type CreateCapsuleCallback = (res: CreateCapsuleReturnObj | null) => void | Promise<void>;
50
+ /**
51
+ * Keeps track of ephem's analytics since initialization.
52
+ */
53
+ declare class Analytics {
54
+ totalCreatedCapsules: number;
55
+ totalUsedCapsules: number;
56
+ totalExpiredCapsules: number;
57
+ constructor();
58
+ }
59
+ declare class Ephem {
60
+ private readonly config;
61
+ private genUUID;
62
+ private slug;
63
+ private capsules;
64
+ private analytics;
65
+ private log;
66
+ constructor(config?: EphemConfig);
67
+ /**
68
+ * Alias Handshake
69
+ */
70
+ createCapsule(callback: CreateCapsuleCallback, config?: CreateCapsuleConfig): Promise<void>;
71
+ createCapsulePromise(config?: CreateCapsuleConfig): Promise<CreateCapsuleReturnObj | null>;
72
+ /**
73
+ * Handles periodic cleanup of in-memory capsules.
74
+ * Works only if capsules are in memory.
75
+ * Else, user should implement their own cleanup elsewhere
76
+ */
77
+ private capsuleCleanUp;
78
+ getCapsule(cid: string): Promise<Capsule | null>;
79
+ removeIndividualCapsule(cid: string, reason: CapsuleDeletionReason): void;
80
+ /**
81
+ * Decrypts encrypted data.
82
+ */
83
+ open(cipher: string): Promise<string | null>;
84
+ getAnalytics(): Readonly<Analytics>;
85
+ }
86
+
87
+ export { type Capsule, type CapsuleDeletionReason, type CreateCapsuleCallback, type CreateCapsuleConfig, type CreateCapsuleReturnObj, type EphemConfig, type EphemConfigInMemory, type EphemConfigPersistent, type Key, type OnCreationFunction, type OnDeletionFuntion, type OnGetByCIDFunction, type OnOpenFunction, Ephem as default };
@@ -0,0 +1,89 @@
1
+ type Key = string;
2
+ interface Capsule {
3
+ privateKey: Key;
4
+ maxOpens: number;
5
+ openCount: number;
6
+ expiresAt: number;
7
+ }
8
+ type OnCreationFunction = (cid: string, privateKey: string, openCount: number, maxOpens: number, expiresAt: number) => Promise<boolean>;
9
+ type OnOpenFunction = (cid: string, openCount: number) => Promise<boolean>;
10
+ type OnGetByCIDFunction = (cid: string) => Promise<{
11
+ privateKey: unknown;
12
+ maxOpens: unknown;
13
+ openCount: unknown;
14
+ expiresAt: unknown;
15
+ } | null>;
16
+ type CapsuleDeletionReason = 'expired' | 'max_opened';
17
+ type OnDeletionFuntion = (cid: string, deleteReason: CapsuleDeletionReason) => Promise<void>;
18
+ interface EphemConfigInMemory {
19
+ inMemory?: true;
20
+ maxConcurrentCapsules?: number;
21
+ defaultCaptsuleLifetimeMS?: number;
22
+ capsuleCleanupIntervalMS?: number;
23
+ onCapsuleCreation?: OnCreationFunction;
24
+ onCapsuleOpen?: OnOpenFunction;
25
+ OnGetCapsuleByCID?: OnGetByCIDFunction;
26
+ onCapsuleDelete?: OnDeletionFuntion;
27
+ logging?: boolean;
28
+ }
29
+ interface EphemConfigPersistent {
30
+ inMemory: false;
31
+ maxConcurrentCapsules?: number;
32
+ defaultCaptsuleLifetimeMS?: number;
33
+ capsuleCleanupIntervalMS?: number;
34
+ onCapsuleCreation: OnCreationFunction;
35
+ onCapsuleOpen: OnOpenFunction;
36
+ OnGetCapsuleByCID: OnGetByCIDFunction;
37
+ onCapsuleDelete: OnDeletionFuntion;
38
+ logging?: boolean;
39
+ }
40
+ type EphemConfig = EphemConfigInMemory | EphemConfigPersistent;
41
+ interface CreateCapsuleReturnObj {
42
+ cid: string;
43
+ publicKey: string;
44
+ }
45
+ interface CreateCapsuleConfig {
46
+ maxOpens?: number;
47
+ lifetimeDurationMS?: number;
48
+ }
49
+ type CreateCapsuleCallback = (res: CreateCapsuleReturnObj | null) => void | Promise<void>;
50
+ /**
51
+ * Keeps track of ephem's analytics since initialization.
52
+ */
53
+ declare class Analytics {
54
+ totalCreatedCapsules: number;
55
+ totalUsedCapsules: number;
56
+ totalExpiredCapsules: number;
57
+ constructor();
58
+ }
59
+ declare class Ephem {
60
+ private readonly config;
61
+ private genUUID;
62
+ private slug;
63
+ private capsules;
64
+ private analytics;
65
+ private log;
66
+ constructor(config?: EphemConfig);
67
+ /**
68
+ * Alias Handshake
69
+ */
70
+ createCapsule(callback: CreateCapsuleCallback, config?: CreateCapsuleConfig): Promise<void>;
71
+ createCapsulePromise(config?: CreateCapsuleConfig): Promise<CreateCapsuleReturnObj | null>;
72
+ /**
73
+ * Handles periodic cleanup of in-memory capsules.
74
+ * Works only if capsules are in memory.
75
+ * Else, user should implement their own cleanup elsewhere
76
+ */
77
+ private capsuleCleanUp;
78
+ getCapsule(cid: string): Promise<Capsule | null>;
79
+ removeIndividualCapsule(cid: string, reason: CapsuleDeletionReason): void;
80
+ /**
81
+ * Decrypts encrypted data.
82
+ */
83
+ open(cipher: string): Promise<string | null>;
84
+ getAnalytics(): Readonly<Analytics>;
85
+ }
86
+
87
+ // @ts-ignore
88
+ export = Ephem;
89
+ export type { Capsule, CapsuleDeletionReason, CreateCapsuleCallback, CreateCapsuleConfig, CreateCapsuleReturnObj, EphemConfig, EphemConfigInMemory, EphemConfigPersistent, Key, OnCreationFunction, OnDeletionFuntion, OnGetByCIDFunction, OnOpenFunction };
package/dist/index.js ADDED
@@ -0,0 +1,308 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ default: () => Ephem
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_node_crypto = require("crypto");
27
+ var import_node_crypto2 = require("crypto");
28
+ var Analytics = class {
29
+ totalCreatedCapsules;
30
+ totalUsedCapsules;
31
+ totalExpiredCapsules;
32
+ constructor() {
33
+ this.totalCreatedCapsules = 0;
34
+ this.totalExpiredCapsules = 0;
35
+ this.totalUsedCapsules = 0;
36
+ }
37
+ };
38
+ var DEFAULT_IN_MEMORY_CONFIG = {
39
+ inMemory: true,
40
+ maxConcurrentCapsules: Infinity,
41
+ defaultCaptsuleLifetimeMS: Infinity,
42
+ capsuleCleanupIntervalMS: 6e4
43
+ };
44
+ function normalizeConfig(config) {
45
+ if (config?.inMemory === false) {
46
+ return {
47
+ ...DEFAULT_IN_MEMORY_CONFIG,
48
+ ...config,
49
+ inMemory: false
50
+ };
51
+ }
52
+ return {
53
+ ...DEFAULT_IN_MEMORY_CONFIG,
54
+ ...config,
55
+ inMemory: true
56
+ };
57
+ }
58
+ function generateCapsuleKeyPair() {
59
+ return new Promise((resolve, reject) => {
60
+ (0, import_node_crypto.generateKeyPair)(
61
+ "rsa",
62
+ {
63
+ modulusLength: 2048,
64
+ publicKeyEncoding: {
65
+ type: "spki",
66
+ format: "pem"
67
+ },
68
+ privateKeyEncoding: {
69
+ type: "pkcs8",
70
+ format: "pem"
71
+ }
72
+ },
73
+ (err, publicKey, privateKey) => {
74
+ if (err) return reject(err);
75
+ resolve({ publicKey, privateKey });
76
+ }
77
+ );
78
+ });
79
+ }
80
+ function base64ToBuffer(b64) {
81
+ return Buffer.from(b64, "base64");
82
+ }
83
+ var Ephem = class {
84
+ config;
85
+ genUUID = () => (0, import_node_crypto.randomUUID)();
86
+ slug = `Ephem`;
87
+ capsules;
88
+ analytics;
89
+ log(...message) {
90
+ if (this.config.logging) {
91
+ console.log(`${this.slug} =>`, ...message);
92
+ }
93
+ }
94
+ constructor(config) {
95
+ this.analytics = new Analytics();
96
+ this.config = normalizeConfig(config);
97
+ this.capsules = /* @__PURE__ */ new Map();
98
+ this.log(`initialized with ${this.config.inMemory ? `in-memory` : `persistent`} mode.`);
99
+ if (this.config.inMemory) {
100
+ setTimeout(() => {
101
+ this.capsuleCleanUp();
102
+ }, this.config.capsuleCleanupIntervalMS || 6e4);
103
+ }
104
+ }
105
+ /**
106
+ * Alias Handshake
107
+ */
108
+ async createCapsule(callback, config = {}) {
109
+ try {
110
+ if (this.config.inMemory && this.capsules.size >= (this.config.maxConcurrentCapsules || Infinity)) {
111
+ callback(null);
112
+ } else {
113
+ const { privateKey, publicKey } = await generateCapsuleKeyPair();
114
+ const cid = this.genUUID();
115
+ const lifetime = config.lifetimeDurationMS === void 0 ? this.config.defaultCaptsuleLifetimeMS ? Math.max(1, this.config.defaultCaptsuleLifetimeMS) : Infinity : Math.max(1, config.lifetimeDurationMS);
116
+ const expiresAt = lifetime === Infinity ? Infinity : Date.now() + lifetime;
117
+ const maxOpens = Math.max(1, config.maxOpens || 0);
118
+ const cbData = {
119
+ publicKey,
120
+ cid
121
+ };
122
+ if (this.config.inMemory) {
123
+ const capsule = {
124
+ expiresAt: expiresAt === Infinity ? 0 : expiresAt,
125
+ maxOpens: maxOpens === Infinity ? 0 : maxOpens,
126
+ openCount: 0,
127
+ privateKey
128
+ };
129
+ this.capsules.set(cid, capsule);
130
+ this.analytics.totalCreatedCapsules++;
131
+ this.log(`new capsul with id ${cid}`);
132
+ callback(cbData);
133
+ } else if (this.config.onCapsuleCreation) {
134
+ const res = await this.config.onCapsuleCreation(cid, privateKey, 0, maxOpens === Infinity ? 0 : maxOpens, expiresAt === Infinity ? 0 : expiresAt);
135
+ if (res) {
136
+ this.analytics.totalCreatedCapsules++;
137
+ this.log(`new capsul with id ${cid}`);
138
+ callback(cbData);
139
+ } else {
140
+ this.log(`Error encountered during capsule creation:`, `onCapsuleCreation returned false.`);
141
+ callback(null);
142
+ }
143
+ } else {
144
+ this.log(`Error encountered during capsule creation:`, `onCapsuleCreation not defined in persistent mode.`);
145
+ callback(null);
146
+ }
147
+ }
148
+ } catch (error) {
149
+ this.log(`Error encountered during capsule creation:`, error);
150
+ callback(null);
151
+ }
152
+ }
153
+ createCapsulePromise(config = {}) {
154
+ return new Promise(async (resolve, reject) => {
155
+ this.createCapsule((r) => {
156
+ if (r === null) {
157
+ reject(r);
158
+ } else {
159
+ resolve(r);
160
+ }
161
+ }, config);
162
+ });
163
+ }
164
+ /**
165
+ * Handles periodic cleanup of in-memory capsules.
166
+ * Works only if capsules are in memory.
167
+ * Else, user should implement their own cleanup elsewhere
168
+ */
169
+ capsuleCleanUp() {
170
+ this.log(`cleanup initialized.`);
171
+ const start = Date.now();
172
+ let n = 0;
173
+ for (const [cid, capsule] of this.capsules) {
174
+ if (capsule.expiresAt <= Date.now()) {
175
+ this.analytics.totalExpiredCapsules++;
176
+ this.capsules.delete(cid);
177
+ n++;
178
+ } else if (capsule.openCount >= capsule.maxOpens && capsule.maxOpens != 0) {
179
+ this.analytics.totalUsedCapsules++;
180
+ this.capsules.delete(cid);
181
+ n++;
182
+ }
183
+ }
184
+ if (n) {
185
+ this.log(`${n} capsule${n == 1 ? "" : "s"} deleted due to expiry or max usage.`);
186
+ } else {
187
+ this.log(`no capsule deleted.`);
188
+ }
189
+ const duration = this.config.capsuleCleanupIntervalMS || 6e4;
190
+ const elapsed = Date.now() - start;
191
+ if (elapsed >= duration) {
192
+ this.capsuleCleanUp();
193
+ } else {
194
+ setTimeout(() => this.capsuleCleanUp(), Math.max(0, duration - elapsed));
195
+ }
196
+ this.log(`cleanup ended.`);
197
+ }
198
+ async getCapsule(cid) {
199
+ if (this.config.inMemory) {
200
+ if (this.capsules.has(cid)) {
201
+ return this.capsules.get(cid);
202
+ } else {
203
+ return null;
204
+ }
205
+ } else {
206
+ if (this.config.OnGetCapsuleByCID) {
207
+ const r = await this.config.OnGetCapsuleByCID(cid);
208
+ if (r) {
209
+ const capsule = {
210
+ expiresAt: Math.max(Number(r.expiresAt) || 0, 0),
211
+ maxOpens: Math.max(Number(r.maxOpens) || 0, 1),
212
+ openCount: Math.max(Number(r.openCount) || 0, 0),
213
+ privateKey: String(r.privateKey) || ""
214
+ };
215
+ return capsule;
216
+ } else {
217
+ this.log(`Get capsule error: OnGetCapsuleByCID did not return a capsule.`);
218
+ return null;
219
+ }
220
+ } else {
221
+ this.log(`Get capsule error: OnGetCapsuleByCID not supplied.`);
222
+ return null;
223
+ }
224
+ }
225
+ }
226
+ removeIndividualCapsule(cid, reason) {
227
+ if (this.config.inMemory) {
228
+ if (this.capsules.has(cid)) {
229
+ this.capsules.delete(cid);
230
+ this.log(`${cid} removed due to '${reason}'.`);
231
+ }
232
+ } else {
233
+ if (this.config.onCapsuleDelete) {
234
+ this.config.onCapsuleDelete(cid, reason);
235
+ } else {
236
+ this.log(`warn: onCapsuleDelete was just required, but not implemented.`);
237
+ }
238
+ }
239
+ }
240
+ /**
241
+ * Decrypts encrypted data.
242
+ */
243
+ async open(cipher) {
244
+ if (cipher.startsWith("##INSECURE##")) {
245
+ const plainText = cipher.slice("##INSECURE##".length);
246
+ this.log(`WARN: Insecure dev-mode capsule opened.`);
247
+ return plainText;
248
+ }
249
+ const parts = cipher.split(".");
250
+ if (parts.length !== 4) return null;
251
+ let [cid, ivB64, cipherTextB64, encKeyB64] = parts;
252
+ cid = cid || "InvalidPlaceHolder";
253
+ const capsule = await this.getCapsule(cid);
254
+ if (!capsule) return null;
255
+ const { expiresAt, maxOpens, openCount, privateKey } = capsule;
256
+ if (expiresAt !== 0 && Date.now() >= expiresAt) {
257
+ this.removeIndividualCapsule(cid, "expired");
258
+ this.analytics.totalExpiredCapsules++;
259
+ return null;
260
+ }
261
+ if (maxOpens !== 0 && openCount >= maxOpens) {
262
+ this.removeIndividualCapsule(cid, "max_opened");
263
+ this.analytics.totalUsedCapsules++;
264
+ return null;
265
+ }
266
+ try {
267
+ const aesKey = (0, import_node_crypto2.privateDecrypt)(
268
+ {
269
+ key: privateKey,
270
+ padding: import_node_crypto2.constants.RSA_PKCS1_OAEP_PADDING,
271
+ oaepHash: "sha256"
272
+ },
273
+ base64ToBuffer(encKeyB64 || "")
274
+ );
275
+ const iv = base64ToBuffer(ivB64 || "");
276
+ const cipherBuf = base64ToBuffer(cipherTextB64 || "");
277
+ const authTag = cipherBuf.subarray(cipherBuf.length - 16);
278
+ const encrypted = cipherBuf.subarray(0, cipherBuf.length - 16);
279
+ const decipher = (0, import_node_crypto2.createDecipheriv)("aes-256-gcm", aesKey, iv);
280
+ decipher.setAuthTag(authTag);
281
+ decipher.setAAD(Buffer.from(cid));
282
+ const decrypted = decipher.update(encrypted) + decipher.final("utf8");
283
+ capsule.openCount++;
284
+ if (this.config.inMemory) {
285
+ this.capsules.set(cid, capsule);
286
+ } else if (this.config.onCapsuleOpen) {
287
+ await this.config.onCapsuleOpen(cid, capsule.openCount);
288
+ }
289
+ if (capsule.expiresAt !== 0 && Date.now() >= capsule.expiresAt) {
290
+ this.removeIndividualCapsule(cid, "expired");
291
+ this.analytics.totalExpiredCapsules++;
292
+ }
293
+ if (capsule.maxOpens !== 0 && capsule.openCount >= capsule.maxOpens) {
294
+ this.removeIndividualCapsule(cid, "max_opened");
295
+ this.analytics.totalUsedCapsules++;
296
+ }
297
+ return decrypted;
298
+ } catch (err) {
299
+ this.log("open() failed:", err);
300
+ return null;
301
+ }
302
+ }
303
+ getAnalytics() {
304
+ const snap = { ...this.analytics };
305
+ return snap;
306
+ }
307
+ };
308
+ if (module.exports.default) { module.exports = module.exports.default; }