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 +69 -17
- package/dist/canonicalize.d.ts +5 -1
- package/dist/canonicalize.js +118 -7
- package/package.json +58 -57
package/README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
-
|
|
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**.
|
|
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
|
|
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: "
|
|
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
|
|
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
|
package/dist/canonicalize.d.ts
CHANGED
package/dist/canonicalize.js
CHANGED
|
@@ -1,11 +1,122 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"json
|
|
32
|
-
"json-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
"test
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
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
|
+
}
|