@arkstack/encryption 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ //#region src/node.d.ts
2
+ /**
3
+ * Key representations the synchronous Node cipher accepts.
4
+ */
5
+ type NodeKeyInput = Uint8Array | string;
6
+ /**
7
+ * Synchronous AES-256-GCM for Node, wire compatible with {@link Cipher}.
8
+ *
9
+ * The Web Crypto API is asynchronous everywhere, which is the right default but
10
+ * a breaking change for code that already calls `Encryption.encrypt()` inline.
11
+ * This entry point keeps that synchronous surface available on the server while
12
+ * emitting the exact same `<iv>:<authTag>:<ciphertext>` payloads, so anything
13
+ * encrypted here decrypts in a browser with `@arkstack/encryption` and vice
14
+ * versa.
15
+ *
16
+ * Import it from `@arkstack/encryption/node`; it is deliberately kept out of
17
+ * the main entry point so browser bundles never pull in `node:crypto`.
18
+ */
19
+ declare class NodeCipher {
20
+ /** The cipher algorithm, matching the Web Crypto implementation. */
21
+ static readonly algorithm = "aes-256-gcm";
22
+ /**
23
+ * Encrypt a string.
24
+ *
25
+ * @param value
26
+ * @param key
27
+ * @returns
28
+ */
29
+ static encrypt(value: string, key: NodeKeyInput): string;
30
+ /**
31
+ * Decrypt a payload produced by {@link encrypt} or by the isomorphic
32
+ * `Cipher`.
33
+ *
34
+ * @param payload
35
+ * @param key
36
+ * @returns
37
+ */
38
+ static decrypt(payload: string, key: NodeKeyInput): string;
39
+ /**
40
+ * Generate a random base64url key.
41
+ *
42
+ * @param length
43
+ * @returns
44
+ */
45
+ static generateKey(length?: number): string;
46
+ /**
47
+ * Hash an arbitrary secret into 32 bytes of key material with SHA-256.
48
+ *
49
+ * Byte for byte identical to `EncryptionKey.fromSecret()`, and unlike
50
+ * {@link resolve} it never treats the secret as raw key material — which
51
+ * matters for values such as `APP_KEY` that happen to be base64url of
52
+ * exactly the key length.
53
+ *
54
+ * @param secret
55
+ * @returns
56
+ */
57
+ static fromSecret(secret: string): Uint8Array;
58
+ /**
59
+ * Constant time comparison of two keys.
60
+ *
61
+ * @param left
62
+ * @param right
63
+ * @returns
64
+ */
65
+ static compare(left: NodeKeyInput, right: NodeKeyInput): boolean;
66
+ /**
67
+ * Turn any accepted representation into 32 bytes of key material, using the
68
+ * same rules as the isomorphic implementation: a base64url string that
69
+ * decodes to exactly the key length is raw material, anything else is a
70
+ * passphrase hashed with SHA-256.
71
+ *
72
+ * @param key
73
+ * @returns
74
+ */
75
+ static resolve(key: NodeKeyInput): Uint8Array;
76
+ }
77
+ //#endregion
78
+ export { NodeCipher, NodeKeyInput };
package/dist/node.js ADDED
@@ -0,0 +1,112 @@
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ //#region src/node.ts
3
+ const ALGORITHM = "aes-256-gcm";
4
+ const IV_LENGTH = 12;
5
+ const KEY_LENGTH = 32;
6
+ /**
7
+ * Synchronous AES-256-GCM for Node, wire compatible with {@link Cipher}.
8
+ *
9
+ * The Web Crypto API is asynchronous everywhere, which is the right default but
10
+ * a breaking change for code that already calls `Encryption.encrypt()` inline.
11
+ * This entry point keeps that synchronous surface available on the server while
12
+ * emitting the exact same `<iv>:<authTag>:<ciphertext>` payloads, so anything
13
+ * encrypted here decrypts in a browser with `@arkstack/encryption` and vice
14
+ * versa.
15
+ *
16
+ * Import it from `@arkstack/encryption/node`; it is deliberately kept out of
17
+ * the main entry point so browser bundles never pull in `node:crypto`.
18
+ */
19
+ var NodeCipher = class {
20
+ /** The cipher algorithm, matching the Web Crypto implementation. */
21
+ static algorithm = ALGORITHM;
22
+ /**
23
+ * Encrypt a string.
24
+ *
25
+ * @param value
26
+ * @param key
27
+ * @returns
28
+ */
29
+ static encrypt(value, key) {
30
+ const iv = randomBytes(IV_LENGTH);
31
+ const cipher = createCipheriv(ALGORITHM, this.resolve(key), iv);
32
+ const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
33
+ return [
34
+ iv,
35
+ cipher.getAuthTag(),
36
+ ciphertext
37
+ ].map((part) => part.toString("base64url")).join(":");
38
+ }
39
+ /**
40
+ * Decrypt a payload produced by {@link encrypt} or by the isomorphic
41
+ * `Cipher`.
42
+ *
43
+ * @param payload
44
+ * @param key
45
+ * @returns
46
+ */
47
+ static decrypt(payload, key) {
48
+ const [iv, authTag, ciphertext] = payload.split(":");
49
+ if (!iv || !authTag || ciphertext === void 0) throw new Error("Invalid encrypted payload format");
50
+ const decipher = createDecipheriv(ALGORITHM, this.resolve(key), Buffer.from(iv, "base64url"));
51
+ decipher.setAuthTag(Buffer.from(authTag, "base64url"));
52
+ return Buffer.concat([decipher.update(Buffer.from(ciphertext, "base64url")), decipher.final()]).toString("utf8");
53
+ }
54
+ /**
55
+ * Generate a random base64url key.
56
+ *
57
+ * @param length
58
+ * @returns
59
+ */
60
+ static generateKey(length = KEY_LENGTH) {
61
+ return randomBytes(length).toString("base64url");
62
+ }
63
+ /**
64
+ * Hash an arbitrary secret into 32 bytes of key material with SHA-256.
65
+ *
66
+ * Byte for byte identical to `EncryptionKey.fromSecret()`, and unlike
67
+ * {@link resolve} it never treats the secret as raw key material — which
68
+ * matters for values such as `APP_KEY` that happen to be base64url of
69
+ * exactly the key length.
70
+ *
71
+ * @param secret
72
+ * @returns
73
+ */
74
+ static fromSecret(secret) {
75
+ return createHash("sha256").update(secret).digest();
76
+ }
77
+ /**
78
+ * Constant time comparison of two keys.
79
+ *
80
+ * @param left
81
+ * @param right
82
+ * @returns
83
+ */
84
+ static compare(left, right) {
85
+ try {
86
+ const a = this.resolve(left);
87
+ const b = this.resolve(right);
88
+ return a.length === b.length && timingSafeEqual(a, b);
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+ /**
94
+ * Turn any accepted representation into 32 bytes of key material, using the
95
+ * same rules as the isomorphic implementation: a base64url string that
96
+ * decodes to exactly the key length is raw material, anything else is a
97
+ * passphrase hashed with SHA-256.
98
+ *
99
+ * @param key
100
+ * @returns
101
+ */
102
+ static resolve(key) {
103
+ if (typeof key !== "string") return Buffer.from(key);
104
+ if (/^[A-Za-z0-9_-]+$/.test(key)) {
105
+ const decoded = Buffer.from(key, "base64url");
106
+ if (decoded.length === KEY_LENGTH && decoded.toString("base64url") === key) return decoded;
107
+ }
108
+ return createHash("sha256").update(key).digest();
109
+ }
110
+ };
111
+ //#endregion
112
+ export { NodeCipher };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@arkstack/encryption",
3
+ "version": "0.18.0",
4
+ "type": "module",
5
+ "description": "Isomorphic end-to-end encryption for Arkstack: AES-256-GCM ciphers, ECDH key pairs, and key generation/comparison utilities that run identically in Node and the browser.",
6
+ "homepage": "https://arkstack.toneflix.net/guide/utilities/encryption",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/arkstack-hq/arkstack.git",
10
+ "directory": "packages/encryption"
11
+ },
12
+ "keywords": [
13
+ "encryption",
14
+ "e2ee",
15
+ "end-to-end",
16
+ "aes-gcm",
17
+ "ecdh",
18
+ "webcrypto",
19
+ "isomorphic",
20
+ "cryptography",
21
+ "arkstack"
22
+ ],
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "sideEffects": false,
30
+ "exports": {
31
+ ".": "./dist/index.js",
32
+ "./node": "./dist/node.js",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.6.2"
37
+ },
38
+ "scripts": {
39
+ "build": "tsdown",
40
+ "test": "vitest",
41
+ "version:patch": "pnpm version patch"
42
+ }
43
+ }