zeyra 1.0.0 → 1.1.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/README.md +113 -69
- package/package.json +20 -6
- package/src/CipherCluster/class.js +57 -0
- package/src/SigningCluster/class.js +34 -0
- package/src/VerificationCluster/class.js +35 -0
- package/src/index.js +12 -1
package/README.md
CHANGED
|
@@ -1,69 +1,113 @@
|
|
|
1
|
-
# Zeyra
|
|
2
|
-
|
|
3
|
-
WebCrypto helpers for
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from "
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
# Zeyra
|
|
2
|
+
|
|
3
|
+
Managed WebCrypto helpers for storage-ready AES-GCM + ECDSA keysets, with lightweight agents and weakly cached clusters.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- AES-GCM 256 encryption/decryption via `CipherAgent`
|
|
8
|
+
- ECDSA P-256 signing/verification via `SigningAgent` and `VerificationAgent`
|
|
9
|
+
- Managed clusters (`CipherCluster`, `SigningCluster`, `VerificationCluster`) cache agents with WeakRef for large keysets
|
|
10
|
+
- `generateKeyset()` produces an exportable JWK bundle you can store or transport
|
|
11
|
+
- Storage/transport-ready artifacts with base64url payloads and SHA-256 digests
|
|
12
|
+
- Pure WebCrypto, no native add-ons; ships as ESM
|
|
13
|
+
- Works with `bytecodec` for UTF-8, compression, and base64url conversions
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Node.js 18+ (global `crypto.subtle`)
|
|
18
|
+
- ESM environment (`"type": "module"` in `package.json`)
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install zeyra
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quickstart (agents)
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { Bytes } from "bytecodec";
|
|
30
|
+
import {
|
|
31
|
+
generateKeyset,
|
|
32
|
+
CipherAgent,
|
|
33
|
+
SigningAgent,
|
|
34
|
+
VerificationAgent,
|
|
35
|
+
} from "zeyra";
|
|
36
|
+
|
|
37
|
+
// One-time key material for a resource
|
|
38
|
+
const { symmetricJwk, privateJwk, publicJwk } = await generateKeyset();
|
|
39
|
+
|
|
40
|
+
// Writers: encrypt + sign
|
|
41
|
+
const cipher = new CipherAgent(symmetricJwk);
|
|
42
|
+
const signer = new SigningAgent(privateJwk);
|
|
43
|
+
const payload = await cipher.encrypt(Bytes.fromString("hello world"));
|
|
44
|
+
const signature = await signer.sign(payload.ciphertext);
|
|
45
|
+
|
|
46
|
+
// Readers / servers: verify ownership + decrypt
|
|
47
|
+
const verifier = new VerificationAgent(publicJwk);
|
|
48
|
+
const authorized = await verifier.verify(payload.ciphertext, signature);
|
|
49
|
+
const plaintext = Bytes.toString(await cipher.decrypt(payload));
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Managed cluster flow
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import {
|
|
56
|
+
generateKeyset,
|
|
57
|
+
CipherCluster,
|
|
58
|
+
SigningCluster,
|
|
59
|
+
VerificationCluster,
|
|
60
|
+
} from "zeyra";
|
|
61
|
+
|
|
62
|
+
const { symmetricJwk, privateJwk, publicJwk } = await generateKeyset();
|
|
63
|
+
|
|
64
|
+
const resource = { id: "file-123", body: "hello world" };
|
|
65
|
+
const artifact = await CipherCluster.encrypt(symmetricJwk, resource);
|
|
66
|
+
const signature = await SigningCluster.sign(privateJwk, resource.id);
|
|
67
|
+
|
|
68
|
+
// VerificationCluster is designed to run on a per-resource server node.
|
|
69
|
+
const authorized = await VerificationCluster.verify(
|
|
70
|
+
publicJwk,
|
|
71
|
+
resource.id,
|
|
72
|
+
signature
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const decrypted = await CipherCluster.decrypt(symmetricJwk, artifact);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## API
|
|
79
|
+
|
|
80
|
+
- `generateKeyset()` -> `{ symmetricJwk, publicJwk, privateJwk }` (all exportable JWKs)
|
|
81
|
+
- `new CipherAgent(symmetricJwk)`
|
|
82
|
+
- `.encrypt(Uint8Array)` -> `{ iv: Uint8Array, ciphertext: ArrayBuffer }`
|
|
83
|
+
- `.decrypt({ iv, ciphertext })` -> `Uint8Array`
|
|
84
|
+
- `new SigningAgent(privateJwk)`
|
|
85
|
+
- `.sign(Uint8Array | ArrayBuffer)` -> `ArrayBuffer` (ECDSA P-256 / SHA-256)
|
|
86
|
+
- `new VerificationAgent(publicJwk)`
|
|
87
|
+
- `.verify(Uint8Array | ArrayBuffer, ArrayBuffer)` -> `boolean`
|
|
88
|
+
- `CipherCluster.encrypt(symmetricJwk, resource)`
|
|
89
|
+
- -> `{ digest, ciphertext, iv }` (all base64url strings; digest is SHA-256 of JSON bytes, pre-encryption, useful for version checks)
|
|
90
|
+
- `CipherCluster.decrypt(symmetricJwk, artifact)`
|
|
91
|
+
- -> `{ digest, ...resource }` (resource object restored from compressed JSON)
|
|
92
|
+
- `SigningCluster.sign(privateJwk, value)` -> `Base64URLString`
|
|
93
|
+
- `VerificationCluster.verify(publicJwk, value, signature)` -> `boolean`
|
|
94
|
+
|
|
95
|
+
See the implementations in `src/index.js` and friends for details.
|
|
96
|
+
|
|
97
|
+
## Testing and benchmarks
|
|
98
|
+
|
|
99
|
+
- Run tests: `npm test` (uses Node's built-in `node:test` runner against `test.js`)
|
|
100
|
+
- Run microbenchmarks (skipped by default): `npm run bench`
|
|
101
|
+
- Pass iterations: `npm run bench -- --iterations=500`
|
|
102
|
+
- Reports ops/sec for encryption and the full encrypt/sign/verify/decrypt pipeline.
|
|
103
|
+
|
|
104
|
+
## Notes
|
|
105
|
+
|
|
106
|
+
- CipherCluster assumes one unique random key per resource (no derivations or shared usage).
|
|
107
|
+
- Cluster classes are intended for client-side usage; `VerificationCluster`/`VerificationAgent` can be hosted per-resource to pre-verify access before downstream identity or ACL checks.
|
|
108
|
+
- Cluster serialization uses JSON and adds a `digest` field; avoid using `digest` in resource objects.
|
|
109
|
+
- WeakRef caching keeps memory usage loose and GC-friendly by design.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeyra",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "WebCrypto
|
|
5
|
-
"main": "src/index.js",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Managed WebCrypto helpers for AES-GCM + ECDSA JWK keysets with storage-ready cluster APIs.",
|
|
6
5
|
"keywords": [
|
|
7
6
|
"webcrypto",
|
|
8
7
|
"aes-gcm",
|
|
9
8
|
"ecdsa",
|
|
10
9
|
"jwk",
|
|
11
10
|
"encryption",
|
|
12
|
-
"signature"
|
|
11
|
+
"signature",
|
|
12
|
+
"cluster",
|
|
13
|
+
"keyset",
|
|
14
|
+
"base64url",
|
|
15
|
+
"compression",
|
|
16
|
+
"storage",
|
|
17
|
+
"transport"
|
|
13
18
|
],
|
|
14
19
|
"license": "MIT",
|
|
15
20
|
"type": "module",
|
|
21
|
+
"main": "src/index.js",
|
|
16
22
|
"scripts": {
|
|
17
|
-
"test": "node --test test.js",
|
|
23
|
+
"test": "node --test test.js && node test.js --bench",
|
|
18
24
|
"bench": "node test.js --bench",
|
|
19
25
|
"prepublishOnly": "npm test"
|
|
20
26
|
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/jortsupetterson/zeyra.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/jortsupetterson/zeyra/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/jortsupetterson/zeyra#readme",
|
|
21
35
|
"dependencies": {
|
|
22
|
-
"bytecodec": "^1.
|
|
36
|
+
"bytecodec": "^1.3.0"
|
|
23
37
|
},
|
|
24
38
|
"engines": {
|
|
25
39
|
"node": ">=18"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Bytes } from "bytecodec";
|
|
2
|
+
import { CipherAgent } from "../CipherAgent/class.js";
|
|
3
|
+
|
|
4
|
+
export class CipherCluster {
|
|
5
|
+
/** @type {WeakMap<JsonWebKey, WeakRef<CipherAgent>>} */
|
|
6
|
+
static #agents = new WeakMap();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {JsonWebKey} symmetricJwk
|
|
10
|
+
* @returns {CipherAgent}
|
|
11
|
+
*/
|
|
12
|
+
static #loadAgent(symmetricJwk) {
|
|
13
|
+
const weakRef = CipherCluster.#agents.get(symmetricJwk);
|
|
14
|
+
/** @type {CipherAgent | undefined} */
|
|
15
|
+
let agent = weakRef?.deref();
|
|
16
|
+
if (!agent) {
|
|
17
|
+
agent = new CipherAgent(symmetricJwk);
|
|
18
|
+
CipherCluster.#agents.set(symmetricJwk, new WeakRef(agent));
|
|
19
|
+
}
|
|
20
|
+
return agent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {JsonWebKey} symmetricJwk
|
|
25
|
+
* @param {any} resource
|
|
26
|
+
* @returns {Promise<{ digest: Base64URLString, ciphertext: Base64URLString, iv: Base64URLString }>}
|
|
27
|
+
*/
|
|
28
|
+
static async encrypt(symmetricJwk, resource) {
|
|
29
|
+
const agent = CipherCluster.#loadAgent(symmetricJwk);
|
|
30
|
+
const bytes = Bytes.fromJSON(resource);
|
|
31
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
32
|
+
const compressed = await Bytes.toCompressed(bytes);
|
|
33
|
+
const envelope = await agent.encrypt(compressed);
|
|
34
|
+
return {
|
|
35
|
+
digest: Bytes.toBase64UrlString(digest),
|
|
36
|
+
ciphertext: Bytes.toBase64UrlString(envelope.ciphertext),
|
|
37
|
+
iv: Bytes.toBase64UrlString(envelope.iv),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {JsonWebKey} symmetricJwk
|
|
43
|
+
* @param {{ digest: Base64URLString, ciphertext: Base64URLString, iv: Base64URLString }} artifact
|
|
44
|
+
* @returns {Promise<any>}
|
|
45
|
+
*/
|
|
46
|
+
static async decrypt(symmetricJwk, artifact) {
|
|
47
|
+
const envelope = {
|
|
48
|
+
ciphertext: Bytes.fromBase64UrlString(artifact.ciphertext),
|
|
49
|
+
iv: Bytes.fromBase64UrlString(artifact.iv),
|
|
50
|
+
};
|
|
51
|
+
const agent = CipherCluster.#loadAgent(symmetricJwk);
|
|
52
|
+
const bytes = await agent.decrypt(envelope);
|
|
53
|
+
const decompressed = await Bytes.fromCompressed(bytes);
|
|
54
|
+
const resource = Bytes.toJSON(decompressed);
|
|
55
|
+
return { digest: artifact.digest, ...resource };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Bytes } from "bytecodec";
|
|
2
|
+
import { SigningAgent } from "../SigningAgent/class.js";
|
|
3
|
+
|
|
4
|
+
export class SigningCluster {
|
|
5
|
+
/** @type {WeakMap<JsonWebKey, WeakRef<SigningAgent>>} */
|
|
6
|
+
static #agents = new WeakMap();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {JsonWebKey} privateJwk
|
|
10
|
+
* @returns {SigningAgent}
|
|
11
|
+
*/
|
|
12
|
+
static #loadAgent(privateJwk) {
|
|
13
|
+
const weakRef = SigningCluster.#agents.get(privateJwk);
|
|
14
|
+
/** @type {SigningAgent | undefined} */
|
|
15
|
+
let agent = weakRef?.deref();
|
|
16
|
+
if (!agent) {
|
|
17
|
+
agent = new SigningAgent(privateJwk);
|
|
18
|
+
SigningCluster.#agents.set(privateJwk, new WeakRef(agent));
|
|
19
|
+
}
|
|
20
|
+
return agent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {JsonWebKey} privateJwk
|
|
25
|
+
* @param {any} value
|
|
26
|
+
* @returns {Promise<Base64URLString>}
|
|
27
|
+
*/
|
|
28
|
+
static async sign(privateJwk, value) {
|
|
29
|
+
const agent = SigningCluster.#loadAgent(privateJwk);
|
|
30
|
+
const bytes = Bytes.fromJSON(value);
|
|
31
|
+
const signature = await agent.sign(bytes);
|
|
32
|
+
return Bytes.toBase64UrlString(signature);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Bytes } from "bytecodec";
|
|
2
|
+
import { VerificationAgent } from "../VerificationAgent/class.js";
|
|
3
|
+
|
|
4
|
+
export class VerificationCluster {
|
|
5
|
+
/** @type {WeakMap<JsonWebKey, WeakRef<VerificationAgent>>} */
|
|
6
|
+
static #agents = new WeakMap();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {JsonWebKey} publicJwk
|
|
10
|
+
* @returns {VerificationAgent}
|
|
11
|
+
*/
|
|
12
|
+
static #loadAgent(publicJwk) {
|
|
13
|
+
const weakRef = VerificationCluster.#agents.get(publicJwk);
|
|
14
|
+
/** @type {VerificationAgent | undefined} */
|
|
15
|
+
let agent = weakRef?.deref();
|
|
16
|
+
if (!agent) {
|
|
17
|
+
agent = new VerificationAgent(publicJwk);
|
|
18
|
+
VerificationCluster.#agents.set(publicJwk, new WeakRef(agent));
|
|
19
|
+
}
|
|
20
|
+
return agent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {JsonWebKey} publicJwk
|
|
25
|
+
* @param {any} value
|
|
26
|
+
* @param {Base64URLString} signature
|
|
27
|
+
* @returns {Promise<boolean>}
|
|
28
|
+
*/
|
|
29
|
+
static async verify(publicJwk, value, signature) {
|
|
30
|
+
const agent = VerificationCluster.#loadAgent(publicJwk);
|
|
31
|
+
const valueBytes = Bytes.fromJSON(value);
|
|
32
|
+
const signatureBytes = Bytes.fromBase64UrlString(signature);
|
|
33
|
+
return await agent.verify(valueBytes, signatureBytes);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,4 +2,15 @@ import { generateKeyset } from "./generateKeyset/index.js";
|
|
|
2
2
|
import { CipherAgent } from "./CipherAgent/class.js";
|
|
3
3
|
import { SigningAgent } from "./SigningAgent/class.js";
|
|
4
4
|
import { VerificationAgent } from "./VerificationAgent/class.js";
|
|
5
|
-
|
|
5
|
+
import { CipherCluster } from "./CipherCluster/class.js";
|
|
6
|
+
import { SigningCluster } from "./SigningCluster/class.js";
|
|
7
|
+
import { VerificationCluster } from "./VerificationCluster/class.js";
|
|
8
|
+
export {
|
|
9
|
+
generateKeyset,
|
|
10
|
+
CipherAgent,
|
|
11
|
+
SigningAgent,
|
|
12
|
+
VerificationAgent,
|
|
13
|
+
CipherCluster,
|
|
14
|
+
SigningCluster,
|
|
15
|
+
VerificationCluster,
|
|
16
|
+
};
|