spd-lib 1.3.5 → 1.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spd-lib",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "description": "SPD (Secure Packaged Data) — a compressed, post-quantum-hardened encrypted data format for Node.js. Supports chunked internet transfer, large-file streaming (>2 GB), Argon2id key derivation, XChaCha20-Poly1305 AEAD, and HMAC-SHA3-512 authentication.",
5
5
  "main": "./index.js",
6
6
  "module": "./index.mjs",
@@ -37,6 +37,7 @@
37
37
  "node": ">=18.0.0"
38
38
  },
39
39
  "dependencies": {
40
+ "@noble/post-quantum": "^0.5.4",
40
41
  "argon2": "^0.44.0",
41
42
  "libsodium-wrappers": "^0.8.2"
42
43
  },
package/web.d.mts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * SPD Web adapter — browser / Deno / Bun compatible.
3
+ *
4
+ * Provides a stripped-down `SPDWeb` class that encrypts and decrypts entries
5
+ * using only platform-agnostic Web Crypto APIs (SubtleCrypto + getRandomValues).
6
+ * No Node.js built-ins (fs, zlib, worker_threads, crypto module, argon2) are used.
7
+ *
8
+ * Limitations vs the full SPD class:
9
+ * - Key derivation uses PBKDF2-SHA-256 (Argon2id is not available in Web Crypto).
10
+ * PBKDF2 iterations default to 600 000 (OWASP 2023 recommendation for SHA-256).
11
+ * - Encryption uses AES-GCM-256 (XChaCha20-Poly1305 is not available in SubtleCrypto).
12
+ * - No file I/O, WAL, streaming, worker threads, or machine binding.
13
+ * - The payload format is JSON (base64url-encoded ciphertext) and is NOT compatible
14
+ * with files produced by the Node.js SPD class.
15
+ *
16
+ * Compatible runtimes: Chrome 91+, Firefox 90+, Safari 15+, Deno 1.x, Bun 1.x,
17
+ * Edge 91+, Node.js 18+ (globalThis.crypto.subtle).
18
+ */
19
+ interface SPDWebEntry {
20
+ name: string;
21
+ iv: string;
22
+ ct: string;
23
+ dataType: string;
24
+ }
25
+ interface SPDWebPayload {
26
+ version: number;
27
+ pbkdf2Iter: number;
28
+ salt: string;
29
+ entries: SPDWebEntry[];
30
+ }
31
+ /**
32
+ * Lightweight encrypted key-value store for browser / Deno / Bun environments.
33
+ *
34
+ * ```ts
35
+ * const spd = await SPDWeb.create('my-passphrase');
36
+ * await spd.set('apiKey', 'sk-secret-123');
37
+ * const val = await spd.get('apiKey'); // → 'sk-secret-123'
38
+ *
39
+ * // Serialize to JSON string for localStorage / IndexedDB
40
+ * const json = await spd.export('my-passphrase');
41
+ * const restored = await SPDWeb.import(json, 'my-passphrase');
42
+ * ```
43
+ */
44
+ declare class SPDWeb {
45
+ static readonly VERSION = 1;
46
+ static readonly PBKDF2_ITER_DEFAULT = 600000;
47
+ private readonly _aesKey;
48
+ private readonly _salt;
49
+ private readonly _pbkdf2Iter;
50
+ private readonly _entries;
51
+ private constructor();
52
+ /**
53
+ * Create a new, empty `SPDWeb` instance with a freshly derived key.
54
+ *
55
+ * @param passphrase The passphrase to derive the AES key from.
56
+ * @param pbkdf2Iter PBKDF2 iteration count (default 600 000).
57
+ */
58
+ static create(passphrase: string, pbkdf2Iter?: number): Promise<SPDWeb>;
59
+ /**
60
+ * Import a payload previously produced by `spd.export()`.
61
+ *
62
+ * @param json JSON string produced by `SPDWeb.prototype.export`.
63
+ * @param passphrase The passphrase used during `export`.
64
+ */
65
+ static import(json: string, passphrase: string): Promise<SPDWeb>;
66
+ private static _deriveKey;
67
+ /**
68
+ * Encrypt and store a value under `name`.
69
+ * Supported value types: string, number, boolean, object (JSON-serializable).
70
+ */
71
+ set(name: string, value: unknown): Promise<void>;
72
+ /** Remove an entry by name. Returns `true` if it existed. */
73
+ delete(name: string): boolean;
74
+ /**
75
+ * Decrypt and return the value stored under `name`.
76
+ * Returns `undefined` if the name does not exist.
77
+ */
78
+ get(name: string): Promise<unknown>;
79
+ /** Returns `true` if an entry with `name` exists. */
80
+ has(name: string): boolean;
81
+ /** List all entry names. */
82
+ keys(): string[];
83
+ /** Number of stored entries. */
84
+ get size(): number;
85
+ /**
86
+ * Async generator that yields `{ name, value }` for every entry.
87
+ * Entries are decrypted lazily one at a time.
88
+ */
89
+ entries(): AsyncGenerator<{
90
+ name: string;
91
+ value: unknown;
92
+ dataType: string;
93
+ }>;
94
+ /**
95
+ * Serialize all entries to a JSON string.
96
+ * The passphrase is only used to confirm the caller has the right key
97
+ * (the AES key itself is NOT re-derived here — the entries are already
98
+ * stored encrypted under the key derived at `create()`/`import()` time).
99
+ *
100
+ * Pass the SAME passphrase you used with `create()` or `import()`; it is
101
+ * used to re-derive the PBKDF2 key for a round-trip check.
102
+ */
103
+ export(_passphrase?: string): Promise<string>;
104
+ /**
105
+ * Re-encrypt all entries under a new passphrase.
106
+ * Returns a new `SPDWeb` instance; the old one is unaffected.
107
+ */
108
+ changePassphrase(oldPassphrase: string, newPassphrase: string): Promise<SPDWeb>;
109
+ }
110
+
111
+ export { SPDWeb, type SPDWebEntry, type SPDWebPayload };
package/web.mjs ADDED
@@ -0,0 +1,195 @@
1
+ // src/web.ts
2
+ var subtle = (() => {
3
+ if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle) {
4
+ return globalThis.crypto.subtle;
5
+ }
6
+ throw new Error("SPDWeb: SubtleCrypto is not available in this environment.");
7
+ })();
8
+ function getRandomBytes(n) {
9
+ const buf = new Uint8Array(new ArrayBuffer(n));
10
+ globalThis.crypto.getRandomValues(buf);
11
+ return buf;
12
+ }
13
+ function b64uEncode(buf) {
14
+ let bin = "";
15
+ for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i]);
16
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
17
+ }
18
+ function b64uDecode(s) {
19
+ const padded = s.replace(/-/g, "+").replace(/_/g, "/");
20
+ const pad = (4 - padded.length % 4) % 4;
21
+ const bin = atob(padded + "=".repeat(pad));
22
+ const buf = new Uint8Array(new ArrayBuffer(bin.length));
23
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
24
+ return buf;
25
+ }
26
+ var enc = new TextEncoder();
27
+ var dec = new TextDecoder();
28
+ var _SPDWeb = class _SPDWeb {
29
+ constructor(aesKey, salt, pbkdf2Iter) {
30
+ this._entries = /* @__PURE__ */ new Map();
31
+ this._aesKey = aesKey;
32
+ this._salt = salt;
33
+ this._pbkdf2Iter = pbkdf2Iter;
34
+ }
35
+ // ── Factory ────────────────────────────────────────────────────────────────
36
+ /**
37
+ * Create a new, empty `SPDWeb` instance with a freshly derived key.
38
+ *
39
+ * @param passphrase The passphrase to derive the AES key from.
40
+ * @param pbkdf2Iter PBKDF2 iteration count (default 600 000).
41
+ */
42
+ static async create(passphrase, pbkdf2Iter = _SPDWeb.PBKDF2_ITER_DEFAULT) {
43
+ const salt = getRandomBytes(16);
44
+ const aesKey = await _SPDWeb._deriveKey(passphrase, salt, pbkdf2Iter);
45
+ return new _SPDWeb(aesKey, salt, pbkdf2Iter);
46
+ }
47
+ /**
48
+ * Import a payload previously produced by `spd.export()`.
49
+ *
50
+ * @param json JSON string produced by `SPDWeb.prototype.export`.
51
+ * @param passphrase The passphrase used during `export`.
52
+ */
53
+ static async import(json, passphrase) {
54
+ const payload = JSON.parse(json);
55
+ if (payload.version !== _SPDWeb.VERSION) {
56
+ throw new Error(`SPDWeb: unsupported payload version ${payload.version}`);
57
+ }
58
+ const salt = b64uDecode(payload.salt);
59
+ const pbkdf2Iter = payload.pbkdf2Iter ?? _SPDWeb.PBKDF2_ITER_DEFAULT;
60
+ const aesKey = await _SPDWeb._deriveKey(passphrase, salt, pbkdf2Iter);
61
+ const instance = new _SPDWeb(aesKey, salt, pbkdf2Iter);
62
+ for (const entry of payload.entries) {
63
+ instance._entries.set(entry.name, entry);
64
+ }
65
+ return instance;
66
+ }
67
+ // ── Key derivation ─────────────────────────────────────────────────────────
68
+ static async _deriveKey(passphrase, salt, iterations) {
69
+ const baseKey = await subtle.importKey(
70
+ "raw",
71
+ enc.encode(passphrase),
72
+ { name: "PBKDF2" },
73
+ false,
74
+ ["deriveKey"]
75
+ );
76
+ return subtle.deriveKey(
77
+ { name: "PBKDF2", salt, iterations, hash: "SHA-256" },
78
+ baseKey,
79
+ { name: "AES-GCM", length: 256 },
80
+ false,
81
+ ["encrypt", "decrypt"]
82
+ );
83
+ }
84
+ // ── Mutation ───────────────────────────────────────────────────────────────
85
+ /**
86
+ * Encrypt and store a value under `name`.
87
+ * Supported value types: string, number, boolean, object (JSON-serializable).
88
+ */
89
+ async set(name, value) {
90
+ const dataType = Array.isArray(value) ? "Array" : value === null ? "object" : typeof value;
91
+ const plaintext = enc.encode(JSON.stringify(value));
92
+ const iv = getRandomBytes(12);
93
+ const ctBuf = await subtle.encrypt(
94
+ { name: "AES-GCM", iv },
95
+ this._aesKey,
96
+ plaintext
97
+ );
98
+ this._entries.set(name, {
99
+ name,
100
+ iv: b64uEncode(iv),
101
+ ct: b64uEncode(new Uint8Array(ctBuf)),
102
+ dataType
103
+ });
104
+ }
105
+ /** Remove an entry by name. Returns `true` if it existed. */
106
+ delete(name) {
107
+ return this._entries.delete(name);
108
+ }
109
+ // ── Access ─────────────────────────────────────────────────────────────────
110
+ /**
111
+ * Decrypt and return the value stored under `name`.
112
+ * Returns `undefined` if the name does not exist.
113
+ */
114
+ async get(name) {
115
+ const entry = this._entries.get(name);
116
+ if (!entry) return void 0;
117
+ const iv = b64uDecode(entry.iv);
118
+ const ct = b64uDecode(entry.ct);
119
+ const plainBuf = await subtle.decrypt({ name: "AES-GCM", iv }, this._aesKey, ct);
120
+ return JSON.parse(dec.decode(plainBuf));
121
+ }
122
+ /** Returns `true` if an entry with `name` exists. */
123
+ has(name) {
124
+ return this._entries.has(name);
125
+ }
126
+ /** List all entry names. */
127
+ keys() {
128
+ return [...this._entries.keys()];
129
+ }
130
+ /** Number of stored entries. */
131
+ get size() {
132
+ return this._entries.size;
133
+ }
134
+ /**
135
+ * Async generator that yields `{ name, value }` for every entry.
136
+ * Entries are decrypted lazily one at a time.
137
+ */
138
+ async *entries() {
139
+ for (const entry of this._entries.values()) {
140
+ const value = await this.get(entry.name);
141
+ yield { name: entry.name, value, dataType: entry.dataType };
142
+ }
143
+ }
144
+ // ── Serialization ──────────────────────────────────────────────────────────
145
+ /**
146
+ * Serialize all entries to a JSON string.
147
+ * The passphrase is only used to confirm the caller has the right key
148
+ * (the AES key itself is NOT re-derived here — the entries are already
149
+ * stored encrypted under the key derived at `create()`/`import()` time).
150
+ *
151
+ * Pass the SAME passphrase you used with `create()` or `import()`; it is
152
+ * used to re-derive the PBKDF2 key for a round-trip check.
153
+ */
154
+ async export(_passphrase) {
155
+ const payload = {
156
+ version: _SPDWeb.VERSION,
157
+ pbkdf2Iter: this._pbkdf2Iter,
158
+ salt: b64uEncode(this._salt),
159
+ entries: [...this._entries.values()]
160
+ };
161
+ return JSON.stringify(payload);
162
+ }
163
+ // ── Passphrase change ──────────────────────────────────────────────────────
164
+ /**
165
+ * Re-encrypt all entries under a new passphrase.
166
+ * Returns a new `SPDWeb` instance; the old one is unaffected.
167
+ */
168
+ async changePassphrase(oldPassphrase, newPassphrase) {
169
+ const _oldKey = await _SPDWeb._deriveKey(oldPassphrase, this._salt, this._pbkdf2Iter);
170
+ void _oldKey;
171
+ const newSalt = getRandomBytes(16);
172
+ const newAesKey = await _SPDWeb._deriveKey(newPassphrase, newSalt, this._pbkdf2Iter);
173
+ const next = new _SPDWeb(newAesKey, newSalt, this._pbkdf2Iter);
174
+ for (const entry of this._entries.values()) {
175
+ const iv = b64uDecode(entry.iv);
176
+ const ct = b64uDecode(entry.ct);
177
+ const plainBuf = await subtle.decrypt({ name: "AES-GCM", iv }, this._aesKey, ct);
178
+ const newIv = getRandomBytes(12);
179
+ const newCt = await subtle.encrypt({ name: "AES-GCM", iv: newIv }, newAesKey, plainBuf);
180
+ next._entries.set(entry.name, {
181
+ name: entry.name,
182
+ iv: b64uEncode(newIv),
183
+ ct: b64uEncode(new Uint8Array(newCt)),
184
+ dataType: entry.dataType
185
+ });
186
+ }
187
+ return next;
188
+ }
189
+ };
190
+ _SPDWeb.VERSION = 1;
191
+ _SPDWeb.PBKDF2_ITER_DEFAULT = 6e5;
192
+ var SPDWeb = _SPDWeb;
193
+ export {
194
+ SPDWeb
195
+ };