@alikhalilll/nuxt-crypto 1.0.3 → 1.2.1

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 CHANGED
@@ -7,6 +7,7 @@ Symmetric encryption for Nuxt 3 / 4, built on the native **Web Crypto API**. Def
7
7
  - **Pluggable algorithms** — swap the default AES-GCM implementation for your own `CryptoAlgorithm` without touching the payload envelope.
8
8
  - **Versioned payload format** — `v1.{salt}.{iv}.{cipher}` with clean forward compatibility.
9
9
  - **Server-only mode** — opt into registering the plugin only on the server so the passphrase never ships to the browser bundle.
10
+ - **Device fingerprint** _(new)_ — bind a payload to the browser that created it via an HttpOnly cookie. Survives IP changes (Wi-Fi ↔ 4G, VPN rotation) while still blocking copy-paste to another browser or device.
10
11
  - **Strong types** — `CryptoService`, `CryptoServiceConfig`, and `CryptoAlgorithm` all exported.
11
12
 
12
13
  ---
@@ -16,21 +17,22 @@ Symmetric encryption for Nuxt 3 / 4, built on the native **Web Crypto API**. Def
16
17
  1. [Install](#install)
17
18
  2. [Register the module](#register-the-module)
18
19
  3. [Usage](#usage)
19
- - [Basic round-trip](#basic-round-trip)
20
- - [Encrypt / decrypt a JSON object](#encrypt--decrypt-a-json-object)
21
- - [Encrypt / decrypt an arbitrary value](#encrypt--decrypt-an-arbitrary-value)
22
- - [Persist a ciphertext in a cookie](#persist-a-ciphertext-in-a-cookie)
23
- - [Bulk encrypt / decrypt (key cache benefits)](#bulk-encrypt--decrypt-key-cache-benefits)
24
- - [Clear the key cache](#clear-the-key-cache)
25
- - [Error handling](#error-handling)
20
+ - [Basic round-trip](#basic-round-trip)
21
+ - [Encrypt / decrypt a JSON object](#encrypt--decrypt-a-json-object)
22
+ - [Encrypt / decrypt an arbitrary value](#encrypt--decrypt-an-arbitrary-value)
23
+ - [Persist a ciphertext in a cookie](#persist-a-ciphertext-in-a-cookie)
24
+ - [Bulk encrypt / decrypt (key cache benefits)](#bulk-encrypt--decrypt-key-cache-benefits)
25
+ - [Clear the key cache](#clear-the-key-cache)
26
+ - [Error handling](#error-handling)
26
27
  4. [Server-only mode](#server-only-mode)
27
28
  5. [Nitro / API routes](#nitro--api-routes)
28
- 6. [Framework-agnostic core](#framework-agnostic-core)
29
- 7. [Custom algorithm](#custom-algorithm)
30
- 8. [Rotating the passphrase (re-encryption)](#rotating-the-passphrase-re-encryption)
31
- 9. [Payload format](#payload-format)
32
- 10. [Module options reference](#module-options-reference)
33
- 11. [Exported types](#exported-types)
29
+ 6. [Device fingerprint](#device-fingerprint)
30
+ 7. [Framework-agnostic core](#framework-agnostic-core)
31
+ 8. [Custom algorithm](#custom-algorithm)
32
+ 9. [Rotating the passphrase (re-encryption)](#rotating-the-passphrase-re-encryption)
33
+ 10. [Payload format](#payload-format)
34
+ 11. [Module options reference](#module-options-reference)
35
+ 12. [Exported types](#exported-types)
34
36
 
35
37
  ---
36
38
 
@@ -49,7 +51,7 @@ export default defineNuxtConfig({
49
51
  crypto: {
50
52
  // Always wire this from an env var. Never commit the passphrase.
51
53
  passphrase: process.env.NUXT_ENCRYPTION_PASSPHRASE ?? '',
52
- provideName: '$crypto', // leading "$" optional
54
+ provideName: '$crypto', // leading "$" optional
53
55
  iterations: 100_000,
54
56
  keyCacheSize: 64,
55
57
  serverOnly: false,
@@ -148,6 +150,7 @@ $crypto.clearKeyCache();
148
150
  ```
149
151
 
150
152
  Useful when:
153
+
151
154
  - You rotate the passphrase (the cache would still hold old keys).
152
155
  - You want to force re-derivation in a security-audit test.
153
156
 
@@ -164,13 +167,13 @@ try {
164
167
 
165
168
  Errors you can see:
166
169
 
167
- | Scenario | Message |
168
- |-----------------------------------------------|---------|
169
- | Payload isn't `a.b.c.d` | `Invalid payload format — expected 4 dot-separated segments.` |
170
- | A segment is empty | `Invalid payload format — one or more segments were empty.` |
171
- | Algorithm version mismatch | `Unsupported payload version: v2 (algorithm expects v1).` |
172
- | Wrong passphrase / tampered ciphertext | Native `OperationError` from Web Crypto. |
173
- | Passphrase not set in module config | `[nuxt-crypto] passphrase is required.` |
170
+ | Scenario | Message |
171
+ | -------------------------------------- | ------------------------------------------------------------- |
172
+ | Payload isn't `a.b.c.d` | `Invalid payload format — expected 4 dot-separated segments.` |
173
+ | A segment is empty | `Invalid payload format — one or more segments were empty.` |
174
+ | Algorithm version mismatch | `Unsupported payload version: v2 (algorithm expects v1).` |
175
+ | Wrong passphrase / tampered ciphertext | Native `OperationError` from Web Crypto. |
176
+ | Passphrase not set in module config | `[nuxt-crypto] passphrase is required.` |
174
177
 
175
178
  ## Server-only mode
176
179
 
@@ -184,30 +187,120 @@ crypto: {
184
187
  ```
185
188
 
186
189
  With this enabled, `$crypto` is `undefined` on the client. Use it only in:
190
+
187
191
  - Nitro server routes (`server/api/*.ts`)
188
192
  - Server-only plugins
189
193
  - `<script setup>` blocks guarded by `import.meta.server`
190
194
 
191
195
  ## Nitro / API routes
192
196
 
197
+ Nitro event handlers run outside the Nuxt app context, so `useNuxtApp()` — and therefore `$crypto` — isn't available there. Use the framework-agnostic core directly. A tiny server utility keeps the service cached across requests:
198
+
199
+ ```ts
200
+ // server/utils/crypto.ts
201
+ import { createCryptoService } from '@alikhalilll/nuxt-crypto/core';
202
+
203
+ let servicePromise: ReturnType<typeof createCryptoService> | null = null;
204
+
205
+ export function useServerCrypto() {
206
+ if (!servicePromise) {
207
+ servicePromise = createCryptoService({
208
+ passphrase: process.env.NUXT_ENCRYPTION_PASSPHRASE!,
209
+ iterations: 100_000,
210
+ });
211
+ }
212
+ return servicePromise;
213
+ }
214
+ ```
215
+
193
216
  Encrypt data before sending it to the browser, decrypt it when it comes back:
194
217
 
195
218
  ```ts
196
219
  // server/api/session/encode.post.ts
197
220
  export default defineEventHandler(async (event) => {
198
221
  const body = await readBody(event);
199
- const { $crypto } = useNuxtApp();
200
- return { token: await $crypto.encrypt(JSON.stringify(body)) };
222
+ const crypto = await useServerCrypto();
223
+ return { token: await crypto.encrypt(JSON.stringify(body)) };
201
224
  });
202
225
 
203
226
  // server/api/session/decode.post.ts
204
227
  export default defineEventHandler(async (event) => {
205
228
  const { token } = await readBody(event);
206
- const { $crypto } = useNuxtApp();
207
- return JSON.parse(await $crypto.decrypt(token));
229
+ const crypto = await useServerCrypto();
230
+ return JSON.parse(await crypto.decrypt(token));
208
231
  });
209
232
  ```
210
233
 
234
+ ## Device fingerprint
235
+
236
+ Bind a payload to the browser that created it, so a copy of the ciphertext in another browser or on another device won't decrypt. Useful for short-lived CSRF tokens, magic links, and anti-replay nonces.
237
+
238
+ The fingerprint is built from an **HttpOnly device-ID cookie** — not the client IP — so it survives Wi-Fi → 4G switches, cell handoffs, VPN rotations, and residential IP rotation while still blocking copy-paste to another origin.
239
+
240
+ ### Setup
241
+
242
+ ```ts
243
+ // nuxt.config.ts
244
+ export default defineNuxtConfig({
245
+ runtimeConfig: {
246
+ // Server-side pepper. Keep this secret; never expose to the client.
247
+ cryptoFingerprintSalt: process.env.NUXT_CRYPTO_FINGERPRINT_SALT ?? '',
248
+ },
249
+ });
250
+ ```
251
+
252
+ ```
253
+ # .env
254
+ NUXT_CRYPTO_FINGERPRINT_SALT=<64 random hex chars>
255
+ ```
256
+
257
+ ### Encrypt / decrypt with a fingerprint
258
+
259
+ ```ts
260
+ // server/api/session.post.ts
261
+ import { getClientFingerprint } from '@alikhalilll/nuxt-crypto/server';
262
+
263
+ export default defineEventHandler(async (event) => {
264
+ const body = await readBody(event);
265
+ const crypto = await useServerCrypto(); // see "Nitro / API routes" above
266
+
267
+ const fingerprint = await getClientFingerprint(event, {
268
+ salt: useRuntimeConfig().cryptoFingerprintSalt,
269
+ });
270
+
271
+ return {
272
+ token: await crypto.encrypt(JSON.stringify(body), { fingerprint }),
273
+ };
274
+ });
275
+ ```
276
+
277
+ The first call sets an HttpOnly cookie (`__nuxt_crypto_device`) with a random 32-byte ID; subsequent calls reuse it. Pass the same `{ fingerprint }` to `crypto.decrypt` on the server to round-trip.
278
+
279
+ ### What this protects against
280
+
281
+ | Scenario | Decrypts? |
282
+ | ----------------------------------------- | ------------------------------------ |
283
+ | Wi-Fi → 4G on the same device | ✅ yes — cookie travels with browser |
284
+ | VPN exit node or ISP IP rotation | ✅ yes |
285
+ | Copy token to another browser on same box | ❌ no — no cookie in that browser |
286
+ | XSS-exfiltrated token to an attacker | ❌ no — HttpOnly cookie unreachable |
287
+ | User clears cookies | ❌ no — permanently unrecoverable |
288
+
289
+ ### `deriveFingerprint` — bring your own device ID
290
+
291
+ If you already manage per-device identity (session table, signed JWT, etc.), skip the cookie helper:
292
+
293
+ ```ts
294
+ import { deriveFingerprint } from '@alikhalilll/nuxt-crypto/server';
295
+
296
+ const fingerprint = await deriveFingerprint({
297
+ deviceId: session.id,
298
+ salt: useRuntimeConfig().cryptoFingerprintSalt,
299
+ });
300
+ ```
301
+
302
+ > **Warning** — a fingerprinted payload becomes undecryptable if the device cookie is cleared or the session rotates. Use only for tokens the user can afford to lose (short sessions, magic links, one-shot nonces), never for long-lived data.
303
+
211
304
  ## Framework-agnostic core
212
305
 
213
306
  Everything the Nuxt plugin wraps is available as a plain factory:
@@ -238,9 +331,15 @@ import type { CryptoAlgorithm } from '@alikhalilll/nuxt-crypto/types';
238
331
 
239
332
  const myAlgo: CryptoAlgorithm = {
240
333
  version: 'v2',
241
- async deriveKey({ subtle, passphrase, salt, iterations }) { /* ... */ },
242
- async encrypt({ subtle, key, plainText }) { /* ... */ },
243
- async decrypt({ subtle, key, cipher, iv }) { /* ... */ },
334
+ async deriveKey({ subtle, passphrase, salt, iterations }) {
335
+ /* ... */
336
+ },
337
+ async encrypt({ subtle, key, plainText }) {
338
+ /* ... */
339
+ },
340
+ async decrypt({ subtle, key, cipher, iv }) {
341
+ /* ... */
342
+ },
244
343
  };
245
344
 
246
345
  const service = await createCryptoService({
@@ -271,22 +370,22 @@ Run `rotate` over your stored ciphertexts during a background migration. Clear t
271
370
 
272
371
  `v1.{saltB64}.{ivB64}.{cipherB64}` — four dot-separated segments, each standard base64:
273
372
 
274
- | Segment | Bytes | Notes |
275
- |---------|-------|-------|
276
- | `v1` | — | Algorithm / version tag. |
277
- | salt | 16 | Per-encryption PBKDF2 salt. |
278
- | iv | 12 | AES-GCM initialization vector. |
373
+ | Segment | Bytes | Notes |
374
+ | ------- | ----- | ---------------------------------- |
375
+ | `v1` | — | Algorithm / version tag. |
376
+ | salt | 16 | Per-encryption PBKDF2 salt. |
377
+ | iv | 12 | AES-GCM initialization vector. |
279
378
  | cipher | N | Ciphertext + 16-byte GCM auth tag. |
280
379
 
281
380
  ## Module options reference
282
381
 
283
- | Option | Type | Default | Purpose |
284
- |----------------|----------|------------|----------------------------------------------------------------------|
285
- | `passphrase` | `string` | `''` | Passphrase to derive the AES key from. Throws at use if empty. |
286
- | `provideName` | `string` | `'$crypto'`| Injected under `$<name>`. Leading `$` is stripped. |
287
- | `iterations` | `number` | `100_000` | PBKDF2 iteration count. |
288
- | `keyCacheSize` | `number` | `64` | Max derived keys kept in memory. Set to 0 to disable caching. |
289
- | `serverOnly` | `boolean`| `false` | When true, plugin runs only on the server. |
382
+ | Option | Type | Default | Purpose |
383
+ | -------------- | --------- | ----------- | -------------------------------------------------------------- |
384
+ | `passphrase` | `string` | `''` | Passphrase to derive the AES key from. Throws at use if empty. |
385
+ | `provideName` | `string` | `'$crypto'` | Injected under `$<name>`. Leading `$` is stripped. |
386
+ | `iterations` | `number` | `100_000` | PBKDF2 iteration count. |
387
+ | `keyCacheSize` | `number` | `64` | Max derived keys kept in memory. Set to 0 to disable caching. |
388
+ | `serverOnly` | `boolean` | `false` | When true, plugin runs only on the server. |
290
389
 
291
390
  ## Exported types
292
391
 
@@ -294,11 +393,16 @@ Run `rotate` over your stored ciphertexts during a background migration. Clear t
294
393
  import type {
295
394
  CryptoService,
296
395
  CryptoServiceConfig,
396
+ CryptoOperationOptions,
297
397
  CryptoAlgorithm,
298
398
  CryptoModuleOptions,
299
399
  Bytes,
300
400
  ParsedPayload,
301
401
  } from '@alikhalilll/nuxt-crypto/types';
402
+
403
+ // Server subpath (Nitro / H3 only)
404
+ import type { ClientFingerprintOptions, FingerprintParts } from '@alikhalilll/nuxt-crypto/server';
405
+ import { getClientFingerprint, deriveFingerprint } from '@alikhalilll/nuxt-crypto/server';
302
406
  ```
303
407
 
304
408
  ## License
@@ -1,10 +1,18 @@
1
+ import {
2
+ fromBase64,
3
+ getRandomBytes,
4
+ getSubtle,
5
+ toBase64
6
+ } from "./chunk-FKXZ7VYW.mjs";
7
+
1
8
  // src/core/algorithms/aes-gcm.ts
2
9
  var aesGcmPbkdf2 = {
3
10
  version: "v1",
4
- async deriveKey({ subtle, passphrase, salt, iterations }) {
11
+ async deriveKey({ subtle, passphrase, fingerprint, salt, iterations }) {
12
+ const material = fingerprint ? `${passphrase}\0${fingerprint}` : passphrase;
5
13
  const keyMaterial = await subtle.importKey(
6
14
  "raw",
7
- new TextEncoder().encode(passphrase),
15
+ new TextEncoder().encode(material),
8
16
  { name: "PBKDF2" },
9
17
  false,
10
18
  ["deriveKey"]
@@ -33,31 +41,6 @@ var aesGcmPbkdf2 = {
33
41
  }
34
42
  };
35
43
 
36
- // src/core/base64.ts
37
- function toBase64(data) {
38
- if (typeof btoa === "function") {
39
- let binary = "";
40
- const CHUNK = 32768;
41
- for (let i = 0; i < data.length; i += CHUNK) {
42
- binary += String.fromCharCode(...data.subarray(i, i + CHUNK));
43
- }
44
- return btoa(binary);
45
- }
46
- return Buffer.from(data).toString("base64");
47
- }
48
- function fromBase64(input) {
49
- if (typeof atob === "function") {
50
- const binary = atob(input);
51
- const out2 = new Uint8Array(binary.length);
52
- for (let i = 0; i < binary.length; i++) out2[i] = binary.charCodeAt(i);
53
- return out2;
54
- }
55
- const buf = Buffer.from(input, "base64");
56
- const out = new Uint8Array(buf.length);
57
- out.set(buf);
58
- return out;
59
- }
60
-
61
44
  // src/core/key-cache.ts
62
45
  var KeyCache = class {
63
46
  constructor(capacity) {
@@ -65,8 +48,8 @@ var KeyCache = class {
65
48
  }
66
49
  capacity;
67
50
  map = /* @__PURE__ */ new Map();
68
- static key(salt, iterations) {
69
- return `${iterations}.${toBase64(salt)}`;
51
+ static key(salt, iterations, fingerprint) {
52
+ return `${iterations}.${fingerprint ?? ""}.${toBase64(salt)}`;
70
53
  }
71
54
  get(key) {
72
55
  const value = this.map.get(key);
@@ -112,24 +95,6 @@ function parsePayload(payload) {
112
95
  };
113
96
  }
114
97
 
115
- // src/core/subtle.ts
116
- async function getSubtle() {
117
- if (globalThis.crypto?.subtle) return globalThis.crypto.subtle;
118
- const { webcrypto } = await import("crypto");
119
- return webcrypto.subtle;
120
- }
121
- function getRandomBytes(length) {
122
- const out = new Uint8Array(length);
123
- const c = globalThis.crypto;
124
- if (c?.getRandomValues) {
125
- c.getRandomValues(out);
126
- return out;
127
- }
128
- throw new Error(
129
- "[nuxt-crypto] globalThis.crypto.getRandomValues is not available. Upgrade your runtime."
130
- );
131
- }
132
-
133
98
  // src/core/create-service.ts
134
99
  var DEFAULT_ITERATIONS = 1e5;
135
100
  var DEFAULT_CACHE = 64;
@@ -145,32 +110,32 @@ async function createCryptoService(config) {
145
110
  const passphrase = String(config.passphrase).trim();
146
111
  const encoder = new TextEncoder();
147
112
  const decoder = new TextDecoder();
148
- async function getDerivedKey(salt) {
149
- const key = KeyCache.key(salt, iterations);
113
+ async function getDerivedKey(salt, fingerprint) {
114
+ const key = KeyCache.key(salt, iterations, fingerprint);
150
115
  const cached = cache.get(key);
151
116
  if (cached) return cached;
152
- const pending = algorithm.deriveKey({ subtle, passphrase, salt, iterations });
117
+ const pending = algorithm.deriveKey({ subtle, passphrase, fingerprint, salt, iterations });
153
118
  cache.set(key, pending);
154
119
  return pending;
155
120
  }
156
121
  return {
157
- async encrypt(plainText) {
122
+ async encrypt(plainText, options) {
158
123
  const salt = getRandomBytes(SALT_BYTES);
159
- const key = await getDerivedKey(salt);
124
+ const key = await getDerivedKey(salt, options?.fingerprint);
160
125
  const encoded = encoder.encode(plainText);
161
126
  const plainBytes = new Uint8Array(new ArrayBuffer(encoded.byteLength));
162
127
  plainBytes.set(encoded);
163
128
  const { cipher, iv } = await algorithm.encrypt({ subtle, key, plainText: plainBytes });
164
129
  return encodePayload({ version: algorithm.version, salt, iv, cipher });
165
130
  },
166
- async decrypt(payload) {
131
+ async decrypt(payload, options) {
167
132
  const parsed = parsePayload(payload);
168
133
  if (parsed.version !== algorithm.version) {
169
134
  throw new Error(
170
135
  `[nuxt-crypto] Unsupported payload version: ${parsed.version} (algorithm expects ${algorithm.version}).`
171
136
  );
172
137
  }
173
- const key = await getDerivedKey(parsed.salt);
138
+ const key = await getDerivedKey(parsed.salt, options?.fingerprint);
174
139
  const plainBytes = await algorithm.decrypt({
175
140
  subtle,
176
141
  key,
@@ -187,13 +152,9 @@ async function createCryptoService(config) {
187
152
 
188
153
  export {
189
154
  aesGcmPbkdf2,
190
- toBase64,
191
- fromBase64,
192
155
  KeyCache,
193
156
  encodePayload,
194
157
  parsePayload,
195
- getSubtle,
196
- getRandomBytes,
197
158
  createCryptoService
198
159
  };
199
- //# sourceMappingURL=chunk-HNTEUBQ7.mjs.map
160
+ //# sourceMappingURL=chunk-FKHKOTFR.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/algorithms/aes-gcm.ts","../src/core/key-cache.ts","../src/core/payload.ts","../src/core/create-service.ts"],"sourcesContent":["import type { CryptoAlgorithm } from '../types';\n\n/**\n * AES-256-GCM with PBKDF2-SHA256 key derivation. This is the default and\n * currently the only built-in algorithm; pluggable via `CryptoServiceConfig.algorithm`.\n */\nexport const aesGcmPbkdf2: CryptoAlgorithm = {\n version: 'v1',\n\n async deriveKey({ subtle, passphrase, fingerprint, salt, iterations }) {\n // NUL separator prevents `\"ab\" + \"cd\"` and `\"abc\" + \"d\"` from colliding.\n const material = fingerprint ? `${passphrase}\\x00${fingerprint}` : passphrase;\n const keyMaterial = await subtle.importKey(\n 'raw',\n new TextEncoder().encode(material),\n { name: 'PBKDF2' },\n false,\n ['deriveKey']\n );\n\n return subtle.deriveKey(\n { name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },\n keyMaterial,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt']\n );\n },\n\n async encrypt({ subtle, key, plainText }) {\n const iv = new Uint8Array(new ArrayBuffer(12));\n globalThis.crypto.getRandomValues(iv);\n const buf = await subtle.encrypt({ name: 'AES-GCM', iv }, key, plainText);\n const cipher = new Uint8Array(new ArrayBuffer(buf.byteLength));\n cipher.set(new Uint8Array(buf));\n return { cipher, iv };\n },\n\n async decrypt({ subtle, key, cipher, iv }) {\n const buf = await subtle.decrypt({ name: 'AES-GCM', iv }, key, cipher);\n const out = new Uint8Array(new ArrayBuffer(buf.byteLength));\n out.set(new Uint8Array(buf));\n return out;\n },\n};\n","import type { Bytes } from './types';\nimport { toBase64 } from './base64';\n\n/**\n * A tiny LRU-ish cache for derived `CryptoKey` values, keyed on the salt.\n * When capacity is exceeded the oldest entry is evicted. Derived key\n * material is expensive (PBKDF2 is designed to be slow), so caching has\n * a big impact when decrypting many payloads in a row.\n */\nexport class KeyCache {\n private readonly map = new Map<string, Promise<CryptoKey>>();\n constructor(private readonly capacity: number) {}\n\n static key(salt: Bytes, iterations: number, fingerprint?: string): string {\n // `.` separates the fields; iterations and base64 can't contain one,\n // so the joined key is unambiguous even when fingerprint is empty.\n return `${iterations}.${fingerprint ?? ''}.${toBase64(salt)}`;\n }\n\n get(key: string): Promise<CryptoKey> | undefined {\n const value = this.map.get(key);\n if (!value) return undefined;\n // Refresh recency by re-inserting.\n this.map.delete(key);\n this.map.set(key, value);\n return value;\n }\n\n set(key: string, value: Promise<CryptoKey>): void {\n if (this.capacity <= 0) return;\n this.map.set(key, value);\n while (this.map.size > this.capacity) {\n const oldest = this.map.keys().next().value;\n if (oldest === undefined) break;\n this.map.delete(oldest);\n }\n }\n\n clear(): void {\n this.map.clear();\n }\n}\n","import { fromBase64, toBase64 } from './base64';\nimport type { Bytes } from './types';\n\n/**\n * Payload serialisation format (v1): `${version}.${saltB64}.${ivB64}.${cipherB64}`.\n *\n * Each part is URL-safe base64, all joined with `.`. The version tag is\n * always the first segment so consumers can route to the correct algorithm\n * during decrypt.\n */\nexport interface ParsedPayload {\n version: string;\n salt: Bytes;\n iv: Bytes;\n cipher: Bytes;\n}\n\nexport function encodePayload(parts: ParsedPayload): string {\n return [parts.version, toBase64(parts.salt), toBase64(parts.iv), toBase64(parts.cipher)].join(\n '.'\n );\n}\n\nexport function parsePayload(payload: string): ParsedPayload {\n const segments = payload.split('.');\n if (segments.length !== 4) {\n throw new Error('[nuxt-crypto] Invalid payload format — expected 4 dot-separated segments.');\n }\n const [version, saltB64, ivB64, cipherB64] = segments;\n if (!version || !saltB64 || !ivB64 || !cipherB64) {\n throw new Error('[nuxt-crypto] Invalid payload format — one or more segments were empty.');\n }\n return {\n version,\n salt: fromBase64(saltB64),\n iv: fromBase64(ivB64),\n cipher: fromBase64(cipherB64),\n };\n}\n","import { aesGcmPbkdf2 } from './algorithms/aes-gcm';\nimport { KeyCache } from './key-cache';\nimport { encodePayload, parsePayload } from './payload';\nimport { getRandomBytes, getSubtle } from './subtle';\nimport type { CryptoAlgorithm, CryptoService, CryptoServiceConfig } from './types';\n\nconst DEFAULT_ITERATIONS = 100_000;\nconst DEFAULT_CACHE = 64;\nconst SALT_BYTES = 16;\n\n/**\n * Create a framework-agnostic `CryptoService` from a passphrase.\n *\n * Uses AES-256-GCM with PBKDF2-SHA256 (100k iterations by default). Pass a\n * different `algorithm` to plug in another cipher while preserving the same\n * versioned payload envelope.\n */\nexport async function createCryptoService(config: CryptoServiceConfig): Promise<CryptoService> {\n if (!config.passphrase) {\n throw new Error('[nuxt-crypto] passphrase is required.');\n }\n\n const iterations = config.iterations ?? DEFAULT_ITERATIONS;\n const algorithm: CryptoAlgorithm = config.algorithm ?? aesGcmPbkdf2;\n const subtle = config.subtle ?? (await getSubtle());\n const cache = new KeyCache(config.keyCacheSize ?? DEFAULT_CACHE);\n const passphrase = String(config.passphrase).trim();\n const encoder = new TextEncoder();\n const decoder = new TextDecoder();\n\n async function getDerivedKey(\n salt: Uint8Array<ArrayBuffer>,\n fingerprint?: string\n ): Promise<CryptoKey> {\n const key = KeyCache.key(salt, iterations, fingerprint);\n const cached = cache.get(key);\n if (cached) return cached;\n const pending = algorithm.deriveKey({ subtle, passphrase, fingerprint, salt, iterations });\n cache.set(key, pending);\n return pending;\n }\n\n return {\n async encrypt(plainText, options) {\n const salt = getRandomBytes(SALT_BYTES);\n const key = await getDerivedKey(salt, options?.fingerprint);\n const encoded = encoder.encode(plainText);\n const plainBytes = new Uint8Array(new ArrayBuffer(encoded.byteLength));\n plainBytes.set(encoded);\n const { cipher, iv } = await algorithm.encrypt({ subtle, key, plainText: plainBytes });\n return encodePayload({ version: algorithm.version, salt, iv, cipher });\n },\n\n async decrypt(payload, options) {\n const parsed = parsePayload(payload);\n if (parsed.version !== algorithm.version) {\n throw new Error(\n `[nuxt-crypto] Unsupported payload version: ${parsed.version} (algorithm expects ${algorithm.version}).`\n );\n }\n const key = await getDerivedKey(parsed.salt, options?.fingerprint);\n const plainBytes = await algorithm.decrypt({\n subtle,\n key,\n cipher: parsed.cipher,\n iv: parsed.iv,\n });\n return decoder.decode(plainBytes);\n },\n\n clearKeyCache(): void {\n cache.clear();\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,50 @@
1
+ // src/core/base64.ts
2
+ function toBase64(data) {
3
+ if (typeof btoa === "function") {
4
+ let binary = "";
5
+ const CHUNK = 32768;
6
+ for (let i = 0; i < data.length; i += CHUNK) {
7
+ binary += String.fromCharCode(...data.subarray(i, i + CHUNK));
8
+ }
9
+ return btoa(binary);
10
+ }
11
+ return Buffer.from(data).toString("base64");
12
+ }
13
+ function fromBase64(input) {
14
+ if (typeof atob === "function") {
15
+ const binary = atob(input);
16
+ const out2 = new Uint8Array(binary.length);
17
+ for (let i = 0; i < binary.length; i++) out2[i] = binary.charCodeAt(i);
18
+ return out2;
19
+ }
20
+ const buf = Buffer.from(input, "base64");
21
+ const out = new Uint8Array(buf.length);
22
+ out.set(buf);
23
+ return out;
24
+ }
25
+
26
+ // src/core/subtle.ts
27
+ async function getSubtle() {
28
+ if (globalThis.crypto?.subtle) return globalThis.crypto.subtle;
29
+ const { webcrypto } = await import("crypto");
30
+ return webcrypto.subtle;
31
+ }
32
+ function getRandomBytes(length) {
33
+ const out = new Uint8Array(length);
34
+ const c = globalThis.crypto;
35
+ if (c?.getRandomValues) {
36
+ c.getRandomValues(out);
37
+ return out;
38
+ }
39
+ throw new Error(
40
+ "[nuxt-crypto] globalThis.crypto.getRandomValues is not available. Upgrade your runtime."
41
+ );
42
+ }
43
+
44
+ export {
45
+ toBase64,
46
+ fromBase64,
47
+ getSubtle,
48
+ getRandomBytes
49
+ };
50
+ //# sourceMappingURL=chunk-FKXZ7VYW.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/base64.ts","../src/core/subtle.ts"],"sourcesContent":["import type { Bytes } from './types';\n\n/** Encode bytes to standard base64. Works in browser and Node. */\nexport function toBase64(data: Bytes): string {\n if (typeof btoa === 'function') {\n let binary = '';\n const CHUNK = 0x8000;\n for (let i = 0; i < data.length; i += CHUNK) {\n binary += String.fromCharCode(...data.subarray(i, i + CHUNK));\n }\n return btoa(binary);\n }\n // Node fallback — Buffer is globally available in Node.\n return Buffer.from(data).toString('base64');\n}\n\n/** Decode a base64 string back to bytes. Works in browser and Node. */\nexport function fromBase64(input: string): Bytes {\n if (typeof atob === 'function') {\n const binary = atob(input);\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n }\n const buf = Buffer.from(input, 'base64');\n const out = new Uint8Array(buf.length);\n out.set(buf);\n return out;\n}\n","/**\n * Resolve a SubtleCrypto implementation across environments:\n *\n * - Browser / Deno / Bun / Node 20+: `globalThis.crypto.subtle` is available.\n * - Older Node: fall back to `node:crypto`'s `webcrypto`.\n */\nexport async function getSubtle(): Promise<SubtleCrypto> {\n if (globalThis.crypto?.subtle) return globalThis.crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n\n/** Fill the given buffer with cryptographically strong random bytes. */\nexport function getRandomBytes(length: number): Uint8Array<ArrayBuffer> {\n const out = new Uint8Array(length);\n const c = globalThis.crypto;\n if (c?.getRandomValues) {\n c.getRandomValues(out);\n return out;\n }\n throw new Error(\n '[nuxt-crypto] globalThis.crypto.getRandomValues is not available. Upgrade your runtime.'\n );\n}\n"],"mappings":";AAGO,SAAS,SAAS,MAAqB;AAC5C,MAAI,OAAO,SAAS,YAAY;AAC9B,QAAI,SAAS;AACb,UAAM,QAAQ;AACd,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,OAAO;AAC3C,gBAAU,OAAO,aAAa,GAAG,KAAK,SAAS,GAAG,IAAI,KAAK,CAAC;AAAA,IAC9D;AACA,WAAO,KAAK,MAAM;AAAA,EACpB;AAEA,SAAO,OAAO,KAAK,IAAI,EAAE,SAAS,QAAQ;AAC5C;AAGO,SAAS,WAAW,OAAsB;AAC/C,MAAI,OAAO,SAAS,YAAY;AAC9B,UAAM,SAAS,KAAK,KAAK;AACzB,UAAMA,OAAM,IAAI,WAAW,OAAO,MAAM;AACxC,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,IAAK,CAAAA,KAAI,CAAC,IAAI,OAAO,WAAW,CAAC;AACpE,WAAOA;AAAA,EACT;AACA,QAAM,MAAM,OAAO,KAAK,OAAO,QAAQ;AACvC,QAAM,MAAM,IAAI,WAAW,IAAI,MAAM;AACrC,MAAI,IAAI,GAAG;AACX,SAAO;AACT;;;ACtBA,eAAsB,YAAmC;AACvD,MAAI,WAAW,QAAQ,OAAQ,QAAO,WAAW,OAAO;AACxD,QAAM,EAAE,UAAU,IAAI,MAAM,OAAO,QAAa;AAChD,SAAO,UAAU;AACnB;AAGO,SAAS,eAAe,QAAyC;AACtE,QAAM,MAAM,IAAI,WAAW,MAAM;AACjC,QAAM,IAAI,WAAW;AACrB,MAAI,GAAG,iBAAiB;AACtB,MAAE,gBAAgB,GAAG;AACrB,WAAO;AAAA,EACT;AACA,QAAM,IAAI;AAAA,IACR;AAAA,EACF;AACF;","names":["out"]}
@@ -1,5 +1,5 @@
1
- import { b as CryptoServiceConfig, a as CryptoService, C as CryptoAlgorithm, B as Bytes } from '../payload-Cht5Ar0K.js';
2
- export { P as ParsedPayload, e as encodePayload, p as parsePayload } from '../payload-Cht5Ar0K.js';
1
+ import { c as CryptoServiceConfig, b as CryptoService, C as CryptoAlgorithm, B as Bytes } from '../payload-BOueSWcI.js';
2
+ export { a as CryptoOperationOptions, P as ParsedPayload, e as encodePayload, p as parsePayload } from '../payload-BOueSWcI.js';
3
3
 
4
4
  /**
5
5
  * Create a framework-agnostic `CryptoService` from a passphrase.
@@ -41,7 +41,7 @@ declare class KeyCache {
41
41
  private readonly capacity;
42
42
  private readonly map;
43
43
  constructor(capacity: number);
44
- static key(salt: Bytes, iterations: number): string;
44
+ static key(salt: Bytes, iterations: number, fingerprint?: string): string;
45
45
  get(key: string): Promise<CryptoKey> | undefined;
46
46
  set(key: string, value: Promise<CryptoKey>): void;
47
47
  clear(): void;
@@ -3,12 +3,14 @@ import {
3
3
  aesGcmPbkdf2,
4
4
  createCryptoService,
5
5
  encodePayload,
6
+ parsePayload
7
+ } from "../chunk-FKHKOTFR.mjs";
8
+ import {
6
9
  fromBase64,
7
10
  getRandomBytes,
8
11
  getSubtle,
9
- parsePayload,
10
12
  toBase64
11
- } from "../chunk-HNTEUBQ7.mjs";
13
+ } from "../chunk-FKXZ7VYW.mjs";
12
14
  export {
13
15
  KeyCache,
14
16
  aesGcmPbkdf2,
package/dist/module.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { NuxtModule } from '@nuxt/schema';
2
2
  import { CryptoModuleOptions } from './types/index.js';
3
- export { B as Bytes, C as CryptoAlgorithm, a as CryptoService, b as CryptoServiceConfig, P as ParsedPayload } from './payload-Cht5Ar0K.js';
3
+ export { B as Bytes, C as CryptoAlgorithm, a as CryptoOperationOptions, b as CryptoService, c as CryptoServiceConfig, P as ParsedPayload } from './payload-BOueSWcI.js';
4
4
 
5
5
  declare const cryptoModule: NuxtModule<CryptoModuleOptions>;
6
6
 
@@ -1,5 +1,23 @@
1
1
  /** Shorthand for the `Uint8Array` we use everywhere in the crypto package. */
2
2
  type Bytes = Uint8Array<ArrayBuffer>;
3
+ /**
4
+ * Options accepted by `encrypt` / `decrypt`. Both calls must receive the
5
+ * same `fingerprint` value to round-trip; passing different fingerprints
6
+ * produces undecryptable ciphertext.
7
+ */
8
+ interface CryptoOperationOptions {
9
+ /**
10
+ * Optional extra key material mixed into the PBKDF2 input. Typically a
11
+ * device fingerprint produced server-side from an HttpOnly device-ID
12
+ * cookie via `getClientFingerprint()` from `@alikhalilll/nuxt-crypto/server`.
13
+ *
14
+ * ⚠️ Binding ciphertext to a fingerprint means the payload becomes
15
+ * undecryptable the moment the device cookie is cleared. Use only for
16
+ * ephemeral tokens (short-lived session cookies, one-time links), never
17
+ * for long-lived user data.
18
+ */
19
+ fingerprint?: string;
20
+ }
3
21
  /**
4
22
  * A CryptoService encrypts plaintext to a serialisable string payload and
5
23
  * decrypts it back. Every operation is async because it hits the Web Crypto
@@ -7,9 +25,9 @@ type Bytes = Uint8Array<ArrayBuffer>;
7
25
  */
8
26
  interface CryptoService {
9
27
  /** Encrypt a UTF-8 string to a versioned, base64-encoded payload. */
10
- encrypt(plainText: string): Promise<string>;
11
- /** Decrypt a payload produced by `encrypt`. */
12
- decrypt(payload: string): Promise<string>;
28
+ encrypt(plainText: string, options?: CryptoOperationOptions): Promise<string>;
29
+ /** Decrypt a payload produced by `encrypt`. Pass the same `fingerprint` used at encrypt time, if any. */
30
+ decrypt(payload: string, options?: CryptoOperationOptions): Promise<string>;
13
31
  /** Clear any cached derived keys. Safe to call at any time. */
14
32
  clearKeyCache(): void;
15
33
  }
@@ -41,10 +59,15 @@ interface CryptoServiceConfig {
41
59
  interface CryptoAlgorithm {
42
60
  /** Short identifier stored as the first segment of every payload (e.g. `'v1'`). */
43
61
  readonly version: string;
44
- /** Derive a CryptoKey from a passphrase + salt pair. Expected to be idempotent. */
62
+ /**
63
+ * Derive a CryptoKey from a passphrase + salt pair. Expected to be idempotent.
64
+ * When `fingerprint` is provided it is mixed into the key-derivation input
65
+ * (alongside the passphrase) so ciphertext becomes bound to that fingerprint.
66
+ */
45
67
  deriveKey(args: {
46
68
  subtle: SubtleCrypto;
47
69
  passphrase: string;
70
+ fingerprint?: string;
48
71
  salt: Bytes;
49
72
  iterations: number;
50
73
  }): Promise<CryptoKey>;
@@ -82,4 +105,4 @@ interface ParsedPayload {
82
105
  declare function encodePayload(parts: ParsedPayload): string;
83
106
  declare function parsePayload(payload: string): ParsedPayload;
84
107
 
85
- export { type Bytes as B, type CryptoAlgorithm as C, type ParsedPayload as P, type CryptoService as a, type CryptoServiceConfig as b, encodePayload as e, parsePayload as p };
108
+ export { type Bytes as B, type CryptoAlgorithm as C, type ParsedPayload as P, type CryptoOperationOptions as a, type CryptoService as b, type CryptoServiceConfig as c, encodePayload as e, parsePayload as p };
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  createCryptoService
3
- } from "../chunk-HNTEUBQ7.mjs";
3
+ } from "../chunk-FKHKOTFR.mjs";
4
+ import "../chunk-FKXZ7VYW.mjs";
4
5
 
5
6
  // src/runtime/plugin.ts
6
7
  import { defineNuxtPlugin } from "#app";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runtime/plugin.ts"],"sourcesContent":["import { defineNuxtPlugin } from '#app';\nimport config from '#build/crypto-config.mjs';\n\nimport { createCryptoService } from '../core/create-service';\n\nexport default defineNuxtPlugin(async () => {\n const { passphrase, iterations, keyCacheSize, provideName } = config;\n\n if (!passphrase && (import.meta as ImportMeta & { dev?: boolean }).dev) {\n // eslint-disable-next-line no-console\n console.warn(\n `[@alikhalilll/nuxt-crypto] Missing passphrase — $${provideName} will throw when used.`\n );\n }\n\n const service = await createCryptoService({\n passphrase: String(passphrase).trim(),\n iterations,\n keyCacheSize,\n });\n\n return {\n provide: {\n [provideName]: service,\n },\n };\n});\n"],"mappings":";;;;;AAAA,SAAS,wBAAwB;AACjC,OAAO,YAAY;AAInB,IAAO,iBAAQ,iBAAiB,YAAY;AAC1C,QAAM,EAAE,YAAY,YAAY,cAAc,YAAY,IAAI;AAE9D,MAAI,CAAC,cAAe,YAA+C,KAAK;AAEtE,YAAQ;AAAA,MACN,yDAAoD,WAAW;AAAA,IACjE;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,oBAAoB;AAAA,IACxC,YAAY,OAAO,UAAU,EAAE,KAAK;AAAA,IACpC;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL,SAAS;AAAA,MACP,CAAC,WAAW,GAAG;AAAA,IACjB;AAAA,EACF;AACF,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/runtime/plugin.ts"],"sourcesContent":["import { defineNuxtPlugin } from '#app';\nimport config from '#build/crypto-config.mjs';\n\nimport { createCryptoService } from '../core/create-service';\n\nexport default defineNuxtPlugin(async () => {\n const { passphrase, iterations, keyCacheSize, provideName } = config;\n\n if (!passphrase && (import.meta as ImportMeta & { dev?: boolean }).dev) {\n // eslint-disable-next-line no-console\n console.warn(\n `[@alikhalilll/nuxt-crypto] Missing passphrase — $${provideName} will throw when used.`\n );\n }\n\n const service = await createCryptoService({\n passphrase: String(passphrase).trim(),\n iterations,\n keyCacheSize,\n });\n\n return {\n provide: {\n [provideName]: service,\n },\n };\n});\n"],"mappings":";;;;;;AAAA,SAAS,wBAAwB;AACjC,OAAO,YAAY;AAInB,IAAO,iBAAQ,iBAAiB,YAAY;AAC1C,QAAM,EAAE,YAAY,YAAY,cAAc,YAAY,IAAI;AAE9D,MAAI,CAAC,cAAe,YAA+C,KAAK;AAEtE,YAAQ;AAAA,MACN,yDAAoD,WAAW;AAAA,IACjE;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,oBAAoB;AAAA,IACxC,YAAY,OAAO,UAAU,EAAE,KAAK;AAAA,IACpC;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL,SAAS;AAAA,MACP,CAAC,WAAW,GAAG;AAAA,IACjB;AAAA,EACF;AACF,CAAC;","names":[]}
@@ -0,0 +1,76 @@
1
+ import { setCookie, H3Event } from 'h3';
2
+
3
+ type CookieSerializeOptions = NonNullable<Parameters<typeof setCookie>[3]>;
4
+ /**
5
+ * Options for {@link getClientFingerprint}.
6
+ */
7
+ interface ClientFingerprintOptions {
8
+ /**
9
+ * Per-app pepper. Stored in a runtime-config secret (e.g.
10
+ * `NUXT_CRYPTO_FINGERPRINT_SALT`) — keeping it off the client means the
11
+ * fingerprint can't be reproduced from a leaked device cookie alone.
12
+ */
13
+ salt: string;
14
+ /** Cookie name holding the device ID. Default `__nuxt_crypto_device`. */
15
+ cookieName?: string;
16
+ /** Cookie lifetime in seconds. Default 1 year. */
17
+ cookieMaxAge?: number;
18
+ /**
19
+ * Override individual cookie flags. Sensible defaults apply (`httpOnly: true`,
20
+ * `sameSite: 'lax'`, `path: '/'`, and `secure` auto-detected from the request
21
+ * protocol).
22
+ */
23
+ cookieOptions?: Partial<CookieSerializeOptions>;
24
+ }
25
+ /**
26
+ * Pure inputs accepted by {@link deriveFingerprint}. Use this if you already
27
+ * manage the device identity yourself (e.g. an existing session-cookie system
28
+ * or a signed JWT) and want to plug a fingerprint into `crypto.encrypt`.
29
+ */
30
+ interface FingerprintParts {
31
+ /** Opaque, stable per-device identifier. 16+ random bytes recommended. */
32
+ deviceId: string;
33
+ /** Per-app pepper. Required. */
34
+ salt: string;
35
+ }
36
+ /**
37
+ * Compute a fingerprint string from explicit parts. SHA-256 → base64.
38
+ *
39
+ * The fingerprint is deterministic — call it twice with the same inputs and
40
+ * you get the same output. That's what lets `crypto.decrypt` succeed on a
41
+ * payload encrypted earlier with the same device ID.
42
+ */
43
+ declare function deriveFingerprint(parts: FingerprintParts): Promise<string>;
44
+ /**
45
+ * Ensure the client has a persistent HttpOnly device-ID cookie, then derive a
46
+ * fingerprint from it. Call this from Nitro route handlers or server
47
+ * middleware before encrypting / decrypting.
48
+ *
49
+ * Why an HttpOnly cookie (and not the client IP)?
50
+ * - **Survives network changes.** The cookie travels with the browser, so
51
+ * Wi-Fi ↔ 4G handoffs, cell-tower roaming, VPN rotations, and residential
52
+ * IP rotation don't invalidate the fingerprint.
53
+ * - **Anti-theft.** The cookie is HttpOnly, so XSS-exfiltrated ciphertext
54
+ * stored in localStorage can't be decrypted — the attacker can't read the
55
+ * cookie.
56
+ * - **Per-device / per-browser.** A token copied to another browser or
57
+ * device finds no device cookie there, so it won't decrypt.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // server/api/session.post.ts
62
+ * import { getClientFingerprint } from '@alikhalilll/nuxt-crypto/server';
63
+ *
64
+ * export default defineEventHandler(async (event) => {
65
+ * const body = await readBody(event);
66
+ * const { $crypto } = useNuxtApp();
67
+ * const fingerprint = await getClientFingerprint(event, {
68
+ * salt: useRuntimeConfig().cryptoFingerprintSalt,
69
+ * });
70
+ * return { token: await $crypto.encrypt(JSON.stringify(body), { fingerprint }) };
71
+ * });
72
+ * ```
73
+ */
74
+ declare function getClientFingerprint(event: H3Event, options: ClientFingerprintOptions): Promise<string>;
75
+
76
+ export { type ClientFingerprintOptions, type FingerprintParts, deriveFingerprint, getClientFingerprint };
@@ -0,0 +1,42 @@
1
+ import {
2
+ getRandomBytes,
3
+ getSubtle,
4
+ toBase64
5
+ } from "../chunk-FKXZ7VYW.mjs";
6
+
7
+ // src/server/index.ts
8
+ import { getCookie, getRequestProtocol, setCookie } from "h3";
9
+ var DEFAULT_COOKIE_NAME = "__nuxt_crypto_device";
10
+ var DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
11
+ var DEVICE_ID_BYTES = 32;
12
+ async function deriveFingerprint(parts) {
13
+ const material = `${parts.deviceId}\0${parts.salt}`;
14
+ const subtle = await getSubtle();
15
+ const hash = await subtle.digest("SHA-256", new TextEncoder().encode(material));
16
+ return toBase64(new Uint8Array(hash));
17
+ }
18
+ async function getClientFingerprint(event, options) {
19
+ if (!options.salt) {
20
+ throw new Error("[nuxt-crypto] getClientFingerprint: `salt` is required.");
21
+ }
22
+ const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;
23
+ let deviceId = getCookie(event, cookieName);
24
+ if (!deviceId) {
25
+ deviceId = toBase64(getRandomBytes(DEVICE_ID_BYTES));
26
+ const isSecure = options.cookieOptions?.secure ?? getRequestProtocol(event, { xForwardedProto: true }) === "https";
27
+ setCookie(event, cookieName, deviceId, {
28
+ httpOnly: true,
29
+ sameSite: "lax",
30
+ path: "/",
31
+ maxAge: options.cookieMaxAge ?? DEFAULT_COOKIE_MAX_AGE,
32
+ ...options.cookieOptions,
33
+ secure: isSecure
34
+ });
35
+ }
36
+ return deriveFingerprint({ deviceId, salt: options.salt });
37
+ }
38
+ export {
39
+ deriveFingerprint,
40
+ getClientFingerprint
41
+ };
42
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["import type { H3Event } from 'h3';\nimport { getCookie, getRequestProtocol, setCookie } from 'h3';\n\nimport { toBase64 } from '../core/base64';\nimport { getRandomBytes, getSubtle } from '../core/subtle';\n\n// h3 doesn't re-export its cookie-options type, so derive it from setCookie.\ntype CookieSerializeOptions = NonNullable<Parameters<typeof setCookie>[3]>;\n\nconst DEFAULT_COOKIE_NAME = '__nuxt_crypto_device';\nconst DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // one year\nconst DEVICE_ID_BYTES = 32;\n\n/**\n * Options for {@link getClientFingerprint}.\n */\nexport interface ClientFingerprintOptions {\n /**\n * Per-app pepper. Stored in a runtime-config secret (e.g.\n * `NUXT_CRYPTO_FINGERPRINT_SALT`) — keeping it off the client means the\n * fingerprint can't be reproduced from a leaked device cookie alone.\n */\n salt: string;\n /** Cookie name holding the device ID. Default `__nuxt_crypto_device`. */\n cookieName?: string;\n /** Cookie lifetime in seconds. Default 1 year. */\n cookieMaxAge?: number;\n /**\n * Override individual cookie flags. Sensible defaults apply (`httpOnly: true`,\n * `sameSite: 'lax'`, `path: '/'`, and `secure` auto-detected from the request\n * protocol).\n */\n cookieOptions?: Partial<CookieSerializeOptions>;\n}\n\n/**\n * Pure inputs accepted by {@link deriveFingerprint}. Use this if you already\n * manage the device identity yourself (e.g. an existing session-cookie system\n * or a signed JWT) and want to plug a fingerprint into `crypto.encrypt`.\n */\nexport interface FingerprintParts {\n /** Opaque, stable per-device identifier. 16+ random bytes recommended. */\n deviceId: string;\n /** Per-app pepper. Required. */\n salt: string;\n}\n\n/**\n * Compute a fingerprint string from explicit parts. SHA-256 → base64.\n *\n * The fingerprint is deterministic — call it twice with the same inputs and\n * you get the same output. That's what lets `crypto.decrypt` succeed on a\n * payload encrypted earlier with the same device ID.\n */\nexport async function deriveFingerprint(parts: FingerprintParts): Promise<string> {\n // NUL separator prevents `\"abc\" + \"d\"` vs `\"ab\" + \"cd\"` collisions.\n const material = `${parts.deviceId}\\x00${parts.salt}`;\n const subtle = await getSubtle();\n const hash = await subtle.digest('SHA-256', new TextEncoder().encode(material));\n return toBase64(new Uint8Array(hash));\n}\n\n/**\n * Ensure the client has a persistent HttpOnly device-ID cookie, then derive a\n * fingerprint from it. Call this from Nitro route handlers or server\n * middleware before encrypting / decrypting.\n *\n * Why an HttpOnly cookie (and not the client IP)?\n * - **Survives network changes.** The cookie travels with the browser, so\n * Wi-Fi ↔ 4G handoffs, cell-tower roaming, VPN rotations, and residential\n * IP rotation don't invalidate the fingerprint.\n * - **Anti-theft.** The cookie is HttpOnly, so XSS-exfiltrated ciphertext\n * stored in localStorage can't be decrypted — the attacker can't read the\n * cookie.\n * - **Per-device / per-browser.** A token copied to another browser or\n * device finds no device cookie there, so it won't decrypt.\n *\n * @example\n * ```ts\n * // server/api/session.post.ts\n * import { getClientFingerprint } from '@alikhalilll/nuxt-crypto/server';\n *\n * export default defineEventHandler(async (event) => {\n * const body = await readBody(event);\n * const { $crypto } = useNuxtApp();\n * const fingerprint = await getClientFingerprint(event, {\n * salt: useRuntimeConfig().cryptoFingerprintSalt,\n * });\n * return { token: await $crypto.encrypt(JSON.stringify(body), { fingerprint }) };\n * });\n * ```\n */\nexport async function getClientFingerprint(\n event: H3Event,\n options: ClientFingerprintOptions\n): Promise<string> {\n if (!options.salt) {\n throw new Error('[nuxt-crypto] getClientFingerprint: `salt` is required.');\n }\n\n const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;\n let deviceId = getCookie(event, cookieName);\n\n if (!deviceId) {\n deviceId = toBase64(getRandomBytes(DEVICE_ID_BYTES));\n const isSecure =\n options.cookieOptions?.secure ??\n getRequestProtocol(event, { xForwardedProto: true }) === 'https';\n setCookie(event, cookieName, deviceId, {\n httpOnly: true,\n sameSite: 'lax',\n path: '/',\n maxAge: options.cookieMaxAge ?? DEFAULT_COOKIE_MAX_AGE,\n ...options.cookieOptions,\n secure: isSecure,\n });\n }\n\n return deriveFingerprint({ deviceId, salt: options.salt });\n}\n"],"mappings":";;;;;;;AACA,SAAS,WAAW,oBAAoB,iBAAiB;AAQzD,IAAM,sBAAsB;AAC5B,IAAM,yBAAyB,KAAK,KAAK,KAAK;AAC9C,IAAM,kBAAkB;AA2CxB,eAAsB,kBAAkB,OAA0C;AAEhF,QAAM,WAAW,GAAG,MAAM,QAAQ,KAAO,MAAM,IAAI;AACnD,QAAM,SAAS,MAAM,UAAU;AAC/B,QAAM,OAAO,MAAM,OAAO,OAAO,WAAW,IAAI,YAAY,EAAE,OAAO,QAAQ,CAAC;AAC9E,SAAO,SAAS,IAAI,WAAW,IAAI,CAAC;AACtC;AAgCA,eAAsB,qBACpB,OACA,SACiB;AACjB,MAAI,CAAC,QAAQ,MAAM;AACjB,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AAEA,QAAM,aAAa,QAAQ,cAAc;AACzC,MAAI,WAAW,UAAU,OAAO,UAAU;AAE1C,MAAI,CAAC,UAAU;AACb,eAAW,SAAS,eAAe,eAAe,CAAC;AACnD,UAAM,WACJ,QAAQ,eAAe,UACvB,mBAAmB,OAAO,EAAE,iBAAiB,KAAK,CAAC,MAAM;AAC3D,cAAU,OAAO,YAAY,UAAU;AAAA,MACrC,UAAU;AAAA,MACV,UAAU;AAAA,MACV,MAAM;AAAA,MACN,QAAQ,QAAQ,gBAAgB;AAAA,MAChC,GAAG,QAAQ;AAAA,MACX,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAEA,SAAO,kBAAkB,EAAE,UAAU,MAAM,QAAQ,KAAK,CAAC;AAC3D;","names":[]}
@@ -1,4 +1,4 @@
1
- export { B as Bytes, C as CryptoAlgorithm, a as CryptoService, b as CryptoServiceConfig, P as ParsedPayload } from '../payload-Cht5Ar0K.js';
1
+ export { B as Bytes, C as CryptoAlgorithm, a as CryptoOperationOptions, b as CryptoService, c as CryptoServiceConfig, P as ParsedPayload } from '../payload-BOueSWcI.js';
2
2
 
3
3
  interface CryptoModuleOptions {
4
4
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alikhalilll/nuxt-crypto",
3
- "version": "1.0.3",
4
- "description": "Symmetric Web Crypto service for Nuxt 3/4. AES-256-GCM + PBKDF2 with key caching and pluggable algorithms.",
3
+ "version": "1.2.1",
4
+ "description": "Symmetric Web Crypto service for Nuxt 3/4. AES-256-GCM + PBKDF2 with key caching, pluggable algorithms, and optional device-fingerprint binding.",
5
5
  "license": "MIT",
6
6
  "author": "alikhalilll",
7
7
  "repository": {
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/alikhalilll/ali-nuxt-toolkit.git",
10
10
  "directory": "packages/crypto"
11
11
  },
12
- "homepage": "https://github.com/alikhalilll/ali-nuxt-toolkit#readme",
12
+ "homepage": "https://alikhalilll.github.io/ali-nuxt-toolkit/crypto",
13
13
  "bugs": {
14
14
  "url": "https://github.com/alikhalilll/ali-nuxt-toolkit/issues"
15
15
  },
@@ -25,7 +25,8 @@
25
25
  "encryption",
26
26
  "aes-gcm",
27
27
  "web-crypto",
28
- "pbkdf2"
28
+ "pbkdf2",
29
+ "fingerprint"
29
30
  ],
30
31
  "exports": {
31
32
  ".": {
@@ -36,6 +37,10 @@
36
37
  "types": "./dist/core/index.d.ts",
37
38
  "import": "./dist/core/index.mjs"
38
39
  },
40
+ "./server": {
41
+ "types": "./dist/server/index.d.ts",
42
+ "import": "./dist/server/index.mjs"
43
+ },
39
44
  "./types": {
40
45
  "types": "./dist/types/index.d.ts",
41
46
  "import": "./dist/types/index.mjs"
@@ -51,6 +56,9 @@
51
56
  "core": [
52
57
  "./dist/core/index.d.ts"
53
58
  ],
59
+ "server": [
60
+ "./dist/server/index.d.ts"
61
+ ],
54
62
  "types": [
55
63
  "./dist/types/index.d.ts"
56
64
  ]
@@ -65,17 +73,22 @@
65
73
  "node": ">=18"
66
74
  },
67
75
  "peerDependencies": {
68
- "@nuxt/kit": "^3.0.0 || ^4.0.0"
76
+ "@nuxt/kit": "^3.0.0 || ^4.0.0",
77
+ "h3": "^1.0.0 || ^2.0.0"
69
78
  },
70
79
  "peerDependenciesMeta": {
71
80
  "@nuxt/kit": {
72
81
  "optional": true
82
+ },
83
+ "h3": {
84
+ "optional": true
73
85
  }
74
86
  },
75
87
  "devDependencies": {
76
88
  "@nuxt/kit": "^4.0.0",
77
89
  "@nuxt/schema": "^4.0.0",
78
90
  "@types/node": "^22.13.0",
91
+ "h3": "^1.13.0",
79
92
  "rimraf": "^6.0.1",
80
93
  "tsup": "^8.5.0",
81
94
  "typescript": "^5.9.2"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/algorithms/aes-gcm.ts","../src/core/base64.ts","../src/core/key-cache.ts","../src/core/payload.ts","../src/core/subtle.ts","../src/core/create-service.ts"],"sourcesContent":["import type { CryptoAlgorithm } from '../types';\n\n/**\n * AES-256-GCM with PBKDF2-SHA256 key derivation. This is the default and\n * currently the only built-in algorithm; pluggable via `CryptoServiceConfig.algorithm`.\n */\nexport const aesGcmPbkdf2: CryptoAlgorithm = {\n version: 'v1',\n\n async deriveKey({ subtle, passphrase, salt, iterations }) {\n const keyMaterial = await subtle.importKey(\n 'raw',\n new TextEncoder().encode(passphrase),\n { name: 'PBKDF2' },\n false,\n ['deriveKey']\n );\n\n return subtle.deriveKey(\n { name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },\n keyMaterial,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt']\n );\n },\n\n async encrypt({ subtle, key, plainText }) {\n const iv = new Uint8Array(new ArrayBuffer(12));\n globalThis.crypto.getRandomValues(iv);\n const buf = await subtle.encrypt({ name: 'AES-GCM', iv }, key, plainText);\n const cipher = new Uint8Array(new ArrayBuffer(buf.byteLength));\n cipher.set(new Uint8Array(buf));\n return { cipher, iv };\n },\n\n async decrypt({ subtle, key, cipher, iv }) {\n const buf = await subtle.decrypt({ name: 'AES-GCM', iv }, key, cipher);\n const out = new Uint8Array(new ArrayBuffer(buf.byteLength));\n out.set(new Uint8Array(buf));\n return out;\n },\n};\n","import type { Bytes } from './types';\n\n/** Encode bytes to standard base64. Works in browser and Node. */\nexport function toBase64(data: Bytes): string {\n if (typeof btoa === 'function') {\n let binary = '';\n const CHUNK = 0x8000;\n for (let i = 0; i < data.length; i += CHUNK) {\n binary += String.fromCharCode(...data.subarray(i, i + CHUNK));\n }\n return btoa(binary);\n }\n // Node fallback — Buffer is globally available in Node.\n return Buffer.from(data).toString('base64');\n}\n\n/** Decode a base64 string back to bytes. Works in browser and Node. */\nexport function fromBase64(input: string): Bytes {\n if (typeof atob === 'function') {\n const binary = atob(input);\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n }\n const buf = Buffer.from(input, 'base64');\n const out = new Uint8Array(buf.length);\n out.set(buf);\n return out;\n}\n","import type { Bytes } from './types';\nimport { toBase64 } from './base64';\n\n/**\n * A tiny LRU-ish cache for derived `CryptoKey` values, keyed on the salt.\n * When capacity is exceeded the oldest entry is evicted. Derived key\n * material is expensive (PBKDF2 is designed to be slow), so caching has\n * a big impact when decrypting many payloads in a row.\n */\nexport class KeyCache {\n private readonly map = new Map<string, Promise<CryptoKey>>();\n constructor(private readonly capacity: number) {}\n\n static key(salt: Bytes, iterations: number): string {\n return `${iterations}.${toBase64(salt)}`;\n }\n\n get(key: string): Promise<CryptoKey> | undefined {\n const value = this.map.get(key);\n if (!value) return undefined;\n // Refresh recency by re-inserting.\n this.map.delete(key);\n this.map.set(key, value);\n return value;\n }\n\n set(key: string, value: Promise<CryptoKey>): void {\n if (this.capacity <= 0) return;\n this.map.set(key, value);\n while (this.map.size > this.capacity) {\n const oldest = this.map.keys().next().value;\n if (oldest === undefined) break;\n this.map.delete(oldest);\n }\n }\n\n clear(): void {\n this.map.clear();\n }\n}\n","import { fromBase64, toBase64 } from './base64';\nimport type { Bytes } from './types';\n\n/**\n * Payload serialisation format (v1): `${version}.${saltB64}.${ivB64}.${cipherB64}`.\n *\n * Each part is URL-safe base64, all joined with `.`. The version tag is\n * always the first segment so consumers can route to the correct algorithm\n * during decrypt.\n */\nexport interface ParsedPayload {\n version: string;\n salt: Bytes;\n iv: Bytes;\n cipher: Bytes;\n}\n\nexport function encodePayload(parts: ParsedPayload): string {\n return [parts.version, toBase64(parts.salt), toBase64(parts.iv), toBase64(parts.cipher)].join(\n '.'\n );\n}\n\nexport function parsePayload(payload: string): ParsedPayload {\n const segments = payload.split('.');\n if (segments.length !== 4) {\n throw new Error('[nuxt-crypto] Invalid payload format — expected 4 dot-separated segments.');\n }\n const [version, saltB64, ivB64, cipherB64] = segments;\n if (!version || !saltB64 || !ivB64 || !cipherB64) {\n throw new Error('[nuxt-crypto] Invalid payload format — one or more segments were empty.');\n }\n return {\n version,\n salt: fromBase64(saltB64),\n iv: fromBase64(ivB64),\n cipher: fromBase64(cipherB64),\n };\n}\n","/**\n * Resolve a SubtleCrypto implementation across environments:\n *\n * - Browser / Deno / Bun / Node 20+: `globalThis.crypto.subtle` is available.\n * - Older Node: fall back to `node:crypto`'s `webcrypto`.\n */\nexport async function getSubtle(): Promise<SubtleCrypto> {\n if (globalThis.crypto?.subtle) return globalThis.crypto.subtle;\n const { webcrypto } = await import('node:crypto');\n return webcrypto.subtle as unknown as SubtleCrypto;\n}\n\n/** Fill the given buffer with cryptographically strong random bytes. */\nexport function getRandomBytes(length: number): Uint8Array<ArrayBuffer> {\n const out = new Uint8Array(length);\n const c = globalThis.crypto;\n if (c?.getRandomValues) {\n c.getRandomValues(out);\n return out;\n }\n throw new Error(\n '[nuxt-crypto] globalThis.crypto.getRandomValues is not available. Upgrade your runtime.'\n );\n}\n","import { aesGcmPbkdf2 } from './algorithms/aes-gcm';\nimport { KeyCache } from './key-cache';\nimport { encodePayload, parsePayload } from './payload';\nimport { getRandomBytes, getSubtle } from './subtle';\nimport type { CryptoAlgorithm, CryptoService, CryptoServiceConfig } from './types';\n\nconst DEFAULT_ITERATIONS = 100_000;\nconst DEFAULT_CACHE = 64;\nconst SALT_BYTES = 16;\n\n/**\n * Create a framework-agnostic `CryptoService` from a passphrase.\n *\n * Uses AES-256-GCM with PBKDF2-SHA256 (100k iterations by default). Pass a\n * different `algorithm` to plug in another cipher while preserving the same\n * versioned payload envelope.\n */\nexport async function createCryptoService(config: CryptoServiceConfig): Promise<CryptoService> {\n if (!config.passphrase) {\n throw new Error('[nuxt-crypto] passphrase is required.');\n }\n\n const iterations = config.iterations ?? DEFAULT_ITERATIONS;\n const algorithm: CryptoAlgorithm = config.algorithm ?? aesGcmPbkdf2;\n const subtle = config.subtle ?? (await getSubtle());\n const cache = new KeyCache(config.keyCacheSize ?? DEFAULT_CACHE);\n const passphrase = String(config.passphrase).trim();\n const encoder = new TextEncoder();\n const decoder = new TextDecoder();\n\n async function getDerivedKey(salt: Uint8Array<ArrayBuffer>): Promise<CryptoKey> {\n const key = KeyCache.key(salt, iterations);\n const cached = cache.get(key);\n if (cached) return cached;\n const pending = algorithm.deriveKey({ subtle, passphrase, salt, iterations });\n cache.set(key, pending);\n return pending;\n }\n\n return {\n async encrypt(plainText: string): Promise<string> {\n const salt = getRandomBytes(SALT_BYTES);\n const key = await getDerivedKey(salt);\n const encoded = encoder.encode(plainText);\n const plainBytes = new Uint8Array(new ArrayBuffer(encoded.byteLength));\n plainBytes.set(encoded);\n const { cipher, iv } = await algorithm.encrypt({ subtle, key, plainText: plainBytes });\n return encodePayload({ version: algorithm.version, salt, iv, cipher });\n },\n\n async decrypt(payload: string): Promise<string> {\n const parsed = parsePayload(payload);\n if (parsed.version !== algorithm.version) {\n throw new Error(\n `[nuxt-crypto] Unsupported payload version: ${parsed.version} (algorithm expects ${algorithm.version}).`\n );\n }\n const key = await getDerivedKey(parsed.salt);\n const plainBytes = await algorithm.decrypt({\n subtle,\n key,\n cipher: parsed.cipher,\n iv: parsed.iv,\n });\n return decoder.decode(plainBytes);\n },\n\n clearKeyCache(): void {\n cache.clear();\n },\n };\n}\n"],"mappings":"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","names":["out"]}