@zumer/snapdom-plugins 3.0.0-beta.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +54 -20
- package/agent-map.d.ts +32 -0
- package/agent-map.js +5 -3
- package/ascii-export.d.ts +10 -0
- package/ascii-export.js +21 -6
- package/capture-frames.js +5 -5
- package/color-tint.js +4 -0
- package/context-export.d.ts +23 -0
- package/context-export.js +4 -1
- package/filter.js +1 -1
- package/gif-export.d.ts +6 -0
- package/gif-export.js +3 -3
- package/html-export.d.ts +8 -0
- package/html-export.js +6 -4
- package/index.d.ts +2 -1
- package/index.js +2 -1
- package/package.json +17 -6
- package/pdf/LICENSE +21 -0
- package/pdf/README.md +22 -0
- package/pdf/REFERENCE.md +520 -0
- package/pdf/src/frontmatter.js +200 -0
- package/pdf/src/furniture.js +398 -0
- package/pdf/src/index.js +2415 -0
- package/pdf/src/mega/README.md +47 -0
- package/pdf/src/mega/diag.js +148 -0
- package/pdf/src/mega/flow.js +56 -0
- package/pdf/src/mega/fonts.js +287 -0
- package/pdf/src/mega/forms.js +479 -0
- package/pdf/src/mega/labels.js +43 -0
- package/pdf/src/mega/meta.js +145 -0
- package/pdf/src/nav.js +357 -0
- package/pdf/src/paginate.js +583 -0
- package/pdf/src/tagged.js +355 -0
- package/pdf/src/text-layer.js +1771 -0
- package/pdf/src/writer/encrypt.js +218 -0
- package/pdf/src/writer/font.js +507 -0
- package/pdf/src/writer/index.js +1135 -0
- package/pdf/src/writer/subset.js +387 -0
- package/pdf.d.ts +294 -0
- package/pdf.js +1 -0
- package/redact-inputs.js +6 -6
- package/vector/CONTRACT.md +906 -0
- package/vector/LICENSE +21 -0
- package/vector/README.md +35 -0
- package/vector/src/adapters/snapdom.js +795 -0
- package/vector/src/collect/box.js +1676 -0
- package/vector/src/collect/font.js +487 -0
- package/vector/src/collect/image.js +1458 -0
- package/vector/src/collect/mathml.js +272 -0
- package/vector/src/collect/text.js +3027 -0
- package/vector/src/css/color.js +823 -0
- package/vector/src/css/gradient.js +1126 -0
- package/vector/src/css/matrix.js +398 -0
- package/vector/src/css/radius.js +124 -0
- package/vector/src/css/shadow.js +201 -0
- package/vector/src/emit/figma-json.js +1995 -0
- package/vector/src/emit/h2d-vector.js +215 -0
- package/vector/src/emit/h2d.js +1147 -0
- package/vector/src/emit/pdf.js +2571 -0
- package/vector/src/emit/svg-flat.js +4842 -0
- package/vector/src/exclusion.js +13 -0
- package/vector/src/index.js +2910 -0
- package/vector/src/paint.js +2908 -0
- package/vector/src/public.js +57 -0
- package/vector/src/writer/encrypt.js +227 -0
- package/vector/src/writer/font.js +507 -0
- package/vector/src/writer/index.js +1148 -0
- package/vector/src/writer/subset.js +387 -0
- package/vector/svd/index.js +72 -0
- package/vector/svd/schema.js +985 -0
- package/vector.d.ts +28 -0
- package/vector.js +1 -0
- package/video-export.d.ts +6 -0
- package/video-export.js +7 -7
- package/pdf-image.d.ts +0 -18
- package/pdf-image.js +0 -114
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard security handler, revision 6: AES-256, and nothing older.
|
|
3
|
+
*
|
|
4
|
+
* A PDF that leaves the browser as a download IS the delivery channel, so the
|
|
5
|
+
* only encryption worth writing here is the kind that protects the file at rest
|
|
6
|
+
* once it has been shared. That rules two things out, in opposite directions:
|
|
7
|
+
*
|
|
8
|
+
* - **RC4 and the /V 1–4 revisions are not offered.** A 40-bit key is brute
|
|
9
|
+
* forced in seconds and 128-bit RC4 is not far behind; both also need MD5,
|
|
10
|
+
* which the platform deliberately does not provide. Writing them by hand to
|
|
11
|
+
* ship a weaker product is not a trade this file makes.
|
|
12
|
+
* - **Permission flags are not protection and are not sold as such.** `/P` is
|
|
13
|
+
* a request a viewer may honour; any tool can ignore it. What actually keeps
|
|
14
|
+
* a document shut is the user password, because without it the bytes cannot
|
|
15
|
+
* be read at all. The flags are written because the format has them, and the
|
|
16
|
+
* caller is told what they are worth.
|
|
17
|
+
*
|
|
18
|
+
* Everything here comes from `crypto.subtle`: SHA-256/384/512 for Algorithm 2.B
|
|
19
|
+
* and AES-CBC for the rest. No dependency, no hand-rolled cipher, no MD5.
|
|
20
|
+
*
|
|
21
|
+
* **One thing WebCrypto will not do**, and the workaround, because it looks like
|
|
22
|
+
* a bug otherwise: 2.B and the key wrapping need AES-CBC with NO padding, and
|
|
23
|
+
* WebCrypto always appends a PKCS#7 block. The bytes it produces before that
|
|
24
|
+
* block ARE the unpadded CBC stream — CBC is sequential, so block n cannot
|
|
25
|
+
* depend on anything after it — so every no-padding call here encrypts and then
|
|
26
|
+
* truncates. `encryptcheck.mjs` asserts that prefix property rather than
|
|
27
|
+
* trusting it.
|
|
28
|
+
*
|
|
29
|
+
* **Every secret is random, so encryption is not deterministic.** The file key,
|
|
30
|
+
* the four salts, the /Perms filler and every IV come from
|
|
31
|
+
* `crypto.getRandomValues`, as ISO 32000-2 intends. The rest of this writer
|
|
32
|
+
* produces the same bytes for the same input; this file is the exception on
|
|
33
|
+
* purpose. An earlier version derived all of them from the passwords and a
|
|
34
|
+
* digest of the plaintext, so two encrypted exports of the same document under
|
|
35
|
+
* the same password were byte-identical, and anyone holding both files learned
|
|
36
|
+
* that their contents matched without knowing either password.
|
|
37
|
+
* `encryptcheck.mjs` asserts that two such exports differ and open to the same
|
|
38
|
+
* text.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const utf8 = (s) => new TextEncoder().encode(s)
|
|
42
|
+
|
|
43
|
+
/** Concatenate byte runs. */
|
|
44
|
+
function cat(...runs) {
|
|
45
|
+
let n = 0
|
|
46
|
+
for (const r of runs) n += r.length
|
|
47
|
+
const out = new Uint8Array(n)
|
|
48
|
+
let at = 0
|
|
49
|
+
for (const r of runs) { out.set(r, at); at += r.length }
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const hex = (b) => [...b].map(x => x.toString(16).padStart(2, '0')).join('').toUpperCase()
|
|
54
|
+
|
|
55
|
+
const random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes))
|
|
56
|
+
|
|
57
|
+
async function sha(bits, data) {
|
|
58
|
+
return new Uint8Array(await crypto.subtle.digest(`SHA-${bits}`, data))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* AES-CBC with no padding, by encrypting and dropping the block WebCrypto adds.
|
|
63
|
+
* `data.length` must be a multiple of 16, which every caller here guarantees.
|
|
64
|
+
*/
|
|
65
|
+
async function aesNoPad(key, iv, data) {
|
|
66
|
+
const k = await crypto.subtle.importKey('raw', key, 'AES-CBC', false, ['encrypt'])
|
|
67
|
+
const full = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, k, data))
|
|
68
|
+
return full.subarray(0, data.length)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** AES-CBC with the PKCS#7 padding the PDF stream/string format wants, IV first. */
|
|
72
|
+
async function aesCbc(key, iv, data) {
|
|
73
|
+
const k = await crypto.subtle.importKey('raw', key, 'AES-CBC', false, ['encrypt'])
|
|
74
|
+
return cat(iv, new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, k, data)))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* ISO 32000-2, Algorithm 2.B — the hardened password hash revision 6 uses.
|
|
79
|
+
*
|
|
80
|
+
* The loop is the point of it: at least 64 rounds, each one AES over 64 copies
|
|
81
|
+
* of (password ‖ K ‖ udata) and then one of three SHA-2 variants chosen by the
|
|
82
|
+
* data itself, and it only stops once the last byte of a round's output falls
|
|
83
|
+
* below round − 32. That makes a guess cost work no table can amortise.
|
|
84
|
+
*
|
|
85
|
+
* @param {Uint8Array} password UTF-8, already truncated to 127 bytes
|
|
86
|
+
* @param {Uint8Array} salt 8 bytes
|
|
87
|
+
* @param {Uint8Array} udata the 48-byte /U for an owner hash; empty for a user one
|
|
88
|
+
*/
|
|
89
|
+
async function hash2B(password, salt, udata) {
|
|
90
|
+
let k = await sha(256, cat(password, salt, udata))
|
|
91
|
+
for (let round = 0; ; round++) {
|
|
92
|
+
const block = cat(password, k, udata)
|
|
93
|
+
const k1 = new Uint8Array(block.length * 64)
|
|
94
|
+
for (let i = 0; i < 64; i++) k1.set(block, i * block.length)
|
|
95
|
+
const e = await aesNoPad(k.subarray(0, 16), k.subarray(16, 32), k1)
|
|
96
|
+
let mod = 0
|
|
97
|
+
for (let i = 0; i < 16; i++) mod += e[i]
|
|
98
|
+
k = await sha([256, 384, 512][mod % 3], e)
|
|
99
|
+
if (round >= 63 && e[e.length - 1] <= round - 31) break
|
|
100
|
+
}
|
|
101
|
+
return k.subarray(0, 32)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A PDF password: UTF-8, capped at 127 bytes, cut on a code point boundary. */
|
|
105
|
+
function passwordBytes(text) {
|
|
106
|
+
const raw = utf8(String(text ?? ''))
|
|
107
|
+
if (raw.length <= 127) return raw
|
|
108
|
+
let end = 127
|
|
109
|
+
while (end > 0 && (raw[end] & 0xc0) === 0x80) end--
|
|
110
|
+
return raw.subarray(0, end)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The permission bits of `/P`. Bits 1–2 and 7–8 are reserved and always 1; every
|
|
115
|
+
* bit this does not turn off stays on, so the default grants everything.
|
|
116
|
+
*
|
|
117
|
+
* Bit 10 — extraction for accessibility — is NOT exposed. It is deprecated in
|
|
118
|
+
* PDF 2.0 (a conforming reader must always allow it) and PDF/UA requires an
|
|
119
|
+
* encrypted file to keep permitting it, so an exporter whose entire selling
|
|
120
|
+
* point is an extractable text layer has no business offering to switch it off.
|
|
121
|
+
*/
|
|
122
|
+
const PERMISSION_BITS = {
|
|
123
|
+
print: 3, modify: 4, copy: 5, annotate: 6,
|
|
124
|
+
fillForms: 9, assemble: 11, printHighRes: 12,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function permissionValue(permissions, warn) {
|
|
128
|
+
let p = -1 // every bit set
|
|
129
|
+
// Bits 1, 2, 7, 8 are reserved and must be 1; -1 already has them.
|
|
130
|
+
for (const [name, value] of Object.entries(permissions || {})) {
|
|
131
|
+
const bit = PERMISSION_BITS[name]
|
|
132
|
+
if (bit === undefined) {
|
|
133
|
+
warn(`encrypt.permissions.${name} is not a permission — ignored. Known: ` +
|
|
134
|
+
`${Object.keys(PERMISSION_BITS).join(', ')}.`)
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
if (value === false) p &= ~(1 << (bit - 1))
|
|
138
|
+
}
|
|
139
|
+
return p | 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Everything a document needs to be encrypted: the file key, and the /Encrypt
|
|
144
|
+
* dictionary that lets a reader recover it from a password.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} args
|
|
147
|
+
* @param {string} args.userPassword opens the document; '' means "opens without one"
|
|
148
|
+
* @param {string} [args.ownerPassword] lifts the permission flags; defaults to the user one
|
|
149
|
+
* @param {object} [args.permissions] `{print:false, copy:false, …}`
|
|
150
|
+
* @param {(msg:string)=>void} args.warn
|
|
151
|
+
*/
|
|
152
|
+
export async function buildEncryption({ userPassword, ownerPassword, permissions, warn }) {
|
|
153
|
+
const user = passwordBytes(userPassword)
|
|
154
|
+
// An owner password equal to the user one is the common case and is fine; what
|
|
155
|
+
// is NOT fine is an owner password alone, because then anyone opens the file
|
|
156
|
+
// and only the advisory flags stand between them and everything.
|
|
157
|
+
const owner = passwordBytes(ownerPassword ?? userPassword)
|
|
158
|
+
const p = permissionValue(permissions, warn)
|
|
159
|
+
|
|
160
|
+
// Random, never derived: see the file comment.
|
|
161
|
+
const fileKey = random(32)
|
|
162
|
+
const uValidation = random(8)
|
|
163
|
+
const uKeySalt = random(8)
|
|
164
|
+
const oValidation = random(8)
|
|
165
|
+
const oKeySalt = random(8)
|
|
166
|
+
|
|
167
|
+
// Algorithm 8: /U proves a user password, /UE hands over the file key.
|
|
168
|
+
const uHash = await hash2B(user, uValidation, new Uint8Array(0))
|
|
169
|
+
const U = cat(uHash, uValidation, uKeySalt)
|
|
170
|
+
const UE = await aesNoPad(await hash2B(user, uKeySalt, new Uint8Array(0)), new Uint8Array(16), fileKey)
|
|
171
|
+
|
|
172
|
+
// Algorithm 9: the owner hashes are bound to /U, so /O cannot be lifted onto
|
|
173
|
+
// another document.
|
|
174
|
+
const oHash = await hash2B(owner, oValidation, U)
|
|
175
|
+
const O = cat(oHash, oValidation, oKeySalt)
|
|
176
|
+
const OE = await aesNoPad(await hash2B(owner, oKeySalt, U), new Uint8Array(16), fileKey)
|
|
177
|
+
|
|
178
|
+
// Algorithm 10: /Perms is the permission set encrypted under the file key, so
|
|
179
|
+
// a reader that HAS the key can tell whether the flags were tampered with.
|
|
180
|
+
// Bytes 12–15 are the random filler the algorithm asks for.
|
|
181
|
+
const perms = new Uint8Array(16)
|
|
182
|
+
new DataView(perms.buffer).setInt32(0, p, true)
|
|
183
|
+
perms.set([0xff, 0xff, 0xff, 0xff], 4)
|
|
184
|
+
perms[8] = 0x54 // 'T': the metadata stream is encrypted too
|
|
185
|
+
perms.set([0x61, 0x64, 0x62], 9) // 'adb'
|
|
186
|
+
perms.set(random(4), 12)
|
|
187
|
+
const Perms = await aesNoPad(fileKey, new Uint8Array(16), perms)
|
|
188
|
+
|
|
189
|
+
const dict =
|
|
190
|
+
'/Filter /Standard /V 5 /R 6 /Length 256 ' +
|
|
191
|
+
'/CF << /StdCF << /CFM /AESV3 /AuthEvent /DocOpen /Length 32 >> >> ' +
|
|
192
|
+
'/StmF /StdCF /StrF /StdCF ' +
|
|
193
|
+
`/O <${hex(O)}> /U <${hex(U)}> /OE <${hex(OE)}> /UE <${hex(UE)}> ` +
|
|
194
|
+
`/Perms <${hex(Perms)}> /P ${p} /EncryptMetadata true`
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
dict,
|
|
198
|
+
p,
|
|
199
|
+
/**
|
|
200
|
+
* Encrypt one stream payload or one string. Revision 6 uses the file key
|
|
201
|
+
* directly (the per-object key derivation of revisions 2–4 is gone), so an
|
|
202
|
+
* object number is not needed and cannot be got wrong. Every call draws its
|
|
203
|
+
* own random IV, which travels as the first 16 bytes.
|
|
204
|
+
*/
|
|
205
|
+
async encrypt(bytes) {
|
|
206
|
+
return aesCbc(fileKey, random(16), bytes)
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The catalog entry that tells an ISO 32000-1 reader this file uses a feature
|
|
213
|
+
* from the next edition. `%PDF-1.7` stays in the header — the file structure IS
|
|
214
|
+
* 1.7 — and AES-256 arrived as Adobe extension level 3 before ISO 32000-2
|
|
215
|
+
* standardised it, which is the shape every reader in the field learned.
|
|
216
|
+
*/
|
|
217
|
+
export const AES256_EXTENSION =
|
|
218
|
+
'/Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >>'
|