@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/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/index.d.ts +914 -0
- package/dist/index.js +1213 -0
- package/dist/node.d.ts +78 -0
- package/dist/node.js +112 -0
- package/package.json +43 -0
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
|
+
}
|