json-seal 0.10.1 → 0.11.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
@@ -14,6 +14,8 @@
14
14
  A lightweight, zero‑dependency library for creating cryptographically signed, tamper‑proof JSON backups.
15
15
  </h3>
16
16
 
17
+ ---
18
+
17
19
  ## **Why json‑seal**
18
20
 
19
21
  Apps often need to store or transmit JSON in a way that guarantees it hasn’t been tampered with — without relying on servers, tokens, or opaque binary formats. Most security libraries focus on encrypted blobs, authentication tokens, or low‑level crypto primitives, but none solve the simple problem:
@@ -22,13 +24,53 @@ Apps often need to store or transmit JSON in a way that guarantees it hasn’t b
22
24
 
23
25
  json‑seal fills that gap. It lets you:
24
26
 
25
- - Canonicalize any JSON value
27
+ - Canonicalise any **JSON‑compatible JavaScript value** into deterministic JSON text
26
28
  - Sign it with a private key
27
29
  - Embed the public key
28
30
  - Verify integrity later
29
- - Detect any tampering — even a single character
31
+ - Detect any tampering
30
32
 
31
- It’s like JWS, but for **arbitrary JSON documents**, without JWT complexity, and designed for **offline‑first apps**, **local backups**, and **portable integrity checks**. It turns any JSON value into a **portable, human‑readable, cryptographically signed artifact** that can be verified anywhere, on any device, with no external dependencies.
33
+ It’s built for **offline‑first apps**, **local backups**, and **portable integrity checks**, where JSON must remain human‑readable and self‑verifying.
34
+
35
+ ---
36
+
37
+ ## **What json‑seal accepts**
38
+
39
+ `signPayload()` accepts any **JSON‑compatible JavaScript value**, including:
40
+
41
+ - objects
42
+ - arrays
43
+ - strings
44
+ - numbers
45
+ - booleans
46
+ - null
47
+
48
+ Typed TypeScript interfaces work automatically as long as their fields are JSON‑compatible.
49
+
50
+ ### **Rejected values**
51
+
52
+ json‑seal **does not** accept values that cannot appear in JSON:
53
+
54
+ - `undefined`
55
+ - functions
56
+ - class instances
57
+ - Dates
58
+ - Maps / Sets
59
+ - Symbols
60
+ - BigInts
61
+ - circular references
62
+ - objects containing unsupported values
63
+
64
+ These are rejected at runtime with a clear error.
65
+
66
+ ### **Important**
67
+
68
+ json‑seal signs **values**, not JSON text.
69
+
70
+ ```ts
71
+ signPayload('{"a":1}') // ❌ signs the string literally
72
+ signPayload({ a: 1 }) // ✔ signs the object
73
+ ```
32
74
 
33
75
  ---
34
76
 
@@ -55,11 +97,15 @@ Everything needed for verification is embedded:
55
97
  - signature
56
98
  - public key
57
99
 
58
- ### **Works Everywhere**
100
+ ### **Works with any JavaScript platform**
59
101
  Browsers, PWAs, Node 18+, Bun, Deno, and mobile runtimes.
60
102
 
103
+ ### **Interoperability**
104
+ json‑seal follows the WebCrypto RSA‑PSS specification (SHA‑256, saltLength = 32).
105
+ Environments built directly on OpenSSL defaults may not verify signatures unless configured to match WebCrypto’s parameters
106
+
61
107
  ### **Zero Dependencies**
62
- Small, auditable, and safe for long‑term use.
108
+ Uses the built‑in WebCrypto API (no polyfills, no external crypto libraries). Small, auditable, and safe for long‑term use.
63
109
 
64
110
  ---
65
111
 
@@ -127,7 +173,7 @@ if (result.valid) {
127
173
  Any modification — even deep inside nested objects — invalidates the signature.
128
174
 
129
175
  ```ts
130
- const tampered = { ...backup, payload: { id: 1, data: "hacked" } };
176
+ const tampered = { ...backup, payload: { id: 1, data: "modified" } };
131
177
 
132
178
  verifyBackup(tampered).valid; // false
133
179
  ```
@@ -140,7 +186,8 @@ verifyBackup(tampered).valid; // false
140
186
  Generates a 2048‑bit RSA‑PSS keypair.
141
187
 
142
188
  ### **`signPayload(payload, privateKey, publicKey)`**
143
- Canonicalizes the payload, signs it, and returns a sealed backup object.
189
+ Canonicalizes the payload, signs it, and returns a sealed backup object.
190
+ The payload must be **JSON‑compatible** (see “What json‑seal accepts”).
144
191
 
145
192
  ### **`verifyBackup(backup)`**
146
193
  Verifies the signature and returns `{ valid, payload? }`.
@@ -150,16 +197,41 @@ Full RFC 8785 Canonical JSON implementation.
150
197
 
151
198
  ---
152
199
 
153
- ## **Prior Art**
200
+ ## Prior Art
201
+
202
+ ### JSON Web Signature (JWS)
203
+
204
+ JWS is the established IETF standard for signing JSON‑related data, but it solves a very different problem. JWS is designed for **token exchange between untrusted parties** (OAuth, OpenID Connect, identity providers), not for **deterministic, portable, tamper‑evident JSON objects**.
205
+
206
+ Key differences:
207
+
208
+ - **JWS signs bytes, not JSON**
209
+ The payload must be base64url‑encoded. Two equivalent JSON objects can produce different signatures.
210
+
211
+ - **No canonicalization**
212
+ JWS does not define how JSON should be normalized. json‑seal uses deterministic canonicalization so the same logical object always produces the same signature.
213
+
214
+ - **Heavy structural overhead**
215
+ Protected headers, unprotected headers, algorithm identifiers, key IDs, and two serialization formats (compact and JSON).
154
216
 
155
- json‑seal builds on ideas from:
217
+ - **Not offline‑first**
218
+ JWS is built for network protocols. json‑seal is built for sealed backups, hash chains, and local integrity.
156
219
 
157
- - **json-canonicalize** — RFC 8785 canonicalization (no signing or backup format)
158
- - **rfc8785 (Python)** pure Python canonicalizer
159
- - **jcs (Elixir)** — Elixir implementation of JCS
160
- - **JOSE / JWS / JWT** — signing standards focused on tokens, not arbitrary JSON
220
+ - **Not WebView‑friendly**
221
+ Most JOSE libraries depend on Node’s crypto module. json‑seal uses WebCrypto and works in browsers, Ionic, Capacitor, and mobile WebViews.
161
222
 
162
- json‑seal combines **canonicalization + signing + verification** into a single, zero‑dependency library designed for **offline‑first, portable JSON integrity**.
223
+ ### In contrast
224
+
225
+ json‑seal focuses on a simpler, narrower goal:
226
+
227
+ - **Pure JSON in, pure JSON out**
228
+ - **Deterministic canonicalization**
229
+ - **WebCrypto‑based RSA‑PSS signatures**
230
+ - **Self‑contained sealed objects with embedded public keys**
231
+ - **Zero dependencies**
232
+ - **Portable across browsers, Node, Deno, Bun, and hybrid mobile apps**
233
+
234
+ It’s not a replacement for JWS — it’s a lightweight alternative for cases where you simply need to **seal JSON and verify it later**, without the complexity of JOSE.
163
235
 
164
236
  ---
165
237
 
@@ -181,11 +253,12 @@ Run tests:
181
253
  ```bash
182
254
  npm test
183
255
  ```
184
-
185
256
  ---
257
+
186
258
  Pull Requests are welcome.
259
+
187
260
  ## **License**
188
261
 
189
262
  MIT
190
263
 
191
- ---
264
+ ---
package/package.json CHANGED
@@ -1,57 +1,58 @@
1
- {
2
- "name": "json-seal",
3
- "version": "0.10.1",
4
- "description": "Create cryptographically signed, tamper‑proof JSON backups with zero dependencies.",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "import": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
13
- }
14
- },
15
- "sideEffects": false,
16
- "engines": {
17
- "node": ">=18"
18
- },
19
- "files": [
20
- "dist",
21
- "README.md",
22
- "LICENSE"
23
- ],
24
- "keywords": [
25
- "cryptography",
26
- "digital-signature",
27
- "tamper-detection",
28
- "data-integrity",
29
- "rsa-pss",
30
- "json",
31
- "json-security",
32
- "json-signing",
33
- "canonicalization",
34
- "typescript",
35
- "nodejs"
36
- ],
37
- "repository": {
38
- "type": "git",
39
- "url": "https://github.com/cmyers/json-seal.git"
40
- },
41
- "homepage": "https://github.com/cmyers/json-seal#readme",
42
- "bugs": {
43
- "url": "https://github.com/cmyers/json-seal/issues"
44
- },
45
- "scripts": {
46
- "build": "tsc",
47
- "test": "vitest",
48
- "test:watch": "vitest --watch"
49
- },
50
- "license": "MIT",
51
- "devDependencies": {
52
- "@types/node": "^25.0.6",
53
- "tsx": "^4.21.0",
54
- "typescript": "^5.9.3",
55
- "vitest": "^4.0.16"
56
- }
57
- }
1
+ {
2
+ "name": "json-seal",
3
+ "version": "0.11.1",
4
+ "author": "Chris Myers <cmyers2015@outlook.com> (https://www.npmjs.com/~cmyers-dev)",
5
+ "description": "Create cryptographically signed, tamper‑proof JSON backups with zero dependencies.",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "sideEffects": false,
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "keywords": [
26
+ "cryptography",
27
+ "digital-signature",
28
+ "tamper-detection",
29
+ "data-integrity",
30
+ "rsa-pss",
31
+ "json",
32
+ "json-security",
33
+ "json-signing",
34
+ "canonicalization",
35
+ "typescript",
36
+ "nodejs"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/cmyers/json-seal.git"
41
+ },
42
+ "homepage": "https://github.com/cmyers/json-seal#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/cmyers/json-seal/issues"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc",
48
+ "test": "vitest",
49
+ "test:watch": "vitest --watch"
50
+ },
51
+ "license": "MIT",
52
+ "devDependencies": {
53
+ "@types/node": "^25.0.6",
54
+ "tsx": "^4.21.0",
55
+ "typescript": "^5.9.3",
56
+ "vitest": "^4.0.16"
57
+ }
58
+ }
@@ -1 +0,0 @@
1
- export declare function canonicalize(obj: any): string;
@@ -1,11 +0,0 @@
1
- export function canonicalize(obj) {
2
- if (obj === null || typeof obj !== "object") {
3
- return JSON.stringify(obj);
4
- }
5
- if (Array.isArray(obj)) {
6
- return `[${obj.map(canonicalize).join(",")}]`;
7
- }
8
- const keys = Object.keys(obj).sort();
9
- const entries = keys.map(k => `"${k}":${canonicalize(obj[k])}`);
10
- return `{${entries.join(",")}}`;
11
- }
package/dist/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from "./canonicalize.js";
2
- export * from "./sign.js";
3
- export * from "./verify.js";
package/dist/index.js DELETED
@@ -1,3 +0,0 @@
1
- export * from "./canonicalize.js";
2
- export * from "./sign.js";
3
- export * from "./verify.js";
package/dist/sign.d.ts DELETED
@@ -1,14 +0,0 @@
1
- export declare function generateKeyPair(): {
2
- privateKey: string;
3
- publicKey: string;
4
- };
5
- export declare function signPayload(payload: any, privateKeyPem: string, publicKeyPem: string): {
6
- version: number;
7
- timestamp: string;
8
- payload: any;
9
- signature: {
10
- algorithm: string;
11
- publicKey: string;
12
- value: string;
13
- };
14
- };
package/dist/sign.js DELETED
@@ -1,33 +0,0 @@
1
- import { generateKeyPairSync, createSign, constants } from "crypto";
2
- import { canonicalize } from "./canonicalize.js";
3
- export function generateKeyPair() {
4
- const { privateKey, publicKey } = generateKeyPairSync("rsa", {
5
- modulusLength: 2048
6
- });
7
- return {
8
- privateKey: privateKey.export({ type: "pkcs1", format: "pem" }),
9
- publicKey: publicKey.export({ type: "spki", format: "pem" })
10
- };
11
- }
12
- export function signPayload(payload, privateKeyPem, publicKeyPem) {
13
- const canonical = canonicalize(payload);
14
- const bytes = Buffer.from(canonical, "utf8");
15
- const signer = createSign("RSA-SHA256");
16
- signer.update(bytes);
17
- signer.end();
18
- const signature = signer.sign({
19
- key: privateKeyPem,
20
- padding: constants.RSA_PKCS1_PSS_PADDING,
21
- saltLength: 32
22
- }).toString("base64");
23
- return {
24
- version: 1,
25
- timestamp: new Date().toISOString(),
26
- payload,
27
- signature: {
28
- algorithm: "RSA-PSS-SHA256",
29
- publicKey: publicKeyPem,
30
- value: signature
31
- }
32
- };
33
- }
package/dist/verify.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export declare function verifyBackup(backup: any): {
2
- valid: boolean;
3
- payload: any;
4
- };
package/dist/verify.js DELETED
@@ -1,19 +0,0 @@
1
- import { createVerify, constants } from "crypto";
2
- import { canonicalize } from "./canonicalize.js";
3
- export function verifyBackup(backup) {
4
- const { payload, signature } = backup;
5
- const canonical = canonicalize(payload);
6
- const bytes = Buffer.from(canonical, "utf8");
7
- const verifier = createVerify("RSA-SHA256");
8
- verifier.update(bytes);
9
- verifier.end();
10
- const valid = verifier.verify({
11
- key: signature.publicKey,
12
- padding: constants.RSA_PKCS1_PSS_PADDING,
13
- saltLength: 32
14
- }, Buffer.from(signature.value, "base64"));
15
- return {
16
- valid,
17
- payload: valid ? payload : undefined
18
- };
19
- }