json-seal 0.11.1 → 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
 
@@ -45,7 +45,7 @@ It’s built for **offline‑first apps**, **local backups**, and **portable int
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" } };
@@ -231,7 +231,7 @@ json‑seal focuses on a simpler, narrower goal:
231
231
  - **Zero dependencies**
232
232
  - **Portable across browsers, Node, Deno, Bun, and hybrid mobile apps**
233
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.
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.
235
235
 
236
236
  ---
237
237
 
@@ -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,5 @@
1
+ /**
2
+ * RFC 8785 Canonical JSON implementation
3
+ * Deterministic, strict, and cross‑runtime stable.
4
+ */
5
+ export declare function canonicalize(value: any): string;
@@ -0,0 +1,122 @@
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
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./canonicalize.js";
2
+ export * from "./sign.js";
3
+ export * from "./verify.js";
4
+ export * from "./keys.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./canonicalize.js";
2
+ export * from "./sign.js";
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 ADDED
@@ -0,0 +1,10 @@
1
+ export declare function signPayload(payload: any, privateKeyPem: string, publicKeyPem: string): Promise<{
2
+ version: number;
3
+ timestamp: string;
4
+ payload: import("./isJsonValue.js").JsonValue;
5
+ signature: {
6
+ algorithm: string;
7
+ publicKey: string;
8
+ value: string;
9
+ };
10
+ }>;
package/dist/sign.js ADDED
@@ -0,0 +1,23 @@
1
+ import { canonicalize } from "./canonicalize.js";
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");
9
+ const canonical = canonicalize(payload);
10
+ const privateKey = await importPrivateKey(privateKeyPem);
11
+ const signatureBytes = await signCanonical(canonical, privateKey);
12
+ const signature = arrayBufferToBase64(signatureBytes);
13
+ return {
14
+ version: 1,
15
+ timestamp: new Date().toISOString(),
16
+ payload,
17
+ signature: {
18
+ algorithm: "RSA-PSS-SHA256",
19
+ publicKey: publicKeyPem,
20
+ value: signature
21
+ }
22
+ };
23
+ }
@@ -0,0 +1,4 @@
1
+ export declare function verifyBackup(backup: any): Promise<{
2
+ valid: boolean;
3
+ payload: any;
4
+ }>;
package/dist/verify.js ADDED
@@ -0,0 +1,15 @@
1
+ import { canonicalize } from "./canonicalize.js";
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) {
6
+ const { payload, signature } = backup;
7
+ const canonical = canonicalize(payload);
8
+ const publicKey = await importPublicKey(signature.publicKey);
9
+ const signatureBytes = base64ToArrayBuffer(signature.value);
10
+ const valid = await verifyCanonical(canonical, signatureBytes, publicKey);
11
+ return {
12
+ valid,
13
+ payload: valid ? payload : undefined
14
+ };
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-seal",
3
- "version": "0.11.1",
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",