json-seal 0.9.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/LICENSE +21 -0
- package/README.md +280 -0
- package/dist/canonicalize.d.ts +1 -0
- package/dist/canonicalize.js +11 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/sign.d.ts +14 -0
- package/dist/sign.js +33 -0
- package/dist/verify.d.ts +4 -0
- package/dist/verify.js +19 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Myers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# **json‑seal**
|
|
2
|
+
|
|
3
|
+
Cryptographically signed, tamper‑proof JSON backups for apps - zero dependencies and a tiny footprint under 5 kB.
|
|
4
|
+
|
|
5
|
+
json‑seal lets you:
|
|
6
|
+
|
|
7
|
+
- Canonicalize any JSON object
|
|
8
|
+
- Sign it with a private key
|
|
9
|
+
- Embed the public key
|
|
10
|
+
- Verify integrity later
|
|
11
|
+
- Detect any tampering - even a single character
|
|
12
|
+
|
|
13
|
+
It’s like JWS, but for **arbitrary JSON documents**, without JWT complexity, and designed for **offline‑first apps**, **local backups**, and **portable integrity checks**.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## **Why json‑seal exists**
|
|
18
|
+
|
|
19
|
+
Most security libraries focus on:
|
|
20
|
+
|
|
21
|
+
- encrypted blobs (iron‑webcrypto)
|
|
22
|
+
- authentication tokens (JOSE/JWS/JWT)
|
|
23
|
+
- low‑level primitives (WebCrypto, libsodium)
|
|
24
|
+
|
|
25
|
+
None of these solves the problem of:
|
|
26
|
+
|
|
27
|
+
**“I need to store or transmit JSON in a way that guarantees it hasn’t been tampered with - while keeping it readable, portable, and framework‑agnostic.”**
|
|
28
|
+
|
|
29
|
+
json‑seal fills that gap.
|
|
30
|
+
|
|
31
|
+
It turns any JSON object into a **sealed artifact** that can be verified anywhere, on any device, without servers, shared secrets, or opaque binary formats. The result is a portable, human‑readable, cryptographically signed JSON document that remains trustworthy for years.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## **Features**
|
|
36
|
+
|
|
37
|
+
### **Deterministic canonicalization**
|
|
38
|
+
Stable, cross‑platform byte representation of JSON for reliable signatures.
|
|
39
|
+
If the JSON changes - even whitespace - verification fails.
|
|
40
|
+
|
|
41
|
+
### **RSA‑PSS digital signatures**
|
|
42
|
+
Modern, secure, asymmetric signing using the WebCrypto API.
|
|
43
|
+
No shared passwords, no symmetric secrets, no server dependency.
|
|
44
|
+
|
|
45
|
+
### **Pure JSON seal format**
|
|
46
|
+
Human‑readable, portable, and easy to store, sync, export, or transmit.
|
|
47
|
+
Everything needed for verification is embedded.
|
|
48
|
+
|
|
49
|
+
### **Browser + Node support**
|
|
50
|
+
Works anywhere `crypto.subtle` is available - modern browsers, PWAs, Node 18+, Bun, Deno, and edge runtimes.
|
|
51
|
+
|
|
52
|
+
### **Framework‑agnostic**
|
|
53
|
+
Angular, React, Vue, Svelte, Ionic, Capacitor, PWAs, Node, Bun, Deno - json‑seal fits everywhere.
|
|
54
|
+
|
|
55
|
+
### **Zero dependencies**
|
|
56
|
+
Small, auditable, and safe for long‑term use.
|
|
57
|
+
No polyfills, no crypto libraries, no runtime baggage.
|
|
58
|
+
|
|
59
|
+
### **Perfect for offline‑first apps**
|
|
60
|
+
Protects:
|
|
61
|
+
|
|
62
|
+
- Local storage
|
|
63
|
+
- IndexedDB
|
|
64
|
+
- Sync engines
|
|
65
|
+
- User‑exported backups
|
|
66
|
+
- Cross‑device data portability
|
|
67
|
+
|
|
68
|
+
json‑seal is built for apps that need **trustworthy, tamper‑proof JSON**, not tokens or encrypted blobs.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## **Installation**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install json-seal
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## **Quick Start**
|
|
81
|
+
|
|
82
|
+
### **Generate a keypair**
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { generateKeyPair } from "json-seal";
|
|
86
|
+
|
|
87
|
+
const { privateKey, publicKey } = await generateKeyPair();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### **Sign a payload**
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { signPayload } from "json-seal";
|
|
94
|
+
|
|
95
|
+
const payload = { id: 1, data: "hello" };
|
|
96
|
+
|
|
97
|
+
const backup = await signPayload(payload, privateKey, publicKey);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### **Verify a backup**
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { verifyBackup } from "json-seal";
|
|
104
|
+
|
|
105
|
+
const result = await verifyBackup(backup);
|
|
106
|
+
|
|
107
|
+
if (result.valid) {
|
|
108
|
+
console.log("Payload:", result.payload);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## **What a signed backup looks like**
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"version": 1,
|
|
119
|
+
"timestamp": "2026-01-11T18:24:55.402Z",
|
|
120
|
+
"payload": { "id": 1, "data": "hello" },
|
|
121
|
+
"signature": {
|
|
122
|
+
"algorithm": "RSA-PSS-SHA256",
|
|
123
|
+
"publicKey": "-----BEGIN PUBLIC KEY----- ...",
|
|
124
|
+
"value": "base64-signature"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Everything needed to verify the backup is embedded.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## **Tamper Detection**
|
|
134
|
+
|
|
135
|
+
Any modification - even deep inside nested objects - invalidates the signature.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const tampered = { ...backup, payload: { id: 1, data: "hacked" } };
|
|
139
|
+
|
|
140
|
+
verifyBackup(tampered).valid; // false
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## **Key Management**
|
|
146
|
+
|
|
147
|
+
`generateKeyPair()` should be called **once**, not on every backup.
|
|
148
|
+
Apps are expected to generate or receive a keypair during onboarding and store it securely.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### **App‑generated keys**
|
|
153
|
+
|
|
154
|
+
Most offline‑first apps generate a keypair on first launch:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { generateKeyPair } from "json-seal";
|
|
158
|
+
|
|
159
|
+
const { privateKey, publicKey } = await generateKeyPair();
|
|
160
|
+
|
|
161
|
+
secureStore.set("privateKey", privateKey);
|
|
162
|
+
secureStore.set("publicKey", publicKey);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
On subsequent runs:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const privateKey = secureStore.get("privateKey");
|
|
169
|
+
const publicKey = secureStore.get("publicKey");
|
|
170
|
+
|
|
171
|
+
const backup = await signPayload(data, privateKey, publicKey);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### **Where to store keys**
|
|
177
|
+
|
|
178
|
+
Storage depends on the platform:
|
|
179
|
+
|
|
180
|
+
- iOS → Keychain
|
|
181
|
+
- Android → Keystore
|
|
182
|
+
- Web → IndexedDB + WebCrypto
|
|
183
|
+
- Desktop → OS keyring or encrypted local file
|
|
184
|
+
- Node → environment variables or encrypted file
|
|
185
|
+
|
|
186
|
+
json‑seal intentionally does **not** handle storage so it can remain environment‑agnostic.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
### **Server‑generated keys**
|
|
191
|
+
|
|
192
|
+
Some architectures prefer the backend to generate and manage keys:
|
|
193
|
+
|
|
194
|
+
1. Server generates keypair
|
|
195
|
+
2. Server stores private key
|
|
196
|
+
3. Server sends public key to the app
|
|
197
|
+
4. App signs backups using the server’s public key
|
|
198
|
+
5. Server verifies integrity later
|
|
199
|
+
|
|
200
|
+
Useful for multi‑device accounts or enterprise systems.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### **Key rotation**
|
|
205
|
+
|
|
206
|
+
json‑seal embeds the public key inside each backup, so old backups remain verifiable even after rotation.
|
|
207
|
+
|
|
208
|
+
Typical strategy:
|
|
209
|
+
|
|
210
|
+
- generate a new keypair yearly
|
|
211
|
+
- store the new private key
|
|
212
|
+
- keep old public keys for verification
|
|
213
|
+
- continue verifying old backups without breaking anything
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### **Importing and exporting keys**
|
|
218
|
+
|
|
219
|
+
Keys are standard PEM strings, so they can be:
|
|
220
|
+
|
|
221
|
+
- backed up
|
|
222
|
+
- migrated
|
|
223
|
+
- synced
|
|
224
|
+
- exported/imported
|
|
225
|
+
|
|
226
|
+
Perfect for long‑term, portable backup formats.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## **API**
|
|
231
|
+
|
|
232
|
+
### **`generateKeyPair()`**
|
|
233
|
+
Generates a 2048‑bit RSA‑PSS keypair using WebCrypto.
|
|
234
|
+
|
|
235
|
+
### **`signPayload(payload, privateKey, publicKey)`**
|
|
236
|
+
- Canonicalizes the JSON
|
|
237
|
+
- Signs it using RSA‑PSS SHA‑256
|
|
238
|
+
- Embeds the public key
|
|
239
|
+
- Returns a portable backup object
|
|
240
|
+
|
|
241
|
+
### **`verifyBackup(backup)`**
|
|
242
|
+
- Re‑canonicalizes the payload
|
|
243
|
+
- Verifies the signature
|
|
244
|
+
- Returns `{ valid: boolean, payload?: any }`
|
|
245
|
+
|
|
246
|
+
### **`canonicalize(obj)`**
|
|
247
|
+
Deterministic JSON serializer with sorted keys.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## **Testing**
|
|
252
|
+
|
|
253
|
+
json‑seal ships with a full Vitest suite covering:
|
|
254
|
+
|
|
255
|
+
- Valid signatures
|
|
256
|
+
- Shallow tampering
|
|
257
|
+
- Deep tampering
|
|
258
|
+
- Missing signature
|
|
259
|
+
- Wrong public key
|
|
260
|
+
- Corrupted signature
|
|
261
|
+
- Canonicalization stability
|
|
262
|
+
- Large payloads
|
|
263
|
+
- Arrays and primitives
|
|
264
|
+
- RSA‑PSS non‑determinism
|
|
265
|
+
|
|
266
|
+
Run tests:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
npm test
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
Pull Requests welcome.
|
|
275
|
+
|
|
276
|
+
## **License**
|
|
277
|
+
|
|
278
|
+
MIT
|
|
279
|
+
|
|
280
|
+
---
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function canonicalize(obj: any): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
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
ADDED
package/dist/index.js
ADDED
package/dist/sign.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
ADDED
package/dist/verify.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "json-seal",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Cryptographically signed, tamper‑proof JSON backups with deterministic canonicalization and RSA‑PSS signatures.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"cryptography",
|
|
26
|
+
"digital-signature",
|
|
27
|
+
"tamper-detection",
|
|
28
|
+
"data-integrity",
|
|
29
|
+
"rsa-pss",
|
|
30
|
+
"json",
|
|
31
|
+
"json-security",
|
|
32
|
+
"json-signing",
|
|
33
|
+
"canonicalization",
|
|
34
|
+
"typescript",
|
|
35
|
+
"nodejs"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/cmyers/json-seal.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/cmyers/json-seal#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/cmyers/json-seal/issues"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"test": "vitest",
|
|
48
|
+
"test:watch": "vitest --watch"
|
|
49
|
+
},
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^25.0.6",
|
|
53
|
+
"tsx": "^4.21.0",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"vitest": "^4.0.16"
|
|
56
|
+
}
|
|
57
|
+
}
|