@majikah/majik-signature 0.0.1 → 0.0.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 +369 -372
- package/dist/core/embed/constants.d.ts +25 -0
- package/dist/core/embed/constants.js +27 -0
- package/dist/core/embed/fallback.d.ts +18 -0
- package/dist/core/embed/fallback.js +31 -0
- package/dist/core/embed/handlers/flac.d.ts +29 -0
- package/dist/core/embed/handlers/flac.js +186 -0
- package/dist/core/embed/handlers/jpeg.d.ts +23 -0
- package/dist/core/embed/handlers/jpeg.js +145 -0
- package/dist/core/embed/handlers/mkv.d.ts +22 -0
- package/dist/core/embed/handlers/mkv.js +45 -0
- package/dist/core/embed/handlers/mp3.d.ts +32 -0
- package/dist/core/embed/handlers/mp3.js +200 -0
- package/dist/core/embed/handlers/mp4.d.ts +27 -0
- package/dist/core/embed/handlers/mp4.js +173 -0
- package/dist/core/embed/handlers/office.d.ts +24 -0
- package/dist/core/embed/handlers/office.js +96 -0
- package/dist/core/embed/handlers/pdf.d.ts +21 -0
- package/dist/core/embed/handlers/pdf.js +183 -0
- package/dist/core/embed/handlers/png.d.ts +23 -0
- package/dist/core/embed/handlers/png.js +138 -0
- package/dist/core/embed/handlers/text.d.ts +29 -0
- package/dist/core/embed/handlers/text.js +102 -0
- package/dist/core/embed/handlers/wav.d.ts +24 -0
- package/dist/core/embed/handlers/wav.js +129 -0
- package/dist/core/embed/majik-embed.d.ts +85 -0
- package/dist/core/embed/majik-embed.js +174 -0
- package/dist/core/embed/registry.d.ts +25 -0
- package/dist/core/embed/registry.js +36 -0
- package/dist/core/embed/utils.d.ts +40 -0
- package/dist/core/embed/utils.js +251 -0
- package/dist/core/types.d.ts +79 -2
- package/dist/majik-signature.d.ts +84 -0
- package/dist/majik-signature.js +97 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -4,58 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
**Majik Signature** is a hybrid post-quantum content signing and verification library for the Majikah ecosystem. Built on top of **Majik Key**, it provides tamper-proof, forgery-resistant digital signatures for any content format — plaintext, JSON, PDF, audio, video, binary — using a dual-algorithm architecture that combines classical Ed25519 with post-quantum ML-DSA-87 (FIPS-204).
|
|
6
6
|
|
|
7
|
+
**Majik Signature now includes built-in file embedding** — sign any file and embed the signature directly into its native metadata. No sidecar files needed. PDFs stay PDFs, WAVs stay WAVs, MP4s stay MP4s.
|
|
8
|
+
|
|
7
9
|
   [](https://opensource.org/licenses/Apache-2.0) 
|
|
8
10
|
|
|
9
11
|
---
|
|
10
12
|
|
|
11
13
|
- [Majik Signature](#majik-signature)
|
|
12
14
|
- [Security Architecture](#security-architecture)
|
|
13
|
-
- [1. Hybrid Dual-Algorithm Signing](#1-hybrid-dual-algorithm-signing)
|
|
14
|
-
- [2. Canonical Payload Binding](#2-canonical-payload-binding)
|
|
15
|
-
- [3. Content-Agnostic Hashing](#3-content-agnostic-hashing)
|
|
16
15
|
- [Overview](#overview)
|
|
17
|
-
- [What is a Majik Signature?](#what-is-a-majik-signature)
|
|
18
|
-
- [Use Cases](#use-cases)
|
|
19
16
|
- [Features](#features)
|
|
20
|
-
- [Security \& Post-Quantum Readiness](#security--post-quantum-readiness)
|
|
21
|
-
- [Content Format Support](#content-format-support)
|
|
22
|
-
- [Developer Experience](#developer-experience)
|
|
23
|
-
- [Serialization \& Portability](#serialization--portability)
|
|
24
17
|
- [Installation](#installation)
|
|
25
18
|
- [Quick Start](#quick-start)
|
|
19
|
+
- [File Embedding — Quick Start](#file-embedding--quick-start)
|
|
26
20
|
- [API Reference](#api-reference)
|
|
27
|
-
- [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- [`MajikSignature.verifyWithKey(content, signature, key)`](#majiksignatureverifywithkeycontent-signature-key)
|
|
31
|
-
- [`MajikSignature.publicKeysFromMajikKey(key)`](#majiksignaturepublickeysfrommajikkeykey)
|
|
32
|
-
- [`MajikSignature.fromJSON(json)`](#majiksignaturefromjsonjson)
|
|
33
|
-
- [`MajikSignature.deserialize(base64)`](#majiksignaturedeserializebase64)
|
|
21
|
+
- [Content Signing (bytes/strings)](#content-signing-bytesstrings)
|
|
22
|
+
- [File Embedding](#file-embedding)
|
|
23
|
+
- [Lower-Level Embed API](#lower-level-embed-api)
|
|
34
24
|
- [Instance Methods](#instance-methods)
|
|
35
|
-
- [`validate()`](#validate)
|
|
36
|
-
- [`isValid()`](#isvalid)
|
|
37
|
-
- [`extractPublicKeys()`](#extractpublickeys)
|
|
38
|
-
- [`toJSON()`](#tojson)
|
|
39
|
-
- [`serialize()`](#serialize)
|
|
40
|
-
- [`toString()`](#tostring)
|
|
41
25
|
- [Getters](#getters)
|
|
26
|
+
- [Supported File Formats](#supported-file-formats)
|
|
42
27
|
- [Usage Examples](#usage-examples)
|
|
43
|
-
- [Example 1: Sign and Verify a Text Document](#example-1-sign-and-verify-a-text-document)
|
|
44
|
-
- [Example 2: Sign and Verify a Binary File](#example-2-sign-and-verify-a-binary-file)
|
|
45
|
-
- [Example 3: Sign a JSON Payload](#example-3-sign-a-json-payload)
|
|
46
|
-
- [Example 4: Serialize and Store a Signature](#example-4-serialize-and-store-a-signature)
|
|
47
|
-
- [Example 5: Verify from Stored Signature](#example-5-verify-from-stored-signature)
|
|
48
|
-
- [Example 6: Verify Using Only Public Keys](#example-6-verify-using-only-public-keys)
|
|
49
|
-
- [Example 7: Sign Audio or Video Content](#example-7-sign-audio-or-video-content)
|
|
50
28
|
- [Signature Envelope](#signature-envelope)
|
|
51
29
|
- [Security Considerations](#security-considerations)
|
|
52
|
-
- [What is Guaranteed](#what-is-guaranteed)
|
|
53
|
-
- [What is Your Responsibility](#what-is-your-responsibility)
|
|
54
|
-
- [What NOT to Do](#what-not-to-do)
|
|
55
|
-
- [What TO Do](#what-to-do)
|
|
56
30
|
- [Related Projects](#related-projects)
|
|
57
|
-
- [Majik Key](#majik-key)
|
|
58
|
-
- [Majik Message](#majik-message)
|
|
59
31
|
- [Contributing](#contributing)
|
|
60
32
|
- [License](#license)
|
|
61
33
|
- [Author](#author)
|
|
@@ -92,18 +64,21 @@ Both signatures cover a **domain-separated canonical payload** that binds togeth
|
|
|
92
64
|
| `ct` | Content type (advisory) |
|
|
93
65
|
| `hash` | SHA-256 of the original content, base64 |
|
|
94
66
|
|
|
95
|
-
This binding means a valid signature cannot be
|
|
96
|
-
- Reused on different content (hash binding)
|
|
97
|
-
- Transferred to a different signer identity (id binding)
|
|
98
|
-
- Replayed with a modified timestamp (ts binding)
|
|
99
|
-
- Forged without both private keys
|
|
67
|
+
This binding means a valid signature cannot be reused on different content, transferred to a different signer, replayed with a modified timestamp, or forged without both private keys.
|
|
100
68
|
|
|
101
69
|
### 3. Content-Agnostic Hashing
|
|
102
70
|
|
|
103
|
-
Content is never embedded in the envelope
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
71
|
+
Content is never embedded in the envelope — only its SHA-256 hash is signed. This means a 500 MB video signs at the same speed as a 10-byte string, and any format is supported identically.
|
|
72
|
+
|
|
73
|
+
### 4. File Embedding Integrity
|
|
74
|
+
|
|
75
|
+
When a signature is embedded into a file, it always covers the **original file bytes before embedding**. Verification automatically strips the embedded signature before re-hashing, so the round-trip is always:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
sign(originalBytes) → embed into file → extract → strip → verify(originalBytes)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Re-signing the same file is always safe and idempotent — the existing signature is stripped before the new one is created.
|
|
107
82
|
|
|
108
83
|
---
|
|
109
84
|
|
|
@@ -111,11 +86,7 @@ Content is never embedded in the envelope. Only its SHA-256 hash is signed. This
|
|
|
111
86
|
|
|
112
87
|
### What is a Majik Signature?
|
|
113
88
|
|
|
114
|
-
A Majik Signature is a cryptographic proof that
|
|
115
|
-
- A specific piece of content (file, document, message, media) was produced or approved by the holder of a specific **Majik Key** account
|
|
116
|
-
- The content has not been modified since it was signed
|
|
117
|
-
- The signature cannot be forged without access to the signer's private keys
|
|
118
|
-
- The signature remains valid against future quantum computing threats
|
|
89
|
+
A Majik Signature is a cryptographic proof that a specific piece of content was produced or approved by the holder of a specific **Majik Key** account, that the content has not been modified since it was signed, and that the signature remains valid against future quantum computing threats.
|
|
119
90
|
|
|
120
91
|
Verification is fully **public** — anyone with the signer's public keys can verify. No private key is ever needed for verification.
|
|
121
92
|
|
|
@@ -125,7 +96,7 @@ Verification is fully **public** — anyone with the signer's public keys can ve
|
|
|
125
96
|
- **File Integrity**: Detect any tampering or modification to distributed files
|
|
126
97
|
- **API Payload Signing**: Sign JSON responses or requests for non-repudiation
|
|
127
98
|
- **Document Authentication**: Certify legal documents, contracts, or records
|
|
128
|
-
- **Media Certification**: Stamp audio, video, or image files as authentic originals
|
|
99
|
+
- **Media Certification**: Stamp audio, video, or image files as authentic originals — with the signature embedded directly in the file's metadata
|
|
129
100
|
- **Software Distribution**: Sign release artifacts to prove they come from the original author
|
|
130
101
|
- **Majikah Ecosystem**: Integrate with Majik Message and other Majikah products for identity-bound content
|
|
131
102
|
|
|
@@ -144,34 +115,36 @@ Verification is fully **public** — anyone with the signer's public keys can ve
|
|
|
144
115
|
|
|
145
116
|
### Content Format Support
|
|
146
117
|
|
|
147
|
-
- **Plain text** —
|
|
148
|
-
- **
|
|
149
|
-
- **
|
|
150
|
-
- **
|
|
151
|
-
- **
|
|
152
|
-
- **
|
|
153
|
-
- **
|
|
154
|
-
- **Any
|
|
118
|
+
- **Plain text**, **JSON**, **Binary** — `Uint8Array` or `string`
|
|
119
|
+
- **PDF, PNG, JPEG** — Signature embedded in native metadata (visible in File → Properties for PDF)
|
|
120
|
+
- **WAV, MP3, FLAC** — Embedded in RIFF/ID3/Vorbis metadata
|
|
121
|
+
- **MP4, MOV, M4A, M4V** — Embedded in `moov/udta` box
|
|
122
|
+
- **DOCX, XLSX, PPTX, ODF** — Embedded as a file entry inside the ZIP container
|
|
123
|
+
- **MKV, WebM** — Embedded via append-safe trailer
|
|
124
|
+
- **HTML, Markdown, JSON, plain text, source code** — Appended comment block
|
|
125
|
+
- **Any other format** — Universal binary trailer (self-describing, cleanly strippable)
|
|
155
126
|
|
|
156
127
|
### Developer Experience
|
|
157
128
|
|
|
158
129
|
- **First-Class TypeScript Support**: Full type definitions for all interfaces and classes
|
|
159
|
-
- **Simple
|
|
160
|
-
- **
|
|
130
|
+
- **Simple Core API**: `sign()` and `verify()` for bytes/strings; `signFile()` and `verifyFile()` for files
|
|
131
|
+
- **One-liner file signing**: `MajikSignature.signFile(blob, key)` — sign and embed in a single call
|
|
132
|
+
- **Format auto-detection**: MIME type and magic-byte sniffing — no manual format hints required
|
|
133
|
+
- **Idempotent re-signing**: Safely re-sign any file without accumulating stacked signatures
|
|
161
134
|
- **Structured Errors**: Typed error hierarchy for precise error handling
|
|
162
|
-
- **
|
|
163
|
-
- **Isomorphic**: Works in Node.js and modern browser environments
|
|
135
|
+
- **Isomorphic**: Works in Node.js and modern browser environments (no native deps)
|
|
164
136
|
|
|
165
137
|
### Serialization & Portability
|
|
166
138
|
|
|
167
139
|
- **JSON Envelope**: Full `toJSON()` / `fromJSON()` round-trip
|
|
168
140
|
- **Base64 Serialization**: `serialize()` / `deserialize()` for compact transport
|
|
169
|
-
- **
|
|
141
|
+
- **File-embedded**: Signature lives inside the file itself — no sidecar files needed
|
|
170
142
|
- **Self-Contained**: Envelope includes signer's public keys — verifiable without a key registry
|
|
171
143
|
|
|
172
144
|
---
|
|
173
145
|
|
|
174
146
|
## Installation
|
|
147
|
+
|
|
175
148
|
```bash
|
|
176
149
|
# Using npm
|
|
177
150
|
npm install @majikah/majik-signature
|
|
@@ -180,17 +153,19 @@ npm install @majikah/majik-signature
|
|
|
180
153
|
npm install @majikah/majik-key
|
|
181
154
|
```
|
|
182
155
|
|
|
156
|
+
No native bindings. Works in Node.js 18+, all modern browsers, Deno, and Bun.
|
|
157
|
+
|
|
183
158
|
---
|
|
184
159
|
|
|
185
160
|
## Quick Start
|
|
161
|
+
|
|
186
162
|
```typescript
|
|
187
163
|
import { MajikKey } from '@majikah/majik-key';
|
|
188
164
|
import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
|
|
189
165
|
|
|
190
|
-
// ── Step 1: Create and unlock a MajikKey
|
|
166
|
+
// ── Step 1: Create and unlock a MajikKey ─────────────────────────────────────
|
|
191
167
|
const mnemonic = MajikKey.generateMnemonic();
|
|
192
168
|
const key = await MajikKey.create(mnemonic, 'my-passphrase', 'My Signing Key');
|
|
193
|
-
// key is unlocked after create() — signing keys are ready
|
|
194
169
|
|
|
195
170
|
// ── Step 2: Sign content ──────────────────────────────────────────────────────
|
|
196
171
|
const document = 'This is the original content of my document.';
|
|
@@ -206,84 +181,97 @@ console.log('Timestamp:', signature.timestamp);
|
|
|
206
181
|
|
|
207
182
|
// ── Step 3: Serialize for storage or transport ────────────────────────────────
|
|
208
183
|
const serialized = signature.serialize(); // base64 string
|
|
209
|
-
// Store in a database, embed in a file, send via HTTP header, etc.
|
|
210
184
|
|
|
211
185
|
// ── Step 4: Verify (no private key needed) ────────────────────────────────────
|
|
212
186
|
const publicKeys = MajikSignature.publicKeysFromMajikKey(key);
|
|
213
187
|
const result = MajikSignature.verify(document, signature, publicKeys);
|
|
214
188
|
|
|
215
|
-
console.log('Valid:', result.valid);
|
|
189
|
+
console.log('Valid:', result.valid); // true
|
|
216
190
|
console.log('Signer:', result.signerId);
|
|
217
|
-
console.log('Hash:', result.contentHash);
|
|
218
191
|
|
|
219
|
-
//
|
|
192
|
+
// Shorthand — verify directly against a MajikKey
|
|
220
193
|
const result2 = MajikSignature.verifyWithKey(document, signature, key);
|
|
221
|
-
console.log('Valid:', result2.valid);
|
|
194
|
+
console.log('Valid:', result2.valid); // true
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## File Embedding — Quick Start
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { MajikKey } from '@majikah/majik-key';
|
|
203
|
+
import { MajikSignature } from '@majikah/majik-signature';
|
|
204
|
+
|
|
205
|
+
// ── Sign a file and embed the signature into it ───────────────────────────────
|
|
206
|
+
const { blob: signedBlob } = await MajikSignature.signFile(file, key);
|
|
207
|
+
// signedBlob is the same format as file — PDF stays PDF, WAV stays WAV, etc.
|
|
208
|
+
// The signature is embedded in the file's native metadata.
|
|
209
|
+
|
|
210
|
+
// ── Verify the embedded signature later ──────────────────────────────────────
|
|
211
|
+
const result = await MajikSignature.verifyFile(signedBlob, key);
|
|
212
|
+
if (result.valid) {
|
|
213
|
+
console.log('Verified — signed by:', result.signerId);
|
|
214
|
+
console.log('At:', result.timestamp);
|
|
215
|
+
console.log('Handler used:', result.handler); // e.g. "PDF", "WAV", "MP4/MOV"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Check if a file is signed (without verifying) ────────────────────────────
|
|
219
|
+
const signed = await MajikSignature.isSigned(file);
|
|
220
|
+
|
|
221
|
+
// ── Extract the embedded signature as a typed instance ───────────────────────
|
|
222
|
+
const sig = await MajikSignature.extractFrom(signedBlob);
|
|
223
|
+
if (sig) {
|
|
224
|
+
console.log(sig.signerId, sig.timestamp, sig.contentHash);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Get the original clean file (signature removed) ──────────────────────────
|
|
228
|
+
const originalBlob = await MajikSignature.stripFrom(signedBlob);
|
|
229
|
+
|
|
230
|
+
// ── Embed an already-computed signature into a file ──────────────────────────
|
|
231
|
+
const sig2 = await MajikSignature.sign(await file.arrayBuffer(), key);
|
|
232
|
+
const signedBlob2 = await sig2.embedIn(file);
|
|
222
233
|
```
|
|
223
234
|
|
|
224
235
|
---
|
|
225
236
|
|
|
226
237
|
## API Reference
|
|
227
238
|
|
|
228
|
-
###
|
|
239
|
+
### Content Signing (bytes/strings)
|
|
229
240
|
|
|
230
241
|
#### `MajikSignature.sign(content, key, options?)`
|
|
231
242
|
|
|
232
|
-
Sign
|
|
233
|
-
|
|
234
|
-
The key must be unlocked and must have signing keys (`key.hasSigningKeys === true`). Keys created with the current version of Majik Key always include signing keys. Legacy keys can be upgraded by re-importing via `importFromMnemonicBackup()`.
|
|
243
|
+
Sign raw bytes or a string with an unlocked MajikKey.
|
|
235
244
|
|
|
236
245
|
**Parameters:**
|
|
237
246
|
- `content: Uint8Array | string` — Content to sign. Strings are UTF-8 encoded before hashing.
|
|
238
247
|
- `key: MajikKey` — An unlocked MajikKey with signing keys present.
|
|
239
|
-
- `options?: SignOptions`
|
|
248
|
+
- `options?: SignOptions`
|
|
240
249
|
- `contentType?: string` — Advisory label (e.g. `"audio/wav"`, `"application/pdf"`). See `CONTENT_TYPES`.
|
|
241
|
-
- `timestamp?: string` — ISO 8601 timestamp override. Defaults to `new Date().toISOString()`.
|
|
250
|
+
- `timestamp?: string` — ISO 8601 timestamp override. Defaults to `new Date().toISOString()`.
|
|
242
251
|
|
|
243
|
-
**Returns:** `Promise<MajikSignature>`
|
|
252
|
+
**Returns:** `Promise<MajikSignature>`
|
|
244
253
|
|
|
245
|
-
**Throws:** `MajikSignatureKeyError` if the key is locked or has no signing keys.
|
|
246
|
-
|
|
247
|
-
**Example:**
|
|
248
|
-
```typescript
|
|
249
|
-
const signature = await MajikSignature.sign(content, key, {
|
|
250
|
-
contentType: 'application/pdf',
|
|
251
|
-
});
|
|
252
|
-
```
|
|
254
|
+
**Throws:** `MajikSignatureKeyError` if the key is locked or has no signing keys.
|
|
253
255
|
|
|
254
256
|
---
|
|
255
257
|
|
|
256
258
|
#### `MajikSignature.verify(content, signature, publicKeys)`
|
|
257
259
|
|
|
258
|
-
Verify a signature against content and the signer's public keys.
|
|
259
|
-
|
|
260
|
-
No private key is needed. Both Ed25519 and ML-DSA-87 must pass. Returns a structured result rather than throwing on invalid signatures — only throws on unexpected internal errors.
|
|
260
|
+
Verify a signature against content and the signer's public keys. Both Ed25519 and ML-DSA-87 must pass.
|
|
261
261
|
|
|
262
262
|
**Parameters:**
|
|
263
|
-
- `content: Uint8Array | string` — The original content that was signed.
|
|
263
|
+
- `content: Uint8Array | string` — The original content that was signed.
|
|
264
264
|
- `signature: MajikSignature | MajikSignatureJSON` — The signature to verify.
|
|
265
|
-
- `publicKeys: MajikSignerPublicKeys` — Signer's Ed25519
|
|
265
|
+
- `publicKeys: MajikSignerPublicKeys` — Signer's Ed25519 and ML-DSA-87 public keys.
|
|
266
266
|
|
|
267
267
|
**Returns:** `VerificationResult`
|
|
268
268
|
```typescript
|
|
269
269
|
{
|
|
270
|
-
valid: boolean;
|
|
271
|
-
signerId: string;
|
|
272
|
-
contentHash: string;
|
|
273
|
-
timestamp: string;
|
|
274
|
-
contentType?: string;
|
|
275
|
-
}
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
**Throws:** `MajikSignatureVerificationError` on unexpected internal failure.
|
|
279
|
-
|
|
280
|
-
**Example:**
|
|
281
|
-
```typescript
|
|
282
|
-
const result = MajikSignature.verify(content, signature, publicKeys);
|
|
283
|
-
if (result.valid) {
|
|
284
|
-
console.log('Verified — signed by:', result.signerId);
|
|
285
|
-
} else {
|
|
286
|
-
console.log('Invalid signature');
|
|
270
|
+
valid: boolean;
|
|
271
|
+
signerId: string;
|
|
272
|
+
contentHash: string;
|
|
273
|
+
timestamp: string;
|
|
274
|
+
contentType?: string;
|
|
287
275
|
}
|
|
288
276
|
```
|
|
289
277
|
|
|
@@ -291,170 +279,193 @@ if (result.valid) {
|
|
|
291
279
|
|
|
292
280
|
#### `MajikSignature.verifyWithKey(content, signature, key)`
|
|
293
281
|
|
|
294
|
-
Convenience
|
|
295
|
-
|
|
296
|
-
**Parameters:**
|
|
297
|
-
- `content: Uint8Array | string` — The original content.
|
|
298
|
-
- `signature: MajikSignature | MajikSignatureJSON` — The signature to verify.
|
|
299
|
-
- `key: MajikKey` — The MajikKey to verify against. Does not need to be unlocked.
|
|
300
|
-
|
|
301
|
-
**Returns:** `VerificationResult` — same as `verify()`.
|
|
302
|
-
|
|
303
|
-
**Example:**
|
|
304
|
-
```typescript
|
|
305
|
-
const result = MajikSignature.verifyWithKey(content, signature, key);
|
|
306
|
-
console.log('Valid:', result.valid);
|
|
307
|
-
```
|
|
282
|
+
Convenience — verify directly against a MajikKey instance. Works on locked keys.
|
|
308
283
|
|
|
309
284
|
---
|
|
310
285
|
|
|
311
286
|
#### `MajikSignature.publicKeysFromMajikKey(key)`
|
|
312
287
|
|
|
313
|
-
Extract
|
|
314
|
-
|
|
315
|
-
**Parameters:**
|
|
316
|
-
- `key: MajikKey` — Any MajikKey with signing keys (locked or unlocked).
|
|
288
|
+
Extract public keys from a MajikKey for use with `verify()`. Works on locked keys.
|
|
317
289
|
|
|
318
290
|
**Returns:** `MajikSignerPublicKeys`
|
|
319
291
|
```typescript
|
|
320
292
|
{
|
|
321
|
-
signerId: string;
|
|
322
|
-
edPublicKey: Uint8Array; //
|
|
323
|
-
mlDsaPublicKey: Uint8Array; //
|
|
293
|
+
signerId: string;
|
|
294
|
+
edPublicKey: Uint8Array; // 32 bytes
|
|
295
|
+
mlDsaPublicKey: Uint8Array; // 2592 bytes
|
|
324
296
|
}
|
|
325
297
|
```
|
|
326
298
|
|
|
327
|
-
|
|
299
|
+
---
|
|
328
300
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// Store publicKeys or pass to verify()
|
|
333
|
-
```
|
|
301
|
+
#### `MajikSignature.fromJSON(json)` / `MajikSignature.deserialize(base64)`
|
|
302
|
+
|
|
303
|
+
Reconstruct a `MajikSignature` from stored JSON or base64.
|
|
334
304
|
|
|
335
305
|
---
|
|
336
306
|
|
|
337
|
-
|
|
307
|
+
### File Embedding
|
|
308
|
+
|
|
309
|
+
These methods sign or verify files with the signature embedded directly in the file. The file format is auto-detected from magic bytes — no manual hints needed in most cases.
|
|
338
310
|
|
|
339
|
-
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
#### `MajikSignature.signFile(file, key, options?)`
|
|
314
|
+
|
|
315
|
+
Sign a file and embed the signature into it in one call. Strips any existing signature before signing so re-signing is always safe.
|
|
340
316
|
|
|
341
317
|
**Parameters:**
|
|
342
|
-
- `
|
|
318
|
+
- `file: Blob` — The file to sign.
|
|
319
|
+
- `key: MajikKey` — An unlocked MajikKey with signing keys.
|
|
320
|
+
- `options?`
|
|
321
|
+
- `contentType?: string` — Advisory label stored in the envelope.
|
|
322
|
+
- `timestamp?: string` — ISO 8601 override.
|
|
323
|
+
- `mimeType?: string` — Override auto-detected MIME type.
|
|
343
324
|
|
|
344
|
-
**Returns:** `MajikSignature
|
|
325
|
+
**Returns:** `Promise<{ blob: Blob; signature: MajikSignature; handler: string; mimeType: string }>`
|
|
345
326
|
|
|
346
|
-
|
|
327
|
+
- `blob` — The signed file. Same format as the input.
|
|
328
|
+
- `signature` — The `MajikSignature` instance, if you need it separately.
|
|
329
|
+
- `handler` — Which format handler was used (e.g. `"PDF"`, `"WAV"`, `"MP4/MOV"`).
|
|
330
|
+
- `mimeType` — The detected MIME type.
|
|
347
331
|
|
|
348
332
|
**Example:**
|
|
349
333
|
```typescript
|
|
350
|
-
const
|
|
334
|
+
const { blob: signedPdf } = await MajikSignature.signFile(pdfBlob, key);
|
|
335
|
+
// signedPdf is a valid PDF with the signature in its /Info dict + XMP metadata
|
|
351
336
|
```
|
|
352
337
|
|
|
353
338
|
---
|
|
354
339
|
|
|
355
|
-
#### `MajikSignature.
|
|
340
|
+
#### `MajikSignature.verifyFile(file, keyOrPublicKeys, options?)`
|
|
356
341
|
|
|
357
|
-
|
|
342
|
+
Verify a file's embedded signature. Accepts either a `MajikKey` instance or raw `MajikSignerPublicKeys`.
|
|
358
343
|
|
|
359
344
|
**Parameters:**
|
|
360
|
-
- `
|
|
361
|
-
|
|
362
|
-
|
|
345
|
+
- `file: Blob` — The signed file.
|
|
346
|
+
- `keyOrPublicKeys: MajikKey | MajikSignerPublicKeys` — The key or public keys to verify against.
|
|
347
|
+
- `options?`
|
|
348
|
+
- `expectedSignerId?: string` — If provided, checks `signerId` before running crypto.
|
|
349
|
+
- `mimeType?: string` — Override auto-detected MIME type.
|
|
363
350
|
|
|
364
|
-
**
|
|
351
|
+
**Returns:** `Promise<VerificationResult & { handler?: string }>`
|
|
365
352
|
|
|
366
353
|
**Example:**
|
|
367
354
|
```typescript
|
|
368
|
-
const
|
|
355
|
+
const result = await MajikSignature.verifyFile(signedWav, key);
|
|
356
|
+
if (result.valid) {
|
|
357
|
+
console.log('Signed by:', result.signerId);
|
|
358
|
+
console.log('At:', result.timestamp);
|
|
359
|
+
}
|
|
369
360
|
```
|
|
370
361
|
|
|
371
362
|
---
|
|
372
363
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
#### `validate()`
|
|
364
|
+
#### `MajikSignature.extractFrom(file, options?)`
|
|
376
365
|
|
|
377
|
-
|
|
366
|
+
Extract the embedded signature as a fully typed `MajikSignature` instance. Returns `null` if no signature is found.
|
|
378
367
|
|
|
379
|
-
**Returns:** `
|
|
380
|
-
|
|
381
|
-
**Throws:** `MajikSignatureValidationError` on any structural problem.
|
|
368
|
+
**Returns:** `Promise<MajikSignature | null>`
|
|
382
369
|
|
|
383
370
|
**Example:**
|
|
384
371
|
```typescript
|
|
385
|
-
|
|
372
|
+
const sig = await MajikSignature.extractFrom(file);
|
|
373
|
+
if (sig) {
|
|
374
|
+
console.log(sig.signerId, sig.timestamp, sig.contentHash);
|
|
375
|
+
}
|
|
386
376
|
```
|
|
387
377
|
|
|
388
378
|
---
|
|
389
379
|
|
|
390
|
-
#### `
|
|
380
|
+
#### `MajikSignature.stripFrom(file, options?)`
|
|
391
381
|
|
|
392
|
-
|
|
382
|
+
Return a clean copy of the file with any embedded signature removed. The returned bytes are exactly what was originally signed.
|
|
393
383
|
|
|
394
|
-
**Returns:** `
|
|
384
|
+
**Returns:** `Promise<Blob>`
|
|
395
385
|
|
|
396
386
|
**Example:**
|
|
397
387
|
```typescript
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
388
|
+
const original = await MajikSignature.stripFrom(signedMp4);
|
|
389
|
+
// original bytes are what was hashed when the signature was created
|
|
401
390
|
```
|
|
402
391
|
|
|
403
392
|
---
|
|
404
393
|
|
|
405
|
-
#### `
|
|
406
|
-
|
|
407
|
-
Extract the signer's public keys from the envelope itself.
|
|
394
|
+
#### `MajikSignature.isSigned(file, options?)`
|
|
408
395
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
**Returns:** `MajikSignerPublicKeys`
|
|
396
|
+
Check whether a file contains an embedded signature. Does not verify — purely a structural presence check. Useful as a fast guard before verification.
|
|
412
397
|
|
|
413
|
-
**
|
|
398
|
+
**Returns:** `Promise<boolean>`
|
|
414
399
|
|
|
415
400
|
**Example:**
|
|
416
401
|
```typescript
|
|
417
|
-
|
|
418
|
-
|
|
402
|
+
if (await MajikSignature.isSigned(file)) {
|
|
403
|
+
const result = await MajikSignature.verifyFile(file, key);
|
|
404
|
+
}
|
|
419
405
|
```
|
|
420
406
|
|
|
421
407
|
---
|
|
422
408
|
|
|
423
|
-
#### `
|
|
409
|
+
#### `signature.embedIn(file, options?)` *(instance method)*
|
|
424
410
|
|
|
425
|
-
|
|
411
|
+
Embed this `MajikSignature` instance into a file. Call on an existing instance when you have already signed the content separately.
|
|
426
412
|
|
|
427
|
-
**
|
|
413
|
+
> **Note:** The signature must have been created from the original file bytes **before** embedding. Use `signFile()` if you want signing and embedding together.
|
|
414
|
+
|
|
415
|
+
**Returns:** `Promise<Blob>`
|
|
428
416
|
|
|
429
417
|
**Example:**
|
|
430
418
|
```typescript
|
|
431
|
-
const
|
|
432
|
-
await
|
|
419
|
+
const originalBytes = new Uint8Array(await file.arrayBuffer());
|
|
420
|
+
const sig = await MajikSignature.sign(originalBytes, key);
|
|
421
|
+
const signedBlob = await sig.embedIn(file);
|
|
433
422
|
```
|
|
434
423
|
|
|
435
424
|
---
|
|
436
425
|
|
|
437
|
-
|
|
426
|
+
### Lower-Level Embed API
|
|
438
427
|
|
|
439
|
-
|
|
428
|
+
For advanced use cases — custom handler registration, explicit format control, or accessing handler metadata — the underlying `MajikSignatureEmbed` class is also exported.
|
|
440
429
|
|
|
441
|
-
|
|
430
|
+
```typescript
|
|
431
|
+
import { MajikSignatureEmbed } from '@majikah/majik-signature';
|
|
442
432
|
|
|
443
|
-
|
|
433
|
+
// Register a custom handler for an unsupported format
|
|
434
|
+
MajikSignatureEmbed.registry.register(new MyCustomHandler());
|
|
444
435
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
436
|
+
// List all registered handlers
|
|
437
|
+
console.log(MajikSignatureEmbed.listHandlers());
|
|
438
|
+
// → ['PDF', 'PNG', 'JPEG', 'WAV', 'MP3', 'MP4/MOV', 'FLAC', 'MKV/WebM',
|
|
439
|
+
// 'Office (DOCX/XLSX/PPTX/ODF)', 'Text/Markup/Source',
|
|
440
|
+
// 'Fallback (Universal Trailer)']
|
|
441
|
+
|
|
442
|
+
// Force the Tier-2 trailer even for natively supported formats
|
|
443
|
+
const { blob } = await MajikSignatureEmbed.embed(file, sig, { forceFallback: true });
|
|
449
444
|
```
|
|
450
445
|
|
|
451
446
|
---
|
|
452
447
|
|
|
453
|
-
|
|
448
|
+
### Instance Methods
|
|
449
|
+
|
|
450
|
+
#### `validate()`
|
|
451
|
+
Validate the envelope's internal structure without performing cryptographic verification. Throws `MajikSignatureValidationError` on any structural problem.
|
|
454
452
|
|
|
455
|
-
|
|
453
|
+
#### `isValid()`
|
|
454
|
+
Returns `true` if the envelope is structurally valid. Never throws — safe to use as a boolean guard.
|
|
455
|
+
|
|
456
|
+
#### `extractPublicKeys()`
|
|
457
|
+
Extract the signer's public keys from the envelope.
|
|
456
458
|
|
|
457
|
-
|
|
459
|
+
> ⚠️ Public keys embedded in the envelope are self-reported by the signer. Always cross-check `signerId` against a trusted source before trusting extracted keys for verification.
|
|
460
|
+
|
|
461
|
+
#### `toJSON()`
|
|
462
|
+
Export the full signature envelope as a plain JSON object.
|
|
463
|
+
|
|
464
|
+
#### `serialize()`
|
|
465
|
+
Serialize the envelope to a compact base64 string. Suitable for embedding in database fields, HTTP headers, file metadata, or sidecar files.
|
|
466
|
+
|
|
467
|
+
#### `toString()`
|
|
468
|
+
Alias for `serialize()`.
|
|
458
469
|
|
|
459
470
|
---
|
|
460
471
|
|
|
@@ -474,250 +485,235 @@ Alias for `serialize()`. Returns the base64 serialized envelope.
|
|
|
474
485
|
|
|
475
486
|
---
|
|
476
487
|
|
|
477
|
-
##
|
|
488
|
+
## Supported File Formats
|
|
478
489
|
|
|
479
|
-
###
|
|
480
|
-
```typescript
|
|
481
|
-
import { MajikKey } from '@majikah/majik-key';
|
|
482
|
-
import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
|
|
490
|
+
### Tier 1 — Native metadata
|
|
483
491
|
|
|
484
|
-
|
|
485
|
-
const mnemonic = MajikKey.generateMnemonic();
|
|
486
|
-
const key = await MajikKey.create(mnemonic, 'passphrase', 'Author Key');
|
|
492
|
+
The signature is stored in each format's built-in metadata container. The file remains structurally valid and the signature survives round-trips through standard tools.
|
|
487
493
|
|
|
488
|
-
|
|
489
|
-
|
|
494
|
+
| Format | Embedding mechanism |
|
|
495
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
496
|
+
| **PDF** | `/Info` dictionary custom key + XMP metadata stream. Visible in File → Properties in most PDF viewers. |
|
|
497
|
+
| **PNG** | `iTXt` chunk with keyword `majik-signature` |
|
|
498
|
+
| **JPEG / JPG** | Custom `APP15` marker segment |
|
|
499
|
+
| **WAV / WAVE** | RIFF `LIST INFO` chunk — `ISIG` entry |
|
|
500
|
+
| **MP3** | ID3v2 `TXXX` frame with description `MAJIK-SIGNATURE` |
|
|
501
|
+
| **MP4 / MOV / M4A / M4V** | `moov → udta → majk` box |
|
|
502
|
+
| **FLAC** | `VORBIS_COMMENT` block — `MAJIK-SIGNATURE=` field |
|
|
503
|
+
| **MKV / WebM** | Append-safe binary trailer |
|
|
504
|
+
| **DOCX / XLSX / PPTX** | `majik-signature.json` entry inside the ZIP container |
|
|
505
|
+
| **ODF (ODT/ODS/ODP)** | Same as OOXML — ZIP entry |
|
|
506
|
+
| **HTML / XML / SVG / Markdown** | `<!-- MAJIK-SIGNATURE-BEGIN -->` block appended at end |
|
|
507
|
+
| **Plain text / JSON / CSV / source code** | Same comment block |
|
|
490
508
|
|
|
491
|
-
|
|
492
|
-
Party A agrees to deliver the software by March 31, 2026.
|
|
493
|
-
`;
|
|
509
|
+
### Tier 2 — Universal trailer
|
|
494
510
|
|
|
495
|
-
|
|
496
|
-
const signature = await MajikSignature.sign(document, key, {
|
|
497
|
-
contentType: CONTENT_TYPES.TEXT,
|
|
498
|
-
});
|
|
511
|
+
For any format not covered above, a self-describing binary trailer is appended:
|
|
499
512
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
// Verify
|
|
505
|
-
const result = MajikSignature.verifyWithKey(document, signature, key);
|
|
506
|
-
console.log('✅ Verified:', result.valid); // true
|
|
513
|
+
```
|
|
514
|
+
[original file bytes][signature JSON UTF-8][8-byte payload length LE][8-byte magic: MAJIKSIG]
|
|
515
|
+
```
|
|
507
516
|
|
|
508
|
-
|
|
509
|
-
const tampered = document + ' (modified)';
|
|
510
|
-
const tamperResult = MajikSignature.verifyWithKey(tampered, signature, key);
|
|
511
|
-
console.log('❌ Tampered rejected:', tamperResult.valid); // false
|
|
512
|
-
}
|
|
517
|
+
The magic bytes at the end allow detection and clean stripping from any file without knowing its format. Most parsers and players ignore trailing bytes.
|
|
513
518
|
|
|
514
|
-
|
|
515
|
-
```
|
|
519
|
+
> **Re-mux warning:** For MKV/WebM and the Tier-2 fallback, the embedded signature will be stripped if the file is re-encoded or re-muxed through a tool that rewrites the container. For MP4, DOCX, and all Tier-1 native-metadata formats, the signature survives standard open → save round-trips.
|
|
516
520
|
|
|
517
521
|
---
|
|
518
522
|
|
|
519
|
-
|
|
523
|
+
## Usage Examples
|
|
524
|
+
|
|
525
|
+
### Example 1: Sign and Verify a Text Document
|
|
520
526
|
```typescript
|
|
521
527
|
import { MajikKey } from '@majikah/majik-key';
|
|
522
528
|
import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
|
|
523
|
-
import { readFileSync } from 'fs';
|
|
524
|
-
|
|
525
|
-
async function signFile() {
|
|
526
|
-
const mnemonic = MajikKey.generateMnemonic();
|
|
527
|
-
const key = await MajikKey.create(mnemonic, 'passphrase', 'Publisher Key');
|
|
528
529
|
|
|
529
|
-
|
|
530
|
-
|
|
530
|
+
const mnemonic = MajikKey.generateMnemonic();
|
|
531
|
+
const key = await MajikKey.create(mnemonic, 'passphrase', 'Author Key');
|
|
531
532
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
533
|
+
const document = `
|
|
534
|
+
AGREEMENT
|
|
535
|
+
This agreement is entered into on January 1, 2026.
|
|
536
|
+
Party A agrees to deliver the software by March 31, 2026.
|
|
537
|
+
`;
|
|
535
538
|
|
|
536
|
-
|
|
537
|
-
|
|
539
|
+
const signature = await MajikSignature.sign(document, key, {
|
|
540
|
+
contentType: CONTENT_TYPES.TEXT,
|
|
541
|
+
});
|
|
538
542
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
console.log('✅ File verified:', result.valid); // true
|
|
542
|
-
}
|
|
543
|
+
const result = MajikSignature.verifyWithKey(document, signature, key);
|
|
544
|
+
console.log('Valid:', result.valid); // true
|
|
543
545
|
|
|
544
|
-
|
|
546
|
+
// Tamper detection
|
|
547
|
+
const tampered = document + ' (modified)';
|
|
548
|
+
const tamperResult = MajikSignature.verifyWithKey(tampered, signature, key);
|
|
549
|
+
console.log('Tampered rejected:', tamperResult.valid); // false
|
|
545
550
|
```
|
|
546
551
|
|
|
547
552
|
---
|
|
548
553
|
|
|
549
|
-
### Example
|
|
554
|
+
### Example 2: Sign a File and Embed the Signature
|
|
550
555
|
```typescript
|
|
551
556
|
import { MajikKey } from '@majikah/majik-key';
|
|
552
|
-
import { MajikSignature
|
|
557
|
+
import { MajikSignature } from '@majikah/majik-signature';
|
|
553
558
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const key = await MajikKey.create(mnemonic, 'passphrase', 'API Key');
|
|
557
|
-
|
|
558
|
-
const payload = {
|
|
559
|
-
userId: 'usr_abc123',
|
|
560
|
-
action: 'transfer',
|
|
561
|
-
amount: 1000,
|
|
562
|
-
currency: 'USD',
|
|
563
|
-
nonce: crypto.randomUUID(),
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
// Always sign the canonical string — agree on stringify format
|
|
567
|
-
const content = JSON.stringify(payload);
|
|
568
|
-
|
|
569
|
-
const signature = await MajikSignature.sign(content, key, {
|
|
570
|
-
contentType: CONTENT_TYPES.JSON,
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
// Attach to the API response
|
|
574
|
-
const response = {
|
|
575
|
-
data: payload,
|
|
576
|
-
signature: signature.toJSON(),
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
// On the receiving end — verify before processing
|
|
580
|
-
const result = MajikSignature.verifyWithKey(
|
|
581
|
-
JSON.stringify(response.data),
|
|
582
|
-
response.signature,
|
|
583
|
-
key,
|
|
584
|
-
);
|
|
585
|
-
|
|
586
|
-
console.log('✅ Payload verified:', result.valid); // true
|
|
587
|
-
}
|
|
559
|
+
const mnemonic = MajikKey.generateMnemonic();
|
|
560
|
+
const key = await MajikKey.create(mnemonic, 'passphrase', 'Artist Key');
|
|
588
561
|
|
|
589
|
-
|
|
562
|
+
// Works for any file — PDF, WAV, MP3, MP4, PNG, DOCX, etc.
|
|
563
|
+
const { blob: signedFile, handler } = await MajikSignature.signFile(file, key);
|
|
564
|
+
|
|
565
|
+
console.log('Signed using handler:', handler);
|
|
566
|
+
// e.g. "PDF", "WAV", "MP4/MOV", "Office (DOCX/XLSX/PPTX/ODF)"
|
|
567
|
+
|
|
568
|
+
// The signed file is the same format — upload or save it directly
|
|
569
|
+
await uploadFile(signedFile);
|
|
590
570
|
```
|
|
591
571
|
|
|
592
572
|
---
|
|
593
573
|
|
|
594
|
-
### Example
|
|
574
|
+
### Example 3: Verify an Embedded Signature
|
|
595
575
|
```typescript
|
|
596
576
|
import { MajikKey } from '@majikah/majik-key';
|
|
597
577
|
import { MajikSignature } from '@majikah/majik-signature';
|
|
598
578
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const key = await MajikKey.create(mnemonic, 'passphrase', 'Storage Key');
|
|
579
|
+
// key does NOT need to be unlocked for verification
|
|
580
|
+
const key = MajikKey.fromJSON(storedKeyJson);
|
|
602
581
|
|
|
603
|
-
|
|
604
|
-
const signature = await MajikSignature.sign(content, key);
|
|
582
|
+
const result = await MajikSignature.verifyFile(downloadedFile, key);
|
|
605
583
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
584
|
+
if (result.valid) {
|
|
585
|
+
console.log('Authentic. Signed by:', result.signerId);
|
|
586
|
+
console.log('Signed at:', result.timestamp);
|
|
587
|
+
} else {
|
|
588
|
+
console.log('Invalid or tampered:', result.reason);
|
|
589
|
+
}
|
|
590
|
+
```
|
|
609
591
|
|
|
610
|
-
|
|
611
|
-
const b64 = signature.serialize();
|
|
612
|
-
localStorage.setItem('doc_sig_001_b64', b64);
|
|
592
|
+
---
|
|
613
593
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
594
|
+
### Example 4: Sign a Binary File (Node.js)
|
|
595
|
+
```typescript
|
|
596
|
+
import { MajikKey } from '@majikah/majik-key';
|
|
597
|
+
import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
|
|
598
|
+
import { readFileSync } from 'fs';
|
|
599
|
+
|
|
600
|
+
const key = await MajikKey.create(mnemonic, 'passphrase', 'Publisher Key');
|
|
601
|
+
const fileBytes = new Uint8Array(readFileSync('./release.zip'));
|
|
602
|
+
|
|
603
|
+
// Option A: Sign bytes, store signature separately
|
|
604
|
+
const signature = await MajikSignature.sign(fileBytes, key, {
|
|
605
|
+
contentType: 'application/zip',
|
|
606
|
+
});
|
|
607
|
+
const result = MajikSignature.verifyWithKey(fileBytes, signature, key);
|
|
608
|
+
console.log('Verified:', result.valid); // true
|
|
618
609
|
|
|
619
|
-
|
|
610
|
+
// Option B: Sign and embed into the file itself
|
|
611
|
+
const fileBlob = new Blob([fileBytes], { type: 'application/zip' });
|
|
612
|
+
const { blob: signedBlob } = await MajikSignature.signFile(fileBlob, key);
|
|
620
613
|
```
|
|
621
614
|
|
|
622
615
|
---
|
|
623
616
|
|
|
624
|
-
### Example 5:
|
|
617
|
+
### Example 5: Sign a JSON Payload
|
|
625
618
|
```typescript
|
|
626
619
|
import { MajikKey } from '@majikah/majik-key';
|
|
627
|
-
import { MajikSignature } from '@majikah/majik-signature';
|
|
620
|
+
import { MajikSignature, CONTENT_TYPES } from '@majikah/majik-signature';
|
|
628
621
|
|
|
629
|
-
|
|
630
|
-
// Reload a stored key (locked) and a stored signature
|
|
631
|
-
const keyJson = JSON.parse(localStorage.getItem('myKey')!);
|
|
632
|
-
const key = MajikKey.fromJSON(keyJson);
|
|
633
|
-
// key does NOT need to be unlocked for verification
|
|
634
|
-
|
|
635
|
-
const content = 'Original content of the certified document.';
|
|
636
|
-
|
|
637
|
-
// Option A: From stored JSON
|
|
638
|
-
const storedJson = JSON.parse(localStorage.getItem('doc_sig_001')!);
|
|
639
|
-
const signatureA = MajikSignature.fromJSON(storedJson);
|
|
640
|
-
const resultA = MajikSignature.verifyWithKey(content, signatureA, key);
|
|
641
|
-
console.log('✅ JSON verify:', resultA.valid); // true
|
|
642
|
-
|
|
643
|
-
// Option B: From stored base64
|
|
644
|
-
const storedB64 = localStorage.getItem('doc_sig_001_b64')!;
|
|
645
|
-
const signatureB = MajikSignature.deserialize(storedB64);
|
|
646
|
-
const resultB = MajikSignature.verifyWithKey(content, signatureB, key);
|
|
647
|
-
console.log('✅ Base64 verify:', resultB.valid); // true
|
|
648
|
-
}
|
|
622
|
+
const key = await MajikKey.create(mnemonic, 'passphrase', 'API Key');
|
|
649
623
|
|
|
650
|
-
|
|
624
|
+
const payload = {
|
|
625
|
+
userId: 'usr_abc123',
|
|
626
|
+
action: 'transfer',
|
|
627
|
+
amount: 1000,
|
|
628
|
+
currency: 'USD',
|
|
629
|
+
nonce: crypto.randomUUID(),
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// Always sign the canonical string — agree on stringify format
|
|
633
|
+
const content = JSON.stringify(payload);
|
|
634
|
+
const signature = await MajikSignature.sign(content, key, {
|
|
635
|
+
contentType: CONTENT_TYPES.JSON,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const response = { data: payload, signature: signature.toJSON() };
|
|
639
|
+
|
|
640
|
+
// On the receiving end
|
|
641
|
+
const result = MajikSignature.verifyWithKey(
|
|
642
|
+
JSON.stringify(response.data),
|
|
643
|
+
response.signature,
|
|
644
|
+
key,
|
|
645
|
+
);
|
|
646
|
+
console.log('Payload verified:', result.valid); // true
|
|
651
647
|
```
|
|
652
648
|
|
|
653
649
|
---
|
|
654
650
|
|
|
655
|
-
### Example 6:
|
|
651
|
+
### Example 6: Extract and Inspect an Embedded Signature
|
|
656
652
|
```typescript
|
|
657
653
|
import { MajikSignature } from '@majikah/majik-signature';
|
|
658
|
-
import type { MajikSignerPublicKeys } from '@majikah/majik-signature';
|
|
659
654
|
|
|
660
|
-
//
|
|
661
|
-
|
|
655
|
+
// Extract without verifying — useful for inspecting provenance metadata
|
|
656
|
+
const sig = await MajikSignature.extractFrom(file);
|
|
662
657
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const storedSig = MajikSignature.fromJSON(/* stored JSON */);
|
|
658
|
+
if (sig) {
|
|
659
|
+
console.log('Signer ID:', sig.signerId);
|
|
660
|
+
console.log('Signed at:', sig.timestamp);
|
|
661
|
+
console.log('Content hash:', sig.contentHash);
|
|
662
|
+
console.log('Content type:', sig.contentType);
|
|
663
|
+
} else {
|
|
664
|
+
console.log('No signature found');
|
|
665
|
+
}
|
|
666
|
+
```
|
|
673
667
|
|
|
674
|
-
|
|
675
|
-
if (storedSig.signerId !== publicKeys.signerId) {
|
|
676
|
-
console.error('❌ Signer mismatch — signature is not from expected identity');
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
668
|
+
---
|
|
679
669
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
670
|
+
### Example 7: Re-sign a File
|
|
671
|
+
```typescript
|
|
672
|
+
import { MajikSignature } from '@majikah/majik-signature';
|
|
684
673
|
|
|
685
|
-
|
|
674
|
+
// signFile() strips any existing signature before signing — always safe to call
|
|
675
|
+
const { blob: resignedFile } = await MajikSignature.signFile(previouslySignedFile, key);
|
|
676
|
+
// The new signature covers the original content bytes, not the previously signed file
|
|
686
677
|
```
|
|
687
678
|
|
|
688
679
|
---
|
|
689
680
|
|
|
690
|
-
### Example
|
|
681
|
+
### Example 8: Verify Using Only Public Keys
|
|
691
682
|
```typescript
|
|
692
|
-
import {
|
|
693
|
-
import {
|
|
683
|
+
import { MajikSignature } from '@majikah/majik-signature';
|
|
684
|
+
import type { MajikSignerPublicKeys } from '@majikah/majik-signature';
|
|
694
685
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
686
|
+
// Public keys received from a trusted source (e.g. a user profile API)
|
|
687
|
+
const publicKeys: MajikSignerPublicKeys = {
|
|
688
|
+
signerId: 'base64-fingerprint-of-the-signer',
|
|
689
|
+
edPublicKey: new Uint8Array(/* 32 bytes */),
|
|
690
|
+
mlDsaPublicKey: new Uint8Array(/* 2592 bytes */),
|
|
691
|
+
};
|
|
698
692
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
693
|
+
// Verify embedded signature without a MajikKey instance
|
|
694
|
+
const result = await MajikSignature.verifyFile(signedFile, publicKeys, {
|
|
695
|
+
expectedSignerId: publicKeys.signerId,
|
|
696
|
+
});
|
|
697
|
+
console.log('Verified:', result.valid);
|
|
698
|
+
```
|
|
702
699
|
|
|
703
|
-
|
|
704
|
-
file.type || CONTENT_TYPES.BINARY;
|
|
700
|
+
---
|
|
705
701
|
|
|
706
|
-
|
|
702
|
+
### Example 9: Serialize and Store a Signature
|
|
703
|
+
```typescript
|
|
704
|
+
const signature = await MajikSignature.sign(content, key);
|
|
707
705
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
console.log('Content Type:', signature.contentType);
|
|
712
|
-
console.log('Signer:', signature.signerId);
|
|
706
|
+
// Store as JSON
|
|
707
|
+
const json = signature.toJSON();
|
|
708
|
+
await db.signatures.insert({ id: docId, sig: json });
|
|
713
709
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
710
|
+
// Store as base64 (HTTP header, metadata field, etc.)
|
|
711
|
+
const b64 = signature.serialize();
|
|
712
|
+
res.setHeader('X-Majik-Signature', b64);
|
|
717
713
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
714
|
+
// Restore later
|
|
715
|
+
const sigFromJson = MajikSignature.fromJSON(json);
|
|
716
|
+
const sigFromB64 = MajikSignature.deserialize(b64);
|
|
721
717
|
```
|
|
722
718
|
|
|
723
719
|
---
|
|
@@ -725,6 +721,7 @@ async function signMediaFile(file: File) {
|
|
|
725
721
|
## Signature Envelope
|
|
726
722
|
|
|
727
723
|
Every `MajikSignature` serializes to the following JSON structure:
|
|
724
|
+
|
|
728
725
|
```json
|
|
729
726
|
{
|
|
730
727
|
"version": 1,
|
|
@@ -760,29 +757,31 @@ The dominant contributor is `mlDsaSignature` (~6 KB base64) and `signerMlDsaPubl
|
|
|
760
757
|
- **Forgery resistance (classical)**: Ed25519 provides 128-bit classical security
|
|
761
758
|
- **Forgery resistance (post-quantum)**: ML-DSA-87 provides NIST Category 5 post-quantum security
|
|
762
759
|
- **Hybrid downgrade resistance**: Both algorithms must be broken simultaneously to forge — a break in one is not sufficient
|
|
760
|
+
- **Embed integrity**: File embedding always signs original bytes — the embedding container is never part of what's signed
|
|
763
761
|
|
|
764
762
|
### What is Your Responsibility
|
|
765
763
|
|
|
766
|
-
- **Signer identity verification**: The library proves content was signed by a specific key. It does not prove who owns that key in the real world.
|
|
767
|
-
- **Byte-for-byte content consistency**: The same bytes must be passed to both `sign()` and `verify()`. For strings, both sides must use
|
|
768
|
-
- **Key upgrade**: Legacy MajikKey accounts without signing keys must be re-imported via `importFromMnemonicBackup()` before signing.
|
|
764
|
+
- **Signer identity verification**: The library proves content was signed by a specific key. It does not prove who owns that key in the real world. Maintain the mapping between `signerId` (fingerprint) and a real-world identity through your own means.
|
|
765
|
+
- **Byte-for-byte content consistency**: The same bytes must be passed to both `sign()` and `verify()`. For strings, both sides must use UTF-8. For JSON, both sides must use the same `JSON.stringify()` output.
|
|
766
|
+
- **Key upgrade**: Legacy MajikKey accounts without signing keys must be re-imported via `importFromMnemonicBackup()` before signing. Check with `key.hasSigningKeys`.
|
|
769
767
|
|
|
770
768
|
### What NOT to Do
|
|
771
769
|
|
|
772
770
|
❌ **DON'T** trust `extractPublicKeys()` without cross-checking `signerId` against a known trusted source
|
|
773
|
-
❌ **DON'T** sign JSON by passing the object directly — always `JSON.stringify()` first
|
|
771
|
+
❌ **DON'T** sign JSON by passing the object directly — always `JSON.stringify()` first
|
|
774
772
|
❌ **DON'T** transform file bytes (compress, transcode, re-encode) between signing and verification
|
|
775
|
-
❌ **DON'T** pass a locked key to `sign()` — call `unlock()` first
|
|
773
|
+
❌ **DON'T** pass a locked key to `sign()` or `signFile()` — call `unlock()` first
|
|
776
774
|
❌ **DON'T** use `contentType` as a security mechanism — it is advisory only and not enforced
|
|
775
|
+
❌ **DON'T** assume a Tier-2 trailer signature survives re-muxing — use native-metadata formats where durability matters
|
|
777
776
|
|
|
778
777
|
### What TO Do
|
|
779
778
|
|
|
780
|
-
✅ **DO** verify `result.signerId` matches a known trusted fingerprint after calling `verify()`
|
|
781
|
-
✅ **DO** use `verifyWithKey()` when you have the signer's `MajikKey` — it handles key extraction safely
|
|
782
|
-
✅ **DO** lock the key immediately after signing
|
|
783
|
-
✅ **DO**
|
|
779
|
+
✅ **DO** verify `result.signerId` matches a known trusted fingerprint after calling `verify()` or `verifyFile()`
|
|
780
|
+
✅ **DO** use `verifyWithKey()` / `verifyFile(key)` when you have the signer's `MajikKey` — it handles key extraction safely
|
|
781
|
+
✅ **DO** lock the key immediately after signing — `key.lock()` purges secret keys from memory
|
|
782
|
+
✅ **DO** use `signFile()` for media and documents to keep signature and content together
|
|
783
|
+
✅ **DO** use `isSigned()` as a fast guard before calling `verifyFile()` in hot paths
|
|
784
784
|
✅ **DO** use `CONTENT_TYPES` constants for standard content type labels
|
|
785
|
-
✅ **DO** call `key.lock()` after `sign()` completes — do not keep keys unlocked longer than needed
|
|
786
785
|
|
|
787
786
|
---
|
|
788
787
|
|
|
@@ -794,9 +793,7 @@ Seed phrase account library — required peer dependency for signing.
|
|
|
794
793
|
### [Majik Message](https://message.majikah.solutions)
|
|
795
794
|
Secure messaging platform using Majik Keys and Majik Signatures for identity-bound communication.
|
|
796
795
|
|
|
797
|
-
[Read Docs](https://majikah.solutions/products/majik-message/docs)
|
|
798
|
-
|
|
799
|
-
Also available on [Microsoft Store](https://apps.microsoft.com/detail/9pmjgvzzjspn) for free.
|
|
796
|
+
[Read Docs](https://majikah.solutions/products/majik-message/docs) · [Microsoft Store](https://apps.microsoft.com/detail/9pmjgvzzjspn)
|
|
800
797
|
|
|
801
798
|
---
|
|
802
799
|
|