1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439 | //! Writes a document whose every string and every stream is ciphertext.
//!
//! The file is locked with AES under a two-hundred-and-fifty-six-bit key. Its
//! user password is empty, so any reader opens it without asking for one, which
//! is what most protected files in the world do; the author's password is set,
//! and the page states it so that the file can be opened both ways.
//!
//! What the file grants is printing and reading aloud. Copying, changing and
//! taking pages out are withheld — as requests a reader honours, not as locks:
//! a reader that ignores them opens the document all the same, and the page
//! says so rather than letting the file be taken for a safe.
//!
//! The page is written in the language `HQF_PDF_LANG` names. The password is
//! not: it is a string typed into a reader, and a translated password opens
//! nothing.
//!
//! Usage: `cargo run --example write_protected -- tmp/protected.pdf [font.ttf]`
//! `HQF_PDF_LANG=fr cargo run --example write_protected`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::cos::Name;
use hqf_pdf::{Document, Encryption, Font, FontHandle, Page, Permissions};
#[path = "shared/out.rs"]
mod out;
#[path = "shared/licence.rs"]
mod licence;
#[path = "shared/language.rs"]
mod language;
use language::Language;
/// The font the example draws with when none is given on the command line: the
/// one committed for the tests, so that the example runs on any machine.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// How far in from the left edge of the sheet every line is set, in points.
const LEFT: f64 = 72.0;
/// The size the heading is set at, in points.
const HEADING_SIZE: f64 = 18.0;
/// Where the baseline of the heading sits, in points up from the foot of the
/// sheet.
const HEADING_BASELINE: f64 = 760.0;
/// The size the body is set at, in points.
const BODY_SIZE: f64 = 11.0;
/// Where the baseline of the first line of the body sits, in points up from the
/// foot of the sheet.
const FIRST_BASELINE: f64 = 716.0;
/// How far one line of the body sits below the one before it, in points.
const LEADING: f64 = 18.0;
/// The size the heading over the closing note is set at, in points.
const NOTE_HEADING_SIZE: f64 = 13.0;
/// Where the baseline of that heading sits, in points up from the foot of the
/// sheet.
const NOTE_HEADING_BASELINE: f64 = 536.0;
/// Where the baseline of the first line of the note sits, in points up from the
/// foot of the sheet.
const NOTE_BASELINE: f64 = 508.0;
/// The author's password, which the page states so that the file can be opened
/// both ways. It stands outside the words: a password is typed into a reader,
/// and a translated one opens nothing.
const OWNER_PASSWORD: &str = "the owner";
/// The thirty-two bytes the file key is built from.
///
/// They are fixed here, so that the example writes the same file on every run
/// and one build can be compared with the last. **A program takes them from its
/// operating system** — `getrandom` in Rust, `os.urandom(32)` in Python. A file
/// locked under a seed anybody can read is a file anybody opens.
const SEED: [u8; 32] = [
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
0x0F, 0x1E, 0x2D, 0x3C, 0x4B, 0x5A, 0x69, 0x78, 0x87, 0x96, 0xA5, 0xB4, 0xC3, 0xD2, 0xE1, 0xF0,
];
/// The words the page is written in, one set per language.
///
/// The password is not among them: it is typed into a reader, not translated.
#[derive(Debug)]
struct Words {
/// What the document is called, both at the head of the page and in what
/// the file says of itself.
title: &'static str,
/// The body of the page, one line to a line.
body: [&'static str; 8],
/// What stands before the author's password.
password_label: &'static str,
/// What stands over the closing note.
note_heading: &'static str,
/// The closing note, which says where the key comes from.
note: [&'static str; 4],
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
}
/// The page in English.
const ENGLISH: Words = Words {
title: "A document written protected",
body: [
"Every string and every stream in this file is ciphertext, locked",
"with AES under a two-hundred-and-fifty-six-bit key.",
"This copy opens without a password: its user password is empty,",
"which is what most protected files in the world do.",
"The reader is asked to allow printing and reading aloud, and to",
"withhold copying, changing and taking pages out. Those are",
"requests a reader honours, not locks: one that ignores them opens",
"the document all the same.",
],
password_label: "The author's password lifts them:",
note_heading: "Where the key comes from",
note: [
"The thirty-two bytes the key is built from are fixed in this",
"example, so that it writes the same file on every run and one",
"build can be compared with the last. A program takes them from",
"its operating system: a seed anybody can read locks nothing.",
],
};
/// The page in French.
const FRENCH: Words = Words {
title: "Un document écrit protégé",
body: [
"Chaque chaîne et chaque flux de ce fichier est chiffré, sous une",
"clé AES de deux cent cinquante-six bits.",
"Cette copie s'ouvre sans mot de passe : son mot de passe",
"utilisateur est vide, comme la plupart des fichiers protégés.",
"Le lecteur est prié d'autoriser l'impression et la lecture à voix",
"haute, et de refuser la copie, la modification et le retrait de",
"pages. Ce sont des demandes qu'un lecteur honore, pas des",
"verrous : celui qui les ignore ouvre le document quand même.",
],
password_label: "Le mot de passe de l'auteur les lève :",
note_heading: "D'où vient la clé",
note: [
"Les trente-deux octets dont la clé est tirée sont figés dans cet",
"exemple, pour qu'il écrive le même fichier à chaque fois et qu'une",
"version se compare à la précédente. Un programme les prend à son",
"système : une graine que tout le monde peut lire ne ferme rien.",
],
};
/// Every language the example is written in. A language is added by writing its
/// own set of words and naming it here.
static WORDS: [(Language, &Words); 2] =
[(Language::English, &ENGLISH), (Language::French, &FRENCH)];
/// Every line of the body, the last of which states the author's password.
fn lines(words: &Words) -> Vec<String> {
let mut all: Vec<String> = words.body.iter().map(|line| (*line).to_owned()).collect();
all.push(format!("{} {OWNER_PASSWORD}", words.password_label));
all
}
/// What the document is locked with: the author's password, and the two things
/// a reader is asked to allow.
fn encryption() -> Encryption {
Encryption::new(SEED)
.owner_password(OWNER_PASSWORD)
.permissions(Permissions::new().printing().extracting_for_accessibility())
}
/// Every line the page draws, in the order they are drawn: what it says, the
/// size it is set at, and where its baseline sits in points up from the foot of
/// the sheet.
fn drawing(words: &Words) -> Vec<(String, f64, f64)> {
let mut drawn = vec![(words.title.to_owned(), HEADING_SIZE, HEADING_BASELINE)];
let mut baseline = FIRST_BASELINE;
for line in lines(words) {
drawn.push((line, BODY_SIZE, baseline));
baseline -= LEADING;
}
drawn.push((
words.note_heading.to_owned(),
NOTE_HEADING_SIZE,
NOTE_HEADING_BASELINE,
));
baseline = NOTE_BASELINE;
for line in words.note {
drawn.push((line.to_owned(), BODY_SIZE, baseline));
baseline -= LEADING;
}
drawn
}
/// The page: every line set by its own origin, each in an object of its own.
fn drawn(font: &FontHandle, words: &Words) -> Result<Content, hqf_pdf::Error> {
let mut content = Content::new();
for (line, size, baseline) in drawing(words) {
content.begin_text();
content.set_font(font.name(), size)?;
content.text_origin(LEFT, baseline)?;
content.show_glyphs(&font.glyphs(&line));
content.end_text();
}
Ok(content)
}
/// The document and its one page, before it is locked.
fn document(words: &Words, font: Font) -> Result<Document, hqf_pdf::Error> {
let mut doc = Document::new();
doc.set_license(licence::licensed());
doc.set_info(Name::new("Title"), words.title);
let handle = doc.add_font(font);
let mut page = Page::a4();
page.content = drawn(&handle, words)?.into_bytes();
doc.add_page(page)?;
Ok(doc)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let language = Language::from_environment()?;
let words = Words::of(language);
let mut args = env::args().skip(1);
// A named file is written as named; the default one carries the language,
// so the two languages do not overwrite each other in `tmp/`.
let out = args
.next()
.unwrap_or_else(|| language.file_name(&out::default_path("protected")));
let font_path = args.next().map_or_else(default_font, PathBuf::from);
let mut doc = document(words, Font::parse(fs::read(&font_path)?)?)?;
doc.protect(encryption());
let bytes = doc.to_bytes()?;
if let Some(parent) = Path::new(&out).parent() {
fs::create_dir_all(parent)?;
}
fs::write(&out, &bytes)?;
println!("wrote {out}: {} bytes", bytes.len());
Ok(())
}
#[cfg(test)]
mod tests {
use hqf_pdf::read::Reader;
use hqf_pdf::{Document, Encryption, Font};
use super::{
LEFT, OWNER_PASSWORD, WORDS, Words, default_font, document, drawing, encryption, language,
};
/// The width of the sheet, in points: A4, which the page is.
const SHEET_WIDTH: f64 = 595.276;
/// The room a line set from the left margin has before it reaches the
/// margin on the other side of the sheet.
const PAPER: f64 = SHEET_WIDTH - 2.0 * LEFT;
/// The lines two languages are allowed to write the same way. There are
/// none: the password is held outside the words.
const SPARED: [&str; 0] = [];
/// The committed font, parsed.
fn a_font() -> Font {
Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
.expect("the committed font parses")
}
/// The bytes the example writes for `words`, locked or in the clear.
fn written(words: &Words, locked: bool) -> Vec<u8> {
let mut doc = document(words, a_font()).expect("the document is built");
if locked {
doc.protect(encryption());
}
doc.to_bytes().expect("the document writes")
}
#[test]
fn every_language_writes_the_page_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the page says these in more than one language: {untranslated:?}"
);
}
/// Nothing on the page is broken to a width: a line longer than the paper
/// runs off the edge of it.
#[test]
fn every_language_writes_lines_that_fit_the_room_they_have() {
let mut doc = Document::new();
let font = doc.add_font(a_font());
for (named, words) in WORDS {
for (line, size, _) in drawing(words) {
let measured = font.measure(&line, size);
assert!(
measured <= PAPER,
"the {} page draws {line:?} over {measured:.1} points, and \
it has {PAPER:.1}",
named.code()
);
}
}
}
/// The bytes a file states its title as, however it spells them: plain
/// where the words are, sixteen bits to a letter where they are not.
fn stated_title(bytes: &[u8]) -> Vec<u8> {
let key = b"/Title (";
let at = bytes
.windows(key.len())
.position(|window| window == key)
.expect("the file states a title")
+ key.len();
let mut stated = Vec::new();
let mut escaped = false;
for byte in &bytes[at..] {
if escaped {
escaped = false;
} else if *byte == b'\\' {
escaped = true;
} else if *byte == b')' {
break;
}
stated.push(*byte);
}
stated
}
/// The file names the handler it was locked with and carries nothing of
/// what it states its title to be. The same document written in the clear
/// carries it plainly, which is what says the reading looks in the right
/// place.
#[test]
fn the_file_carries_nothing_it_draws_in_the_clear() {
for (named, words) in WORDS {
let code = named.code();
let plain = written(words, false);
let stated = stated_title(&plain);
assert!(
stated.len() > 8,
"the {code} document states its title where a file states one"
);
assert!(
!String::from_utf8_lossy(&plain).contains("/Encrypt"),
"and names no handler"
);
let locked = written(words, true);
let text = String::from_utf8_lossy(&locked);
assert!(text.contains("/Encrypt"), "the {code} file is protected");
assert!(text.contains("/AESV3"), "and names the cipher it used");
assert!(
!locked
.windows(stated.len())
.any(|window| window == stated.as_slice()),
"the {code} file carries its title in the clear"
);
}
}
/// What the file says of itself comes back out of it, which noise would not
/// spell. The user password is empty, so an empty one opens it; the
/// author's opens it too.
#[test]
fn what_the_document_says_of_itself_comes_back_out_of_the_file() {
for (named, words) in WORDS {
let bytes = written(words, true);
for password in ["", OWNER_PASSWORD] {
let reader = Reader::with_password(bytes.clone(), password)
.unwrap_or_else(|error| panic!("{password:?} opens it: {error}"));
assert_eq!(
reader.information().title.unwrap_or_default(),
words.title,
"the {} file states its title",
named.code()
);
}
}
}
/// The page says printing and reading aloud are allowed and the rest is
/// withheld; the file states the same thing, as the flags of table 22 with
/// every reserved bit set — 0xFFFFF0C0, which is -3904 — plus bit 3 for
/// printing, which is 4, and bit 10 for reading aloud, which is 512.
#[test]
fn what_the_page_says_is_allowed_is_what_the_file_states() {
let bytes = written(Words::of(language::Language::English), true);
assert_eq!(-3904 + 4 + 512, -3388, "the flags add up");
assert!(
String::from_utf8_lossy(&bytes).contains("/P -3388"),
"the file states what it grants"
);
}
/// The same document written twice is the same file, which is what lets one
/// build be compared with the last; another key writes another file.
#[test]
fn the_same_seed_writes_the_same_file_and_another_writes_another() {
let words = Words::of(language::Language::English);
assert_eq!(
written(words, true),
written(words, true),
"a protected document is still the same file"
);
let mut other = document(words, a_font()).expect("the document is built");
other.protect(Encryption::new([1u8; 32]).owner_password(OWNER_PASSWORD));
assert_ne!(
written(words, true),
other.to_bytes().expect("the document writes"),
"another key writes another file"
);
}
}
|