json-seal 0.10.1 → 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
@@ -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
31
  - Detect any tampering — even a single character
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 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
+ ```
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? }`.
@@ -154,7 +201,7 @@ Full RFC 8785 Canonical JSON implementation.
154
201
 
155
202
  json‑seal builds on ideas from:
156
203
 
157
- - **json-canonicalize** — RFC 8785 canonicalization (no signing or backup format)
204
+ - **json-canonicalize** — RFC 8785 canonicalization
158
205
  - **rfc8785 (Python)** — pure Python canonicalizer
159
206
  - **jcs (Elixir)** — Elixir implementation of JCS
160
207
  - **JOSE / JWS / JWT** — signing standards focused on tokens, not arbitrary JSON
@@ -183,7 +230,9 @@ npm test
183
230
  ```
184
231
 
185
232
  ---
233
+
186
234
  Pull Requests are welcome.
235
+
187
236
  ## **License**
188
237
 
189
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.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.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
+ }