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 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
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./canonicalize.js";
2
+ export * from "./sign.js";
3
+ export * from "./verify.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./canonicalize.js";
2
+ export * from "./sign.js";
3
+ export * from "./verify.js";
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
+ }
@@ -0,0 +1,4 @@
1
+ export declare function verifyBackup(backup: any): {
2
+ valid: boolean;
3
+ payload: any;
4
+ };
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
+ }