@alikhalilll/nuxt-crypto 1.2.1 → 1.3.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 +11 -2
- package/dist/core/index.d.ts +20 -15
- package/dist/core/index.mjs +3 -25
- package/dist/create-service-CgBxeMb0.mjs +179 -0
- package/dist/create-service-CgBxeMb0.mjs.map +1 -0
- package/dist/index-DkMNygRG.d.ts +30 -0
- package/dist/module.d.ts +7 -5
- package/dist/module.mjs +61 -64
- package/dist/module.mjs.map +1 -1
- package/dist/payload-CvzYMicC.d.ts +111 -0
- package/dist/runtime/plugin.d.ts +5 -0
- package/dist/runtime/plugin.mjs +13 -25
- package/dist/runtime/plugin.mjs.map +1 -1
- package/dist/server/index.d.ts +25 -23
- package/dist/server/index.mjs +67 -36
- package/dist/server/index.mjs.map +1 -1
- package/dist/subtle-Dh8m0gIA.mjs +51 -0
- package/dist/subtle-Dh8m0gIA.mjs.map +1 -0
- package/dist/types/index.d.ts +3 -30
- package/dist/types/index.mjs +0 -2
- package/logo-dark.png +0 -0
- package/logo-light.png +0 -0
- package/logo.svg +23 -0
- package/package.json +12 -5
- package/dist/chunk-EFZPSZWO.mjs +0 -1
- package/dist/chunk-EFZPSZWO.mjs.map +0 -1
- package/dist/chunk-FKHKOTFR.mjs +0 -160
- package/dist/chunk-FKHKOTFR.mjs.map +0 -1
- package/dist/chunk-FKXZ7VYW.mjs +0 -50
- package/dist/chunk-FKXZ7VYW.mjs.map +0 -1
- package/dist/core/index.mjs.map +0 -1
- package/dist/payload-BOueSWcI.d.ts +0 -108
- package/dist/types/index.mjs.map +0 -1
package/dist/runtime/plugin.mjs
CHANGED
|
@@ -1,30 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createCryptoService
|
|
3
|
-
} from "../chunk-FKHKOTFR.mjs";
|
|
4
|
-
import "../chunk-FKXZ7VYW.mjs";
|
|
5
|
-
|
|
6
|
-
// src/runtime/plugin.ts
|
|
1
|
+
import { t as createCryptoService } from "../create-service-CgBxeMb0.mjs";
|
|
7
2
|
import { defineNuxtPlugin } from "#app";
|
|
8
3
|
import config from "#build/crypto-config.mjs";
|
|
4
|
+
//#region src/runtime/plugin.ts
|
|
9
5
|
var plugin_default = defineNuxtPlugin(async () => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
iterations,
|
|
19
|
-
keyCacheSize
|
|
20
|
-
});
|
|
21
|
-
return {
|
|
22
|
-
provide: {
|
|
23
|
-
[provideName]: service
|
|
24
|
-
}
|
|
25
|
-
};
|
|
6
|
+
const { passphrase, iterations, keyCacheSize, provideName } = config;
|
|
7
|
+
if (!passphrase && import.meta.dev) console.warn(`[@alikhalilll/nuxt-crypto] Missing passphrase — $${provideName} will throw when used.`);
|
|
8
|
+
const service = await createCryptoService({
|
|
9
|
+
passphrase: String(passphrase).trim(),
|
|
10
|
+
iterations,
|
|
11
|
+
keyCacheSize
|
|
12
|
+
});
|
|
13
|
+
return { provide: { [provideName]: service } };
|
|
26
14
|
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
//#endregion
|
|
16
|
+
export { plugin_default as default };
|
|
17
|
+
|
|
30
18
|
//# sourceMappingURL=plugin.mjs.map
|
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":[],"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":";;;;AAKA,IAAA,iBAAe,iBAAiB,YAAY;CAC1C,MAAM,EAAE,YAAY,YAAY,cAAc,gBAAgB;CAE9D,IAAI,CAAC,cAAe,OAAO,KAAwC,KAEjE,QAAQ,KACN,oDAAoD,YAAY,uBAClE;CAGF,MAAM,UAAU,MAAM,oBAAoB;EACxC,YAAY,OAAO,UAAU,EAAE,KAAK;EACpC;EACA;CACF,CAAC;CAED,OAAO,EACL,SAAS,GACN,cAAc,QACjB,EACF;AACF,CAAC"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { H3Event, setCookie } from "h3";
|
|
2
2
|
|
|
3
|
+
//#region src/server/index.d.ts
|
|
3
4
|
type CookieSerializeOptions = NonNullable<Parameters<typeof setCookie>[3]>;
|
|
4
5
|
/**
|
|
5
6
|
* Options for {@link getClientFingerprint}.
|
|
6
7
|
*/
|
|
7
8
|
interface ClientFingerprintOptions {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Per-app pepper. Stored in a runtime-config secret (e.g.
|
|
11
|
+
* `NUXT_CRYPTO_FINGERPRINT_SALT`) — keeping it off the client means the
|
|
12
|
+
* fingerprint can't be reproduced from a leaked device cookie alone.
|
|
13
|
+
*/
|
|
14
|
+
salt: string;
|
|
15
|
+
/** Cookie name holding the device ID. Default `__nuxt_crypto_device`. */
|
|
16
|
+
cookieName?: string;
|
|
17
|
+
/** Cookie lifetime in seconds. Default 1 year. */
|
|
18
|
+
cookieMaxAge?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Override individual cookie flags. Sensible defaults apply (`httpOnly: true`,
|
|
21
|
+
* `sameSite: 'lax'`, `path: '/'`, and `secure` auto-detected from the request
|
|
22
|
+
* protocol).
|
|
23
|
+
*/
|
|
24
|
+
cookieOptions?: Partial<CookieSerializeOptions>;
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Pure inputs accepted by {@link deriveFingerprint}. Use this if you already
|
|
@@ -28,10 +29,10 @@ interface ClientFingerprintOptions {
|
|
|
28
29
|
* or a signed JWT) and want to plug a fingerprint into `crypto.encrypt`.
|
|
29
30
|
*/
|
|
30
31
|
interface FingerprintParts {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
/** Opaque, stable per-device identifier. 16+ random bytes recommended. */
|
|
33
|
+
deviceId: string;
|
|
34
|
+
/** Per-app pepper. Required. */
|
|
35
|
+
salt: string;
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
38
|
* Compute a fingerprint string from explicit parts. SHA-256 → base64.
|
|
@@ -72,5 +73,6 @@ declare function deriveFingerprint(parts: FingerprintParts): Promise<string>;
|
|
|
72
73
|
* ```
|
|
73
74
|
*/
|
|
74
75
|
declare function getClientFingerprint(event: H3Event, options: ClientFingerprintOptions): Promise<string>;
|
|
75
|
-
|
|
76
|
-
export {
|
|
76
|
+
//#endregion
|
|
77
|
+
export { ClientFingerprintOptions, FingerprintParts, deriveFingerprint, getClientFingerprint };
|
|
78
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,42 +1,73 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getRandomBytes,
|
|
3
|
-
getSubtle,
|
|
4
|
-
toBase64
|
|
5
|
-
} from "../chunk-FKXZ7VYW.mjs";
|
|
6
|
-
|
|
7
|
-
// src/server/index.ts
|
|
1
|
+
import { i as toBase64, n as getSubtle, t as getRandomBytes } from "../subtle-Dh8m0gIA.mjs";
|
|
8
2
|
import { getCookie, getRequestProtocol, setCookie } from "h3";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
//#region src/server/index.ts
|
|
4
|
+
const DEFAULT_COOKIE_NAME = "__nuxt_crypto_device";
|
|
5
|
+
const DEFAULT_COOKIE_MAX_AGE = 3600 * 24 * 365;
|
|
6
|
+
const DEVICE_ID_BYTES = 32;
|
|
7
|
+
/**
|
|
8
|
+
* Compute a fingerprint string from explicit parts. SHA-256 → base64.
|
|
9
|
+
*
|
|
10
|
+
* The fingerprint is deterministic — call it twice with the same inputs and
|
|
11
|
+
* you get the same output. That's what lets `crypto.decrypt` succeed on a
|
|
12
|
+
* payload encrypted earlier with the same device ID.
|
|
13
|
+
*/
|
|
12
14
|
async function deriveFingerprint(parts) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return toBase64(new Uint8Array(hash));
|
|
15
|
+
const material = `${parts.deviceId}\x00${parts.salt}`;
|
|
16
|
+
const hash = await (await getSubtle()).digest("SHA-256", new TextEncoder().encode(material));
|
|
17
|
+
return toBase64(new Uint8Array(hash));
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Ensure the client has a persistent HttpOnly device-ID cookie, then derive a
|
|
21
|
+
* fingerprint from it. Call this from Nitro route handlers or server
|
|
22
|
+
* middleware before encrypting / decrypting.
|
|
23
|
+
*
|
|
24
|
+
* Why an HttpOnly cookie (and not the client IP)?
|
|
25
|
+
* - **Survives network changes.** The cookie travels with the browser, so
|
|
26
|
+
* Wi-Fi ↔ 4G handoffs, cell-tower roaming, VPN rotations, and residential
|
|
27
|
+
* IP rotation don't invalidate the fingerprint.
|
|
28
|
+
* - **Anti-theft.** The cookie is HttpOnly, so XSS-exfiltrated ciphertext
|
|
29
|
+
* stored in localStorage can't be decrypted — the attacker can't read the
|
|
30
|
+
* cookie.
|
|
31
|
+
* - **Per-device / per-browser.** A token copied to another browser or
|
|
32
|
+
* device finds no device cookie there, so it won't decrypt.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* // server/api/session.post.ts
|
|
37
|
+
* import { getClientFingerprint } from '@alikhalilll/nuxt-crypto/server';
|
|
38
|
+
*
|
|
39
|
+
* export default defineEventHandler(async (event) => {
|
|
40
|
+
* const body = await readBody(event);
|
|
41
|
+
* const { $crypto } = useNuxtApp();
|
|
42
|
+
* const fingerprint = await getClientFingerprint(event, {
|
|
43
|
+
* salt: useRuntimeConfig().cryptoFingerprintSalt,
|
|
44
|
+
* });
|
|
45
|
+
* return { token: await $crypto.encrypt(JSON.stringify(body), { fingerprint }) };
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
18
49
|
async function getClientFingerprint(event, options) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
if (!options.salt) throw new Error("[nuxt-crypto] getClientFingerprint: `salt` is required.");
|
|
51
|
+
const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
52
|
+
let deviceId = getCookie(event, cookieName);
|
|
53
|
+
if (!deviceId) {
|
|
54
|
+
deviceId = toBase64(getRandomBytes(DEVICE_ID_BYTES));
|
|
55
|
+
const isSecure = options.cookieOptions?.secure ?? getRequestProtocol(event, { xForwardedProto: true }) === "https";
|
|
56
|
+
setCookie(event, cookieName, deviceId, {
|
|
57
|
+
httpOnly: true,
|
|
58
|
+
sameSite: "lax",
|
|
59
|
+
path: "/",
|
|
60
|
+
maxAge: options.cookieMaxAge ?? DEFAULT_COOKIE_MAX_AGE,
|
|
61
|
+
...options.cookieOptions,
|
|
62
|
+
secure: isSecure
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return deriveFingerprint({
|
|
66
|
+
deviceId,
|
|
67
|
+
salt: options.salt
|
|
68
|
+
});
|
|
37
69
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
70
|
+
//#endregion
|
|
71
|
+
export { deriveFingerprint, getClientFingerprint };
|
|
72
|
+
|
|
42
73
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +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":"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"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":";;;AASA,MAAM,sBAAsB;AAC5B,MAAM,yBAAyB,OAAU,KAAK;AAC9C,MAAM,kBAAkB;;;;;;;;AA2CxB,eAAsB,kBAAkB,OAA0C;CAEhF,MAAM,WAAW,GAAG,MAAM,SAAS,MAAM,MAAM;CAE/C,MAAM,OAAO,OAAM,MADE,UAAU,GACL,OAAO,WAAW,IAAI,YAAY,EAAE,OAAO,QAAQ,CAAC;CAC9E,OAAO,SAAS,IAAI,WAAW,IAAI,CAAC;AACtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,qBACpB,OACA,SACiB;CACjB,IAAI,CAAC,QAAQ,MACX,MAAM,IAAI,MAAM,yDAAyD;CAG3E,MAAM,aAAa,QAAQ,cAAc;CACzC,IAAI,WAAW,UAAU,OAAO,UAAU;CAE1C,IAAI,CAAC,UAAU;EACb,WAAW,SAAS,eAAe,eAAe,CAAC;EACnD,MAAM,WACJ,QAAQ,eAAe,UACvB,mBAAmB,OAAO,EAAE,iBAAiB,KAAK,CAAC,MAAM;EAC3D,UAAU,OAAO,YAAY,UAAU;GACrC,UAAU;GACV,UAAU;GACV,MAAM;GACN,QAAQ,QAAQ,gBAAgB;GAChC,GAAG,QAAQ;GACX,QAAQ;EACV,CAAC;CACH;CAEA,OAAO,kBAAkB;EAAE;EAAU,MAAM,QAAQ;CAAK,CAAC;AAC3D"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/core/base64.ts
|
|
2
|
+
/** Encode bytes to standard base64. Works in browser and Node. */
|
|
3
|
+
function toBase64(data) {
|
|
4
|
+
if (typeof btoa === "function") {
|
|
5
|
+
let binary = "";
|
|
6
|
+
const CHUNK = 32768;
|
|
7
|
+
for (let i = 0; i < data.length; i += CHUNK) binary += String.fromCharCode(...data.subarray(i, i + CHUNK));
|
|
8
|
+
return btoa(binary);
|
|
9
|
+
}
|
|
10
|
+
return Buffer.from(data).toString("base64");
|
|
11
|
+
}
|
|
12
|
+
/** Decode a base64 string back to bytes. Works in browser and Node. */
|
|
13
|
+
function fromBase64(input) {
|
|
14
|
+
if (typeof atob === "function") {
|
|
15
|
+
const binary = atob(input);
|
|
16
|
+
const out = new Uint8Array(binary.length);
|
|
17
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
const buf = Buffer.from(input, "base64");
|
|
21
|
+
const out = new Uint8Array(buf.length);
|
|
22
|
+
out.set(buf);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/core/subtle.ts
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a SubtleCrypto implementation across environments:
|
|
29
|
+
*
|
|
30
|
+
* - Browser / Deno / Bun / Node 20+: `globalThis.crypto.subtle` is available.
|
|
31
|
+
* - Older Node: fall back to `node:crypto`'s `webcrypto`.
|
|
32
|
+
*/
|
|
33
|
+
async function getSubtle() {
|
|
34
|
+
if (globalThis.crypto?.subtle) return globalThis.crypto.subtle;
|
|
35
|
+
const { webcrypto } = await import("node:crypto");
|
|
36
|
+
return webcrypto.subtle;
|
|
37
|
+
}
|
|
38
|
+
/** Fill the given buffer with cryptographically strong random bytes. */
|
|
39
|
+
function getRandomBytes(length) {
|
|
40
|
+
const out = new Uint8Array(length);
|
|
41
|
+
const c = globalThis.crypto;
|
|
42
|
+
if (c?.getRandomValues) {
|
|
43
|
+
c.getRandomValues(out);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
throw new Error("[nuxt-crypto] globalThis.crypto.getRandomValues is not available. Upgrade your runtime.");
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { toBase64 as i, getSubtle as n, fromBase64 as r, getRandomBytes as t };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=subtle-Dh8m0gIA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subtle-Dh8m0gIA.mjs","names":[],"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":";;AAGA,SAAgB,SAAS,MAAqB;CAC5C,IAAI,OAAO,SAAS,YAAY;EAC9B,IAAI,SAAS;EACb,MAAM,QAAQ;EACd,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,OACpC,UAAU,OAAO,aAAa,GAAG,KAAK,SAAS,GAAG,IAAI,KAAK,CAAC;EAE9D,OAAO,KAAK,MAAM;CACpB;CAEA,OAAO,OAAO,KAAK,IAAI,EAAE,SAAS,QAAQ;AAC5C;;AAGA,SAAgB,WAAW,OAAsB;CAC/C,IAAI,OAAO,SAAS,YAAY;EAC9B,MAAM,SAAS,KAAK,KAAK;EACzB,MAAM,MAAM,IAAI,WAAW,OAAO,MAAM;EACxC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,IAAI,KAAK,OAAO,WAAW,CAAC;EACpE,OAAO;CACT;CACA,MAAM,MAAM,OAAO,KAAK,OAAO,QAAQ;CACvC,MAAM,MAAM,IAAI,WAAW,IAAI,MAAM;CACrC,IAAI,IAAI,GAAG;CACX,OAAO;AACT;;;;;;;;;ACtBA,eAAsB,YAAmC;CACvD,IAAI,WAAW,QAAQ,QAAQ,OAAO,WAAW,OAAO;CACxD,MAAM,EAAE,cAAc,MAAM,OAAO;CACnC,OAAO,UAAU;AACnB;;AAGA,SAAgB,eAAe,QAAyC;CACtE,MAAM,MAAM,IAAI,WAAW,MAAM;CACjC,MAAM,IAAI,WAAW;CACrB,IAAI,GAAG,iBAAiB;EACtB,EAAE,gBAAgB,GAAG;EACrB,OAAO;CACT;CACA,MAAM,IAAI,MACR,yFACF;AACF"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Passphrase used to derive the AES key. Prefer wiring this from a
|
|
6
|
-
* runtime config env var (e.g. `NUXT_ENCRYPTION_PASSPHRASE`) rather than
|
|
7
|
-
* committing it.
|
|
8
|
-
*/
|
|
9
|
-
passphrase: string;
|
|
10
|
-
/**
|
|
11
|
-
* Name under which the service is provided. Accepts `$crypto` or `crypto`
|
|
12
|
-
* — the leading `$` is stripped. Default: `$crypto`.
|
|
13
|
-
*/
|
|
14
|
-
provideName?: string;
|
|
15
|
-
/** PBKDF2 iterations. Default 100_000. */
|
|
16
|
-
iterations?: number;
|
|
17
|
-
/**
|
|
18
|
-
* Max number of derived keys held in memory. Default 64. Set to 0 to
|
|
19
|
-
* disable caching.
|
|
20
|
-
*/
|
|
21
|
-
keyCacheSize?: number;
|
|
22
|
-
/**
|
|
23
|
-
* When `true`, the runtime plugin is only registered on the server. Useful
|
|
24
|
-
* when you do not want the plaintext passphrase shipped to the browser.
|
|
25
|
-
* Default: `false`.
|
|
26
|
-
*/
|
|
27
|
-
serverOnly?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type { CryptoModuleOptions };
|
|
1
|
+
import { a as CryptoAlgorithm, c as CryptoServiceConfig, i as Bytes, o as CryptoOperationOptions, s as CryptoService, t as ParsedPayload } from "../payload-CvzYMicC.js";
|
|
2
|
+
import { t as CryptoModuleOptions } from "../index-DkMNygRG.js";
|
|
3
|
+
export type { Bytes, CryptoAlgorithm, CryptoModuleOptions, CryptoOperationOptions, CryptoService, CryptoServiceConfig, ParsedPayload };
|
package/dist/types/index.mjs
CHANGED
package/logo-dark.png
ADDED
|
Binary file
|
package/logo-light.png
ADDED
|
Binary file
|
package/logo.svg
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
|
2
|
+
<circle cx="32" cy="32" r="26" stroke="currentColor" stroke-width="2.5"/>
|
|
3
|
+
<g stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
|
4
|
+
<g transform="translate(20 32)">
|
|
5
|
+
<line x1="-4" y1="0" x2="4" y2="0"/>
|
|
6
|
+
<line x1="0" y1="-4" x2="0" y2="4"/>
|
|
7
|
+
<line x1="-3" y1="-3" x2="3" y2="3"/>
|
|
8
|
+
<line x1="3" y1="-3" x2="-3" y2="3"/>
|
|
9
|
+
</g>
|
|
10
|
+
<g transform="translate(32 32)">
|
|
11
|
+
<line x1="-4" y1="0" x2="4" y2="0"/>
|
|
12
|
+
<line x1="0" y1="-4" x2="0" y2="4"/>
|
|
13
|
+
<line x1="-3" y1="-3" x2="3" y2="3"/>
|
|
14
|
+
<line x1="3" y1="-3" x2="-3" y2="3"/>
|
|
15
|
+
</g>
|
|
16
|
+
<g transform="translate(44 32)">
|
|
17
|
+
<line x1="-4" y1="0" x2="4" y2="0"/>
|
|
18
|
+
<line x1="0" y1="-4" x2="0" y2="4"/>
|
|
19
|
+
<line x1="-3" y1="-3" x2="3" y2="3"/>
|
|
20
|
+
<line x1="3" y1="-3" x2="-3" y2="3"/>
|
|
21
|
+
</g>
|
|
22
|
+
</g>
|
|
23
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alikhalilll/nuxt-crypto",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
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",
|
|
@@ -66,6 +66,9 @@
|
|
|
66
66
|
},
|
|
67
67
|
"files": [
|
|
68
68
|
"dist",
|
|
69
|
+
"logo.svg",
|
|
70
|
+
"logo-dark.png",
|
|
71
|
+
"logo-light.png",
|
|
69
72
|
"README.md",
|
|
70
73
|
"LICENSE"
|
|
71
74
|
],
|
|
@@ -90,12 +93,16 @@
|
|
|
90
93
|
"@types/node": "^22.13.0",
|
|
91
94
|
"h3": "^1.13.0",
|
|
92
95
|
"rimraf": "^6.0.1",
|
|
93
|
-
"
|
|
94
|
-
"typescript": "^5.9.2"
|
|
96
|
+
"tsdown": "0.22.0",
|
|
97
|
+
"typescript": "^5.9.2",
|
|
98
|
+
"unrun": "0.3.0"
|
|
95
99
|
},
|
|
96
100
|
"scripts": {
|
|
97
101
|
"clean": "rimraf dist",
|
|
98
|
-
"build": "
|
|
99
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
102
|
+
"build": "tsdown",
|
|
103
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
104
|
+
"validate-dist": "tsx ../../scripts/validate/dist-validate.ts --pkg crypto",
|
|
105
|
+
"validate-consumer": "tsx ../../scripts/validate/consumer-validate.ts --pkg crypto",
|
|
106
|
+
"validate": "pnpm build && pnpm validate-dist && pnpm validate-consumer"
|
|
100
107
|
}
|
|
101
108
|
}
|
package/dist/chunk-EFZPSZWO.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//# sourceMappingURL=chunk-EFZPSZWO.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/chunk-FKHKOTFR.mjs
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
fromBase64,
|
|
3
|
-
getRandomBytes,
|
|
4
|
-
getSubtle,
|
|
5
|
-
toBase64
|
|
6
|
-
} from "./chunk-FKXZ7VYW.mjs";
|
|
7
|
-
|
|
8
|
-
// src/core/algorithms/aes-gcm.ts
|
|
9
|
-
var aesGcmPbkdf2 = {
|
|
10
|
-
version: "v1",
|
|
11
|
-
async deriveKey({ subtle, passphrase, fingerprint, salt, iterations }) {
|
|
12
|
-
const material = fingerprint ? `${passphrase}\0${fingerprint}` : passphrase;
|
|
13
|
-
const keyMaterial = await subtle.importKey(
|
|
14
|
-
"raw",
|
|
15
|
-
new TextEncoder().encode(material),
|
|
16
|
-
{ name: "PBKDF2" },
|
|
17
|
-
false,
|
|
18
|
-
["deriveKey"]
|
|
19
|
-
);
|
|
20
|
-
return subtle.deriveKey(
|
|
21
|
-
{ name: "PBKDF2", salt, iterations, hash: "SHA-256" },
|
|
22
|
-
keyMaterial,
|
|
23
|
-
{ name: "AES-GCM", length: 256 },
|
|
24
|
-
false,
|
|
25
|
-
["encrypt", "decrypt"]
|
|
26
|
-
);
|
|
27
|
-
},
|
|
28
|
-
async encrypt({ subtle, key, plainText }) {
|
|
29
|
-
const iv = new Uint8Array(new ArrayBuffer(12));
|
|
30
|
-
globalThis.crypto.getRandomValues(iv);
|
|
31
|
-
const buf = await subtle.encrypt({ name: "AES-GCM", iv }, key, plainText);
|
|
32
|
-
const cipher = new Uint8Array(new ArrayBuffer(buf.byteLength));
|
|
33
|
-
cipher.set(new Uint8Array(buf));
|
|
34
|
-
return { cipher, iv };
|
|
35
|
-
},
|
|
36
|
-
async decrypt({ subtle, key, cipher, iv }) {
|
|
37
|
-
const buf = await subtle.decrypt({ name: "AES-GCM", iv }, key, cipher);
|
|
38
|
-
const out = new Uint8Array(new ArrayBuffer(buf.byteLength));
|
|
39
|
-
out.set(new Uint8Array(buf));
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// src/core/key-cache.ts
|
|
45
|
-
var KeyCache = class {
|
|
46
|
-
constructor(capacity) {
|
|
47
|
-
this.capacity = capacity;
|
|
48
|
-
}
|
|
49
|
-
capacity;
|
|
50
|
-
map = /* @__PURE__ */ new Map();
|
|
51
|
-
static key(salt, iterations, fingerprint) {
|
|
52
|
-
return `${iterations}.${fingerprint ?? ""}.${toBase64(salt)}`;
|
|
53
|
-
}
|
|
54
|
-
get(key) {
|
|
55
|
-
const value = this.map.get(key);
|
|
56
|
-
if (!value) return void 0;
|
|
57
|
-
this.map.delete(key);
|
|
58
|
-
this.map.set(key, value);
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
set(key, value) {
|
|
62
|
-
if (this.capacity <= 0) return;
|
|
63
|
-
this.map.set(key, value);
|
|
64
|
-
while (this.map.size > this.capacity) {
|
|
65
|
-
const oldest = this.map.keys().next().value;
|
|
66
|
-
if (oldest === void 0) break;
|
|
67
|
-
this.map.delete(oldest);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
clear() {
|
|
71
|
-
this.map.clear();
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// src/core/payload.ts
|
|
76
|
-
function encodePayload(parts) {
|
|
77
|
-
return [parts.version, toBase64(parts.salt), toBase64(parts.iv), toBase64(parts.cipher)].join(
|
|
78
|
-
"."
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
function parsePayload(payload) {
|
|
82
|
-
const segments = payload.split(".");
|
|
83
|
-
if (segments.length !== 4) {
|
|
84
|
-
throw new Error("[nuxt-crypto] Invalid payload format \u2014 expected 4 dot-separated segments.");
|
|
85
|
-
}
|
|
86
|
-
const [version, saltB64, ivB64, cipherB64] = segments;
|
|
87
|
-
if (!version || !saltB64 || !ivB64 || !cipherB64) {
|
|
88
|
-
throw new Error("[nuxt-crypto] Invalid payload format \u2014 one or more segments were empty.");
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
version,
|
|
92
|
-
salt: fromBase64(saltB64),
|
|
93
|
-
iv: fromBase64(ivB64),
|
|
94
|
-
cipher: fromBase64(cipherB64)
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// src/core/create-service.ts
|
|
99
|
-
var DEFAULT_ITERATIONS = 1e5;
|
|
100
|
-
var DEFAULT_CACHE = 64;
|
|
101
|
-
var SALT_BYTES = 16;
|
|
102
|
-
async function createCryptoService(config) {
|
|
103
|
-
if (!config.passphrase) {
|
|
104
|
-
throw new Error("[nuxt-crypto] passphrase is required.");
|
|
105
|
-
}
|
|
106
|
-
const iterations = config.iterations ?? DEFAULT_ITERATIONS;
|
|
107
|
-
const algorithm = config.algorithm ?? aesGcmPbkdf2;
|
|
108
|
-
const subtle = config.subtle ?? await getSubtle();
|
|
109
|
-
const cache = new KeyCache(config.keyCacheSize ?? DEFAULT_CACHE);
|
|
110
|
-
const passphrase = String(config.passphrase).trim();
|
|
111
|
-
const encoder = new TextEncoder();
|
|
112
|
-
const decoder = new TextDecoder();
|
|
113
|
-
async function getDerivedKey(salt, fingerprint) {
|
|
114
|
-
const key = KeyCache.key(salt, iterations, fingerprint);
|
|
115
|
-
const cached = cache.get(key);
|
|
116
|
-
if (cached) return cached;
|
|
117
|
-
const pending = algorithm.deriveKey({ subtle, passphrase, fingerprint, salt, iterations });
|
|
118
|
-
cache.set(key, pending);
|
|
119
|
-
return pending;
|
|
120
|
-
}
|
|
121
|
-
return {
|
|
122
|
-
async encrypt(plainText, options) {
|
|
123
|
-
const salt = getRandomBytes(SALT_BYTES);
|
|
124
|
-
const key = await getDerivedKey(salt, options?.fingerprint);
|
|
125
|
-
const encoded = encoder.encode(plainText);
|
|
126
|
-
const plainBytes = new Uint8Array(new ArrayBuffer(encoded.byteLength));
|
|
127
|
-
plainBytes.set(encoded);
|
|
128
|
-
const { cipher, iv } = await algorithm.encrypt({ subtle, key, plainText: plainBytes });
|
|
129
|
-
return encodePayload({ version: algorithm.version, salt, iv, cipher });
|
|
130
|
-
},
|
|
131
|
-
async decrypt(payload, options) {
|
|
132
|
-
const parsed = parsePayload(payload);
|
|
133
|
-
if (parsed.version !== algorithm.version) {
|
|
134
|
-
throw new Error(
|
|
135
|
-
`[nuxt-crypto] Unsupported payload version: ${parsed.version} (algorithm expects ${algorithm.version}).`
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
const key = await getDerivedKey(parsed.salt, options?.fingerprint);
|
|
139
|
-
const plainBytes = await algorithm.decrypt({
|
|
140
|
-
subtle,
|
|
141
|
-
key,
|
|
142
|
-
cipher: parsed.cipher,
|
|
143
|
-
iv: parsed.iv
|
|
144
|
-
});
|
|
145
|
-
return decoder.decode(plainBytes);
|
|
146
|
-
},
|
|
147
|
-
clearKeyCache() {
|
|
148
|
-
cache.clear();
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export {
|
|
154
|
-
aesGcmPbkdf2,
|
|
155
|
-
KeyCache,
|
|
156
|
-
encodePayload,
|
|
157
|
-
parsePayload,
|
|
158
|
-
createCryptoService
|
|
159
|
-
};
|
|
160
|
-
//# sourceMappingURL=chunk-FKHKOTFR.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
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":[]}
|