json-seal 0.10.1 → 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 +89 -16
- package/package.json +58 -57
- package/dist/canonicalize.d.ts +0 -1
- package/dist/canonicalize.js +0 -11
- 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
|
@@ -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
|
-
- Detect any tampering
|
|
31
|
+
- Detect any tampering
|
|
30
32
|
|
|
31
|
-
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
|
+
|
|
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? }`.
|
|
@@ -150,16 +197,41 @@ Full RFC 8785 Canonical JSON implementation.
|
|
|
150
197
|
|
|
151
198
|
---
|
|
152
199
|
|
|
153
|
-
##
|
|
200
|
+
## Prior Art
|
|
201
|
+
|
|
202
|
+
### JSON Web Signature (JWS)
|
|
203
|
+
|
|
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**.
|
|
205
|
+
|
|
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).
|
|
154
216
|
|
|
155
|
-
|
|
217
|
+
- **Not offline‑first**
|
|
218
|
+
JWS is built for network protocols. json‑seal is built for sealed backups, hash chains, and local integrity.
|
|
156
219
|
|
|
157
|
-
- **
|
|
158
|
-
|
|
159
|
-
- **jcs (Elixir)** — Elixir implementation of JCS
|
|
160
|
-
- **JOSE / JWS / JWT** — signing standards focused on tokens, not arbitrary JSON
|
|
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.
|
|
161
222
|
|
|
162
|
-
|
|
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.
|
|
163
235
|
|
|
164
236
|
---
|
|
165
237
|
|
|
@@ -181,11 +253,12 @@ Run tests:
|
|
|
181
253
|
```bash
|
|
182
254
|
npm test
|
|
183
255
|
```
|
|
184
|
-
|
|
185
256
|
---
|
|
257
|
+
|
|
186
258
|
Pull Requests are welcome.
|
|
259
|
+
|
|
187
260
|
## **License**
|
|
188
261
|
|
|
189
262
|
MIT
|
|
190
263
|
|
|
191
|
-
---
|
|
264
|
+
---
|
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.1",
|
|
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
|
+
}
|
package/dist/canonicalize.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function canonicalize(obj: any): string;
|
package/dist/canonicalize.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export function canonicalize(obj) {
|
|
2
|
-
if (obj === null || typeof obj !== "object") {
|
|
3
|
-
return JSON.stringify(obj);
|
|
4
|
-
}
|
|
5
|
-
if (Array.isArray(obj)) {
|
|
6
|
-
return `[${obj.map(canonicalize).join(",")}]`;
|
|
7
|
-
}
|
|
8
|
-
const keys = Object.keys(obj).sort();
|
|
9
|
-
const entries = keys.map(k => `"${k}":${canonicalize(obj[k])}`);
|
|
10
|
-
return `{${entries.join(",")}}`;
|
|
11
|
-
}
|
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
|
-
}
|