json-seal 0.11.0 → 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 +35 -11
- package/package.json +1 -1
- package/dist/canonicalize.d.ts +0 -5
- package/dist/canonicalize.js +0 -122
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/dist/sign.d.ts +0 -14
- package/dist/sign.js +0 -33
- package/dist/verify.d.ts +0 -4
- package/dist/verify.js +0 -19
package/README.md
CHANGED
|
@@ -28,9 +28,9 @@ json‑seal fills that gap. It lets you:
|
|
|
28
28
|
- Sign it with a private key
|
|
29
29
|
- Embed the public key
|
|
30
30
|
- Verify integrity later
|
|
31
|
-
- Detect any tampering
|
|
31
|
+
- Detect any tampering
|
|
32
32
|
|
|
33
|
-
It’s
|
|
33
|
+
It’s built for **offline‑first apps**, **local backups**, and **portable integrity checks**, where JSON must remain human‑readable and self‑verifying.
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -197,16 +197,41 @@ Full RFC 8785 Canonical JSON implementation.
|
|
|
197
197
|
|
|
198
198
|
---
|
|
199
199
|
|
|
200
|
-
##
|
|
200
|
+
## Prior Art
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
### JSON Web Signature (JWS)
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
- **rfc8785 (Python)** — pure Python canonicalizer
|
|
206
|
-
- **jcs (Elixir)** — Elixir implementation of JCS
|
|
207
|
-
- **JOSE / JWS / JWT** — signing standards focused on tokens, not arbitrary JSON
|
|
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**.
|
|
208
205
|
|
|
209
|
-
|
|
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).
|
|
216
|
+
|
|
217
|
+
- **Not offline‑first**
|
|
218
|
+
JWS is built for network protocols. json‑seal is built for sealed backups, hash chains, and local integrity.
|
|
219
|
+
|
|
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.
|
|
222
|
+
|
|
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.
|
|
210
235
|
|
|
211
236
|
---
|
|
212
237
|
|
|
@@ -228,7 +253,6 @@ Run tests:
|
|
|
228
253
|
```bash
|
|
229
254
|
npm test
|
|
230
255
|
```
|
|
231
|
-
|
|
232
256
|
---
|
|
233
257
|
|
|
234
258
|
Pull Requests are welcome.
|
|
@@ -237,4 +261,4 @@ Pull Requests are welcome.
|
|
|
237
261
|
|
|
238
262
|
MIT
|
|
239
263
|
|
|
240
|
-
---
|
|
264
|
+
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "json-seal",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"author": "Chris Myers <cmyers2015@outlook.com> (https://www.npmjs.com/~cmyers-dev)",
|
|
5
5
|
"description": "Create cryptographically signed, tamper‑proof JSON backups with zero dependencies.",
|
|
6
6
|
"type": "module",
|
package/dist/canonicalize.d.ts
DELETED
package/dist/canonicalize.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
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}`);
|
|
21
|
-
}
|
|
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;
|
|
92
|
-
}
|
|
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])}`);
|
|
112
|
-
return `{${entries.join(",")}}`;
|
|
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/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
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
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
|
-
}
|