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 +56 -7
- package/dist/canonicalize.d.ts +5 -1
- package/dist/canonicalize.js +118 -7
- package/package.json +58 -57
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
|
-
-
|
|
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**.
|
|
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
|
|
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: "
|
|
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
|
|
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
|
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
|
+
}
|