json-seal 0.11.0 → 0.11.2

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
@@ -18,9 +18,9 @@
18
18
 
19
19
  ## **Why json‑seal**
20
20
 
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:
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
22
 
23
- **“I need to store JSON in a way that guarantees integrity while keeping it readable, portable, and framework‑agnostic.”**
23
+ **“I need to store JSON in a way that guarantees integrity - while keeping it readable, portable, and framework‑agnostic.”**
24
24
 
25
25
  json‑seal fills that gap. It lets you:
26
26
 
@@ -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
 
@@ -45,7 +45,7 @@ It’s like JWS, but for **arbitrary JSON documents**, without JWT complexity, a
45
45
  - booleans
46
46
  - null
47
47
 
48
- Typed TypeScript interfaces work automatically as long as their fields are JSON‑compatible.
48
+ TypeScript interfaces work automatically as long as their fields are JSON‑compatible.
49
49
 
50
50
  ### **Rejected values**
51
51
 
@@ -170,7 +170,7 @@ if (result.valid) {
170
170
 
171
171
  ## **Tamper Detection**
172
172
 
173
- Any modification even deep inside nested objects invalidates the signature.
173
+ Any modification - even deep inside nested objects - invalidates the signature.
174
174
 
175
175
  ```ts
176
176
  const tampered = { ...backup, payload: { id: 1, data: "modified" } };
@@ -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
+ ---
@@ -0,0 +1,2 @@
1
+ export declare function arrayBufferToBase64(buf: ArrayBuffer): string;
2
+ export declare function base64ToArrayBuffer(base64: string): ArrayBuffer;
package/dist/base64.js ADDED
@@ -0,0 +1,23 @@
1
+ // Cross‑runtime base64 helpers (browser + Node 18+)
2
+ const _atob = typeof atob === "function"
3
+ ? atob
4
+ : (b64) => Buffer.from(b64, "base64").toString("binary");
5
+ const _btoa = typeof btoa === "function"
6
+ ? btoa
7
+ : (bin) => Buffer.from(bin, "binary").toString("base64");
8
+ export function arrayBufferToBase64(buf) {
9
+ const bytes = new Uint8Array(buf);
10
+ let binary = "";
11
+ for (let i = 0; i < bytes.length; i++) {
12
+ binary += String.fromCharCode(bytes[i]);
13
+ }
14
+ return _btoa(binary);
15
+ }
16
+ export function base64ToArrayBuffer(base64) {
17
+ const binary = _atob(base64);
18
+ const bytes = new Uint8Array(binary.length);
19
+ for (let i = 0; i < binary.length; i++) {
20
+ bytes[i] = binary.charCodeAt(i);
21
+ }
22
+ return bytes.buffer;
23
+ }
@@ -0,0 +1 @@
1
+ export declare function signCanonical(canonical: string, privateKey: CryptoKey): Promise<ArrayBuffer>;
@@ -0,0 +1,8 @@
1
+ const encoder = new TextEncoder();
2
+ export async function signCanonical(canonical, privateKey) {
3
+ const bytes = encoder.encode(canonical);
4
+ return crypto.subtle.sign({
5
+ name: "RSA-PSS",
6
+ saltLength: 32
7
+ }, privateKey, bytes);
8
+ }
@@ -0,0 +1 @@
1
+ export declare function verifyCanonical(canonical: string, signature: ArrayBuffer, publicKey: CryptoKey): Promise<boolean>;
@@ -0,0 +1,8 @@
1
+ const encoder = new TextEncoder();
2
+ export async function verifyCanonical(canonical, signature, publicKey) {
3
+ const bytes = encoder.encode(canonical);
4
+ return crypto.subtle.verify({
5
+ name: "RSA-PSS",
6
+ saltLength: 32
7
+ }, publicKey, signature, bytes);
8
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./canonicalize.js";
2
2
  export * from "./sign.js";
3
3
  export * from "./verify.js";
4
+ export * from "./keys.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./canonicalize.js";
2
2
  export * from "./sign.js";
3
3
  export * from "./verify.js";
4
+ export * from "./keys.js";
@@ -0,0 +1,4 @@
1
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
2
+ [key: string]: JsonValue;
3
+ };
4
+ export declare function isJsonValue(value: unknown): value is JsonValue;
@@ -0,0 +1,25 @@
1
+ export function isJsonValue(value) {
2
+ if (value === null ||
3
+ typeof value === "string" ||
4
+ typeof value === "number" ||
5
+ typeof value === "boolean") {
6
+ return true;
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return value.every(isJsonValue);
10
+ }
11
+ // Objects (but not class instances, Dates, Maps, Sets, etc.)
12
+ if (typeof value === "object") {
13
+ const proto = Object.getPrototypeOf(value);
14
+ if (proto !== Object.prototype && proto !== null) {
15
+ return false;
16
+ }
17
+ for (const key of Object.keys(value)) {
18
+ if (!isJsonValue(value[key])) {
19
+ return false;
20
+ }
21
+ }
22
+ return true;
23
+ }
24
+ return false;
25
+ }
package/dist/keys.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export declare function generateKeyPair(): Promise<{
2
+ privateKey: string;
3
+ publicKey: string;
4
+ }>;
5
+ export declare function importPrivateKey(privateKeyPem: string): Promise<CryptoKey>;
6
+ export declare function importPublicKey(publicKeyPem: string): Promise<CryptoKey>;
package/dist/keys.js ADDED
@@ -0,0 +1,29 @@
1
+ import { pemToArrayBuffer, arrayBufferToPem } from "./pem.js";
2
+ export async function generateKeyPair() {
3
+ const { publicKey, privateKey } = await crypto.subtle.generateKey({
4
+ name: "RSA-PSS",
5
+ modulusLength: 2048,
6
+ publicExponent: new Uint8Array([1, 0, 1]),
7
+ hash: "SHA-256"
8
+ }, true, ["sign", "verify"]);
9
+ const pkcs8 = await crypto.subtle.exportKey("pkcs8", privateKey);
10
+ const spki = await crypto.subtle.exportKey("spki", publicKey);
11
+ return {
12
+ privateKey: arrayBufferToPem(pkcs8, "private"),
13
+ publicKey: arrayBufferToPem(spki, "public")
14
+ };
15
+ }
16
+ export async function importPrivateKey(privateKeyPem) {
17
+ const pkcs8 = pemToArrayBuffer(privateKeyPem);
18
+ return crypto.subtle.importKey("pkcs8", pkcs8, {
19
+ name: "RSA-PSS",
20
+ hash: "SHA-256"
21
+ }, false, ["sign"]);
22
+ }
23
+ export async function importPublicKey(publicKeyPem) {
24
+ const spki = pemToArrayBuffer(publicKeyPem);
25
+ return crypto.subtle.importKey("spki", spki, {
26
+ name: "RSA-PSS",
27
+ hash: "SHA-256"
28
+ }, false, ["verify"]);
29
+ }
package/dist/pem.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function pemToArrayBuffer(pem: string): ArrayBuffer;
2
+ export declare function arrayBufferToPem(buf: ArrayBuffer, type: "private" | "public"): string;
package/dist/pem.js ADDED
@@ -0,0 +1,19 @@
1
+ import { arrayBufferToBase64, base64ToArrayBuffer } from "./base64.js";
2
+ // strip header/footer + whitespace
3
+ function pemBody(pem) {
4
+ return pem
5
+ .replace(/-----BEGIN [^-]+-----/, "")
6
+ .replace(/-----END [^-]+-----/, "")
7
+ .replace(/\s+/g, "");
8
+ }
9
+ export function pemToArrayBuffer(pem) {
10
+ const base64 = pemBody(pem);
11
+ return base64ToArrayBuffer(base64);
12
+ }
13
+ export function arrayBufferToPem(buf, type) {
14
+ const base64 = arrayBufferToBase64(buf);
15
+ if (type === "private") {
16
+ return `-----BEGIN PRIVATE KEY-----\n${base64}\n-----END PRIVATE KEY-----`;
17
+ }
18
+ return `-----BEGIN PUBLIC KEY-----\n${base64}\n-----END PUBLIC KEY-----`;
19
+ }
package/dist/sign.d.ts CHANGED
@@ -1,14 +1,10 @@
1
- export declare function generateKeyPair(): {
2
- privateKey: string;
3
- publicKey: string;
4
- };
5
- export declare function signPayload(payload: any, privateKeyPem: string, publicKeyPem: string): {
1
+ export declare function signPayload(payload: any, privateKeyPem: string, publicKeyPem: string): Promise<{
6
2
  version: number;
7
3
  timestamp: string;
8
- payload: any;
4
+ payload: import("./isJsonValue.js").JsonValue;
9
5
  signature: {
10
6
  algorithm: string;
11
7
  publicKey: string;
12
8
  value: string;
13
9
  };
14
- };
10
+ }>;
package/dist/sign.js CHANGED
@@ -1,25 +1,15 @@
1
- import { generateKeyPairSync, createSign, constants } from "crypto";
2
1
  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) {
2
+ import { importPrivateKey } from "./keys.js";
3
+ import { signCanonical } from "./crypto-sign.js";
4
+ import { arrayBufferToBase64 } from "./base64.js";
5
+ import { isJsonValue } from "./isJsonValue.js";
6
+ export async function signPayload(payload, privateKeyPem, publicKeyPem) {
7
+ if (!isJsonValue(payload))
8
+ throw new Error("signPayload only accepts JSON-compatible values");
13
9
  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");
10
+ const privateKey = await importPrivateKey(privateKeyPem);
11
+ const signatureBytes = await signCanonical(canonical, privateKey);
12
+ const signature = arrayBufferToBase64(signatureBytes);
23
13
  return {
24
14
  version: 1,
25
15
  timestamp: new Date().toISOString(),
package/dist/verify.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare function verifyBackup(backup: any): {
1
+ export declare function verifyBackup(backup: any): Promise<{
2
2
  valid: boolean;
3
3
  payload: any;
4
- };
4
+ }>;
package/dist/verify.js CHANGED
@@ -1,17 +1,13 @@
1
- import { createVerify, constants } from "crypto";
2
1
  import { canonicalize } from "./canonicalize.js";
3
- export function verifyBackup(backup) {
2
+ import { importPublicKey } from "./keys.js";
3
+ import { verifyCanonical } from "./crypto-verify.js";
4
+ import { base64ToArrayBuffer } from "./base64.js";
5
+ export async function verifyBackup(backup) {
4
6
  const { payload, signature } = backup;
5
7
  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"));
8
+ const publicKey = await importPublicKey(signature.publicKey);
9
+ const signatureBytes = base64ToArrayBuffer(signature.value);
10
+ const valid = await verifyCanonical(canonical, signatureBytes, publicKey);
15
11
  return {
16
12
  valid,
17
13
  payload: valid ? payload : undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-seal",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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",