@majikah/majik-signature 0.0.4 → 0.0.6
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.
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* handlers/pdf.ts — PDF handler
|
|
2
|
+
* handlers/pdf.ts — PDF handler
|
|
3
3
|
*
|
|
4
|
-
* Embeds the MajikSignature
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Embeds the MajikSignature as a binary trailer appended after the PDF's
|
|
5
|
+
* last %%EOF marker. This approach is:
|
|
6
|
+
* - Deterministic: strip() is a pure byte slice, no parsing
|
|
7
|
+
* - Non-destructive: the PDF remains valid and openable
|
|
8
|
+
* - Spec-compliant: appending after %%EOF is allowed by PDF 1.7 §7.5.6
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* If you want human-readable metadata visible in Adobe/Preview, call
|
|
11
|
+
* MajikSignatureClient.addDisplayMetadata() after signing — that is a
|
|
12
|
+
* separate display-only step and does not affect verification.
|
|
10
13
|
*/
|
|
11
14
|
import { FormatHandler } from "../../types";
|
|
12
15
|
export declare class PdfHandler implements FormatHandler {
|
|
13
16
|
readonly name = "PDF";
|
|
14
17
|
readonly supportedMimeTypes: readonly ["application/pdf"];
|
|
18
|
+
private _findMagic;
|
|
15
19
|
canHandle(bytes: Uint8Array, mimeType?: string): boolean;
|
|
16
20
|
embed(bytes: Uint8Array, signatureJson: string): Promise<Uint8Array>;
|
|
17
|
-
extract(bytes: Uint8Array): Promise<string | null>;
|
|
18
21
|
strip(bytes: Uint8Array): Promise<Uint8Array>;
|
|
19
|
-
|
|
20
|
-
private _extractFromXMP;
|
|
22
|
+
extract(bytes: Uint8Array): Promise<string | null>;
|
|
21
23
|
}
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* handlers/pdf.ts — PDF handler
|
|
2
|
+
* handlers/pdf.ts — PDF handler
|
|
3
3
|
*
|
|
4
|
-
* Embeds the MajikSignature
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Embeds the MajikSignature as a binary trailer appended after the PDF's
|
|
5
|
+
* last %%EOF marker. This approach is:
|
|
6
|
+
* - Deterministic: strip() is a pure byte slice, no parsing
|
|
7
|
+
* - Non-destructive: the PDF remains valid and openable
|
|
8
|
+
* - Spec-compliant: appending after %%EOF is allowed by PDF 1.7 §7.5.6
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* If you want human-readable metadata visible in Adobe/Preview, call
|
|
11
|
+
* MajikSignatureClient.addDisplayMetadata() after signing — that is a
|
|
12
|
+
* separate display-only step and does not affect verification.
|
|
10
13
|
*/
|
|
11
|
-
import { PDFDocument, PDFName, PDFString, PDFHexString } from "pdf-lib";
|
|
12
|
-
import { MAJIK_NAMESPACE, SIGNATURE_KEY } from "../constants";
|
|
13
14
|
const PDF_MAGIC = [0x25, 0x50, 0x44, 0x46]; // %PDF
|
|
15
|
+
const MAGIC = new TextEncoder().encode("\n%%MajikSig%%\n");
|
|
14
16
|
export class PdfHandler {
|
|
15
17
|
name = "PDF";
|
|
16
18
|
supportedMimeTypes = ["application/pdf"];
|
|
19
|
+
_findMagic(bytes) {
|
|
20
|
+
outer: for (let i = bytes.length - MAGIC.length; i >= 0; i--) {
|
|
21
|
+
if (bytes[i] !== MAGIC[0])
|
|
22
|
+
continue;
|
|
23
|
+
for (let j = 1; j < MAGIC.length; j++) {
|
|
24
|
+
if (bytes[i + j] !== MAGIC[j])
|
|
25
|
+
continue outer;
|
|
26
|
+
}
|
|
27
|
+
return i;
|
|
28
|
+
}
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
17
31
|
canHandle(bytes, mimeType) {
|
|
18
32
|
if (mimeType === "application/pdf")
|
|
19
33
|
return true;
|
|
@@ -24,160 +38,27 @@ export class PdfHandler {
|
|
|
24
38
|
bytes[3] === PDF_MAGIC[3]);
|
|
25
39
|
}
|
|
26
40
|
async embed(bytes, signatureJson) {
|
|
27
|
-
// Strip any existing signature first
|
|
28
41
|
const clean = await this.strip(bytes);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// (some PDF viewers show keywords in properties)
|
|
39
|
-
try {
|
|
40
|
-
pdf.setKeywords([`${SIGNATURE_KEY}:${signatureJson}`]);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
// Non-critical — ignore if keywords already set in a way we can't override
|
|
44
|
-
}
|
|
45
|
-
// ── 2. XMP metadata stream ───────────────────────────────────────────────
|
|
46
|
-
const xmp = this._buildXMP(signatureJson);
|
|
47
|
-
try {
|
|
48
|
-
pdf.setProducer("MajikSignatureEmbed/1.0");
|
|
49
|
-
// Set raw XMP via the underlying context
|
|
50
|
-
const metadataStream = pdf.context.stream(xmp, {
|
|
51
|
-
Type: "Metadata",
|
|
52
|
-
Subtype: "XML",
|
|
53
|
-
});
|
|
54
|
-
const metadataRef = pdf.context.register(metadataStream);
|
|
55
|
-
pdf.catalog.set(PDFName.of("Metadata"), metadataRef);
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
// XMP embed is best-effort; /Info key is the primary store
|
|
59
|
-
}
|
|
60
|
-
return pdf.save();
|
|
61
|
-
}
|
|
62
|
-
async extract(bytes) {
|
|
63
|
-
try {
|
|
64
|
-
const pdf = await PDFDocument.load(bytes, {
|
|
65
|
-
ignoreEncryption: true,
|
|
66
|
-
updateMetadata: false,
|
|
67
|
-
});
|
|
68
|
-
// ── Try /Info dictionary ──
|
|
69
|
-
const infoDict = pdf.context.lookup(pdf.context.trailerInfo.Info);
|
|
70
|
-
if (infoDict) {
|
|
71
|
-
const sigValue = infoDict.get(PDFName.of(SIGNATURE_KEY));
|
|
72
|
-
if (sigValue) {
|
|
73
|
-
const raw = sigValue instanceof PDFHexString
|
|
74
|
-
? sigValue.decodeText()
|
|
75
|
-
: sigValue.asString();
|
|
76
|
-
if (raw)
|
|
77
|
-
return raw;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// ── Try Keywords field ──
|
|
81
|
-
try {
|
|
82
|
-
const keywords = pdf.getKeywords();
|
|
83
|
-
if (keywords) {
|
|
84
|
-
const prefix = `${SIGNATURE_KEY}:`;
|
|
85
|
-
if (keywords.startsWith(prefix)) {
|
|
86
|
-
return keywords.slice(prefix.length);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
/* ignore */
|
|
92
|
-
}
|
|
93
|
-
// ── Try XMP metadata stream ──
|
|
94
|
-
try {
|
|
95
|
-
const metadataRef = pdf.catalog.get(PDFName.of("Metadata"));
|
|
96
|
-
if (metadataRef) {
|
|
97
|
-
const metadataStream = pdf.context.lookup(metadataRef);
|
|
98
|
-
if (metadataStream) {
|
|
99
|
-
const xmpBytes = metadataStream.getContents();
|
|
100
|
-
const xmpStr = new TextDecoder().decode(xmpBytes);
|
|
101
|
-
const extracted = this._extractFromXMP(xmpStr);
|
|
102
|
-
if (extracted)
|
|
103
|
-
return extracted;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
/* ignore */
|
|
109
|
-
}
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
42
|
+
const sigBytes = new TextEncoder().encode(signatureJson);
|
|
43
|
+
const lenBytes = new Uint8Array(4);
|
|
44
|
+
new DataView(lenBytes.buffer).setUint32(0, sigBytes.length, false);
|
|
45
|
+
const out = new Uint8Array(clean.length + MAGIC.length + sigBytes.length + lenBytes.length);
|
|
46
|
+
out.set(clean, 0);
|
|
47
|
+
out.set(MAGIC, clean.length);
|
|
48
|
+
out.set(sigBytes, clean.length + MAGIC.length);
|
|
49
|
+
out.set(lenBytes, clean.length + MAGIC.length + sigBytes.length);
|
|
50
|
+
return out;
|
|
115
51
|
}
|
|
116
52
|
async strip(bytes) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
ignoreEncryption: true,
|
|
120
|
-
updateMetadata: false,
|
|
121
|
-
});
|
|
122
|
-
const infoDict = pdf.context.lookup(pdf.context.trailerInfo.Info);
|
|
123
|
-
if (infoDict) {
|
|
124
|
-
infoDict.delete(PDFName.of(SIGNATURE_KEY));
|
|
125
|
-
}
|
|
126
|
-
// Remove signature from keywords
|
|
127
|
-
try {
|
|
128
|
-
const keywords = pdf.getKeywords();
|
|
129
|
-
if (keywords) {
|
|
130
|
-
const prefix = `${SIGNATURE_KEY}:`;
|
|
131
|
-
if (keywords.startsWith(prefix)) {
|
|
132
|
-
pdf.setKeywords([]);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
/* ignore */
|
|
138
|
-
}
|
|
139
|
-
// Remove XMP metadata (regenerate clean)
|
|
140
|
-
try {
|
|
141
|
-
pdf.catalog.delete(PDFName.of("Metadata"));
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
/* ignore */
|
|
145
|
-
}
|
|
146
|
-
return pdf.save();
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
// If PDF is unreadable, return as-is
|
|
150
|
-
return bytes;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// ─── XMP Helpers ────────────────────────────────────────────────────────────
|
|
154
|
-
_buildXMP(signatureJson) {
|
|
155
|
-
// Escape XML entities in the JSON (rare but safe)
|
|
156
|
-
const escaped = signatureJson
|
|
157
|
-
.replace(/&/g, "&")
|
|
158
|
-
.replace(/</g, "<")
|
|
159
|
-
.replace(/>/g, ">");
|
|
160
|
-
return `<?xpacket begin="\uFEFF" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
|
161
|
-
<x:xmpmeta xmlns:x="adobe:ns:meta/">
|
|
162
|
-
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
163
|
-
<rdf:Description rdf:about=""
|
|
164
|
-
xmlns:majik="${MAJIK_NAMESPACE}">
|
|
165
|
-
<majik:signature>${escaped}</majik:signature>
|
|
166
|
-
</rdf:Description>
|
|
167
|
-
</rdf:RDF>
|
|
168
|
-
</x:xmpmeta>
|
|
169
|
-
<?xpacket end="w"?>`;
|
|
53
|
+
const i = this._findMagic(bytes);
|
|
54
|
+
return i === -1 ? bytes : bytes.slice(0, i);
|
|
170
55
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.replace(/>/g, ">")
|
|
179
|
-
.trim();
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
56
|
+
async extract(bytes) {
|
|
57
|
+
const i = this._findMagic(bytes);
|
|
58
|
+
if (i === -1)
|
|
59
|
+
return null;
|
|
60
|
+
const sigStart = i + MAGIC.length;
|
|
61
|
+
const sigEnd = bytes.length - 4;
|
|
62
|
+
return new TextDecoder().decode(bytes.slice(sigStart, sigEnd));
|
|
182
63
|
}
|
|
183
64
|
}
|
|
@@ -71,7 +71,7 @@ export declare class MajikSignatureEmbed {
|
|
|
71
71
|
*/
|
|
72
72
|
static verifyWithKey(file: Blob, key: MajikKey, MajikSig: MajikSignatureStaticAdapter, options?: ExtractOptions & {
|
|
73
73
|
expectedSignerId?: string;
|
|
74
|
-
}): Promise<EmbedVerifyResult>;
|
|
74
|
+
}, debug?: boolean): Promise<EmbedVerifyResult>;
|
|
75
75
|
/**
|
|
76
76
|
* Return a clean copy of the file with any embedded signature removed.
|
|
77
77
|
*/
|
|
@@ -155,9 +155,9 @@ export class MajikSignatureEmbed {
|
|
|
155
155
|
* Called from MajikSignature.verifyFile() — MajikSig passed to avoid
|
|
156
156
|
* circular import.
|
|
157
157
|
*/
|
|
158
|
-
static async verifyWithKey(file, key, MajikSig, options) {
|
|
158
|
+
static async verifyWithKey(file, key, MajikSig, options, debug = false) {
|
|
159
159
|
const publicKeys = MajikSig.publicKeysFromMajikKey(key);
|
|
160
|
-
return MajikSignatureEmbed.verify(file, publicKeys, MajikSig, options);
|
|
160
|
+
return MajikSignatureEmbed.verify(file, publicKeys, MajikSig, options, debug);
|
|
161
161
|
}
|
|
162
162
|
/**
|
|
163
163
|
* Return a clean copy of the file with any embedded signature removed.
|
|
@@ -142,7 +142,7 @@ export declare class MajikSignature {
|
|
|
142
142
|
static verifyFile(file: Blob, keyOrPublicKeys: MajikKey | MajikSignerPublicKeys, options?: {
|
|
143
143
|
expectedSignerId?: string;
|
|
144
144
|
mimeType?: string;
|
|
145
|
-
}): Promise<VerificationResult & {
|
|
145
|
+
}, debug?: boolean): Promise<VerificationResult & {
|
|
146
146
|
handler?: string;
|
|
147
147
|
}>;
|
|
148
148
|
/**
|
package/dist/majik-signature.js
CHANGED
|
@@ -410,13 +410,17 @@ export class MajikSignature {
|
|
|
410
410
|
* const result = await MajikSignature.verifyFile(signedBlob, key);
|
|
411
411
|
* if (result.valid) console.log("Signed by", result.signerId);
|
|
412
412
|
*/
|
|
413
|
-
static async verifyFile(file, keyOrPublicKeys, options) {
|
|
413
|
+
static async verifyFile(file, keyOrPublicKeys, options, debug = false) {
|
|
414
414
|
if (MajikSignature._isMajikKey(keyOrPublicKeys)) {
|
|
415
|
+
if (debug)
|
|
416
|
+
console.log("Verifying with MajikKey");
|
|
415
417
|
return MajikSignatureEmbed.verifyWithKey(file, keyOrPublicKeys, MajikSignature, // ← adapter
|
|
416
|
-
options);
|
|
418
|
+
options, debug);
|
|
417
419
|
}
|
|
420
|
+
if (debug)
|
|
421
|
+
console.log("Verifying with public keys");
|
|
418
422
|
return MajikSignatureEmbed.verify(file, keyOrPublicKeys, MajikSignature, // ← adapter
|
|
419
|
-
options);
|
|
423
|
+
options, debug);
|
|
420
424
|
}
|
|
421
425
|
/**
|
|
422
426
|
* Embed this MajikSignature instance into a file.
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@majikah/majik-signature",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "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 — using a dual-algorithm architecture that combines classical Ed25519 with post-quantum ML-DSA-87 (FIPS-204).",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.6",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|