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 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 — even a single character
31
+ - Detect any tampering
32
32
 
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**.
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
- ## **Prior Art**
200
+ ## Prior Art
201
201
 
202
- json‑seal builds on ideas from:
202
+ ### JSON Web Signature (JWS)
203
203
 
204
- - **json-canonicalize** RFC 8785 canonicalization
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
- json‑seal combines **canonicalization + signing + verification** into a single, zero‑dependency library designed for **offline‑first, portable JSON integrity**.
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.0",
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",
@@ -1,5 +0,0 @@
1
- /**
2
- * RFC 8785 Canonical JSON implementation
3
- * Deterministic, strict, and cross‑runtime stable.
4
- */
5
- export declare function canonicalize(value: any): string;
@@ -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
@@ -1,3 +0,0 @@
1
- export * from "./canonicalize.js";
2
- export * from "./sign.js";
3
- export * from "./verify.js";
package/dist/index.js DELETED
@@ -1,3 +0,0 @@
1
- export * from "./canonicalize.js";
2
- export * from "./sign.js";
3
- export * from "./verify.js";
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
@@ -1,4 +0,0 @@
1
- export declare function verifyBackup(backup: any): {
2
- valid: boolean;
3
- payload: any;
4
- };
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
- }