json-seal 0.10.0 → 0.11.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 CHANGED
@@ -1,13 +1,18 @@
1
- # **jsonseal**
2
- ![npm version](https://img.shields.io/npm/v/json-seal)
3
- ![Deterministic JSON](https://img.shields.io/badge/Deterministic%20JSON-RFC%208785%20Compliant-success)
4
- ![crypto](https://img.shields.io/badge/crypto-RSA--PSS-green)
5
- ![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
6
- ![types](https://img.shields.io/badge/types-TypeScript-blue)
7
- ![bundle size](https://img.shields.io/bundlephobia/minzip/json-seal)
8
- ![npm downloads](https://img.shields.io/npm/dm/json-seal)
9
-
10
- **A lightweight, zero‑dependency library for creating cryptographically signed, tamper‑proof JSON backups.**
1
+ <h1 align="center">json-seal</h1>
2
+
3
+ <p align="center">
4
+ <img src="https://github.com/cmyers/json-seal/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI" />
5
+ <img src="https://img.shields.io/npm/v/json-seal" alt="npm version" />
6
+ <img src="https://img.shields.io/badge/Deterministic%20JSON-RFC%208785%20Compliant-success" alt="Deterministic JSON" />
7
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="dependencies" />
8
+ <img src="https://img.shields.io/badge/types-TypeScript-blue" alt="types" />
9
+ <img src="https://img.shields.io/bundlephobia/minzip/json-seal" alt="bundle size" />
10
+ <img src="https://img.shields.io/github/license/cmyers/json-seal" alt="license" />
11
+ </p>
12
+
13
+ <h3 align="center">
14
+ A lightweight, zero‑dependency library for creating cryptographically signed, tamper‑proof JSON backups.
15
+ </h3>
11
16
 
12
17
  ---
13
18
 
@@ -19,13 +24,53 @@ Apps often need to store or transmit JSON in a way that guarantees it hasn’t b
19
24
 
20
25
  json‑seal fills that gap. It lets you:
21
26
 
22
- - Canonicalize any JSON value
27
+ - Canonicalise any **JSON‑compatible JavaScript value** into deterministic JSON text
23
28
  - Sign it with a private key
24
29
  - Embed the public key
25
30
  - Verify integrity later
26
31
  - Detect any tampering — even a single character
27
32
 
28
- 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 like JWS, but for **arbitrary JSON documents**, without JWT complexity, and designed for **offline‑first apps**, **local backups**, and **portable integrity checks**.
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
+ ```
29
74
 
30
75
  ---
31
76
 
@@ -52,11 +97,15 @@ Everything needed for verification is embedded:
52
97
  - signature
53
98
  - public key
54
99
 
55
- ### **Works Everywhere**
100
+ ### **Works with any JavaScript platform**
56
101
  Browsers, PWAs, Node 18+, Bun, Deno, and mobile runtimes.
57
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
+
58
107
  ### **Zero Dependencies**
59
- 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.
60
109
 
61
110
  ---
62
111
 
@@ -124,7 +173,7 @@ if (result.valid) {
124
173
  Any modification — even deep inside nested objects — invalidates the signature.
125
174
 
126
175
  ```ts
127
- const tampered = { ...backup, payload: { id: 1, data: "hacked" } };
176
+ const tampered = { ...backup, payload: { id: 1, data: "modified" } };
128
177
 
129
178
  verifyBackup(tampered).valid; // false
130
179
  ```
@@ -137,7 +186,8 @@ verifyBackup(tampered).valid; // false
137
186
  Generates a 2048‑bit RSA‑PSS keypair.
138
187
 
139
188
  ### **`signPayload(payload, privateKey, publicKey)`**
140
- 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”).
141
191
 
142
192
  ### **`verifyBackup(backup)`**
143
193
  Verifies the signature and returns `{ valid, payload? }`.
@@ -151,7 +201,7 @@ Full RFC 8785 Canonical JSON implementation.
151
201
 
152
202
  json‑seal builds on ideas from:
153
203
 
154
- - **json-canonicalize** — RFC 8785 canonicalization (no signing or backup format)
204
+ - **json-canonicalize** — RFC 8785 canonicalization
155
205
  - **rfc8785 (Python)** — pure Python canonicalizer
156
206
  - **jcs (Elixir)** — Elixir implementation of JCS
157
207
  - **JOSE / JWS / JWT** — signing standards focused on tokens, not arbitrary JSON
@@ -180,7 +230,9 @@ npm test
180
230
  ```
181
231
 
182
232
  ---
233
+
183
234
  Pull Requests are welcome.
235
+
184
236
  ## **License**
185
237
 
186
238
  MIT
@@ -1 +1,5 @@
1
- export declare function canonicalize(obj: any): string;
1
+ /**
2
+ * RFC 8785 Canonical JSON implementation
3
+ * Deterministic, strict, and cross‑runtime stable.
4
+ */
5
+ export declare function canonicalize(value: any): string;
@@ -1,11 +1,122 @@
1
- export function canonicalize(obj) {
2
- if (obj === null || typeof obj !== "object") {
3
- return JSON.stringify(obj);
1
+ /**
2
+ * RFC 8785 Canonical JSON implementation
3
+ * Deterministic, strict, and cross‑runtime stable.
4
+ */
5
+ export function canonicalize(value) {
6
+ switch (typeof value) {
7
+ case "string":
8
+ return escapeString(value);
9
+ case "number":
10
+ return serializeNumber(value);
11
+ case "boolean":
12
+ return value ? "true" : "false";
13
+ case "object":
14
+ if (value === null)
15
+ return "null";
16
+ if (Array.isArray(value))
17
+ return serializeArray(value);
18
+ return serializeObject(value);
19
+ default:
20
+ throw new Error(`Unsupported type in canonical JSON: ${typeof value}`);
4
21
  }
5
- if (Array.isArray(obj)) {
6
- return `[${obj.map(canonicalize).join(",")}]`;
22
+ }
23
+ /* -------------------------------------------------------------------------- */
24
+ /* Strings */
25
+ /* -------------------------------------------------------------------------- */
26
+ function escapeString(str) {
27
+ // RFC 8785 uses ECMAScript string escaping rules.
28
+ let out = '"';
29
+ for (let i = 0; i < str.length; i++) {
30
+ const c = str.charCodeAt(i);
31
+ switch (c) {
32
+ case 0x22:
33
+ out += '\\"';
34
+ break; // "
35
+ case 0x5C:
36
+ out += '\\\\';
37
+ break; // \
38
+ case 0x08:
39
+ out += '\\b';
40
+ break;
41
+ case 0x0C:
42
+ out += '\\f';
43
+ break;
44
+ case 0x0A:
45
+ out += '\\n';
46
+ break;
47
+ case 0x0D:
48
+ out += '\\r';
49
+ break;
50
+ case 0x09:
51
+ out += '\\t';
52
+ break;
53
+ default:
54
+ if (c < 0x20) {
55
+ // Control characters → \u00XX
56
+ out += "\\u" + hex4(c);
57
+ }
58
+ else {
59
+ out += str[i];
60
+ }
61
+ }
62
+ }
63
+ return out + '"';
64
+ }
65
+ function hex4(n) {
66
+ return n.toString(16).padStart(4, "0");
67
+ }
68
+ /* -------------------------------------------------------------------------- */
69
+ /* Numbers */
70
+ /* -------------------------------------------------------------------------- */
71
+ function serializeNumber(n) {
72
+ if (!Number.isFinite(n)) {
73
+ throw new Error("Non‑finite numbers are not permitted in canonical JSON");
74
+ }
75
+ // RFC 8785: -0 must be serialized as 0
76
+ if (Object.is(n, -0))
77
+ return "0";
78
+ // Use JS number → string, then normalize exponent form
79
+ let s = n.toString();
80
+ // Normalize exponent to uppercase E
81
+ if (s.includes("e")) {
82
+ const [mantissa, exp] = s.split("e");
83
+ const sign = exp.startsWith("-") ? "-" : "+";
84
+ let digits = exp.replace(/^[+-]/, "");
85
+ // Remove leading zeros in exponent
86
+ digits = digits.replace(/^0+/, "");
87
+ if (digits === "")
88
+ digits = "0";
89
+ // RFC 8785: exponent must not include "+"
90
+ const normalized = `${mantissa}E${sign === "-" ? "-" : ""}${digits}`;
91
+ return normalized;
7
92
  }
8
- const keys = Object.keys(obj).sort();
9
- const entries = keys.map(k => `"${k}":${canonicalize(obj[k])}`);
93
+ return s;
94
+ }
95
+ /* -------------------------------------------------------------------------- */
96
+ /* Arrays */
97
+ /* -------------------------------------------------------------------------- */
98
+ function serializeArray(arr) {
99
+ const items = arr.map(canonicalize);
100
+ return `[${items.join(",")}]`;
101
+ }
102
+ /* -------------------------------------------------------------------------- */
103
+ /* Objects */
104
+ /* -------------------------------------------------------------------------- */
105
+ function serializeObject(obj) {
106
+ const keys = Object.keys(obj);
107
+ // RFC 8785: duplicate keys MUST be rejected
108
+ detectDuplicateKeys(keys);
109
+ // Sort by UTF‑16 code units (JS default)
110
+ keys.sort();
111
+ const entries = keys.map(k => `${escapeString(k)}:${canonicalize(obj[k])}`);
10
112
  return `{${entries.join(",")}}`;
11
113
  }
114
+ function detectDuplicateKeys(keys) {
115
+ const seen = new Set();
116
+ for (const k of keys) {
117
+ if (seen.has(k)) {
118
+ throw new Error(`Duplicate key in object: ${k}`);
119
+ }
120
+ seen.add(k);
121
+ }
122
+ }
package/package.json CHANGED
@@ -1,57 +1,58 @@
1
- {
2
- "name": "json-seal",
3
- "version": "0.10.0",
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.0",
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
+ }