1use onestore::{
2 Arena, ExGuid, RevisionIndex, Section, Store,
3 document::{Document, Kind},
4 op::{Edit, Op},
5 page::Page,
6 protected::{Error, Key, Limits, UnlockedSection, rekey},
7};
8use std::{fs, path::Path};
9
10/// The native fixtures with their passwords.
11fn fixtures() -> Vec<(Vec<u8>, String)> {
12 [
13 ("native-encrypted", "encrypted-01/notebook/synthetic.one"),
14 ("native-protected-boundaries", "notebook/synthetic.one"),
15 ]
16 .into_iter()
17 .map(|(root, notebook)| {
18 let root = Path::new("../../corpus").join(root);
19 let manifest: serde_json::Value =
20 serde_json::from_slice(&fs::read(root.join("manifest.json")).unwrap()).unwrap();
21 (
22 fs::read(root.join(notebook)).unwrap(),
23 manifest["password"].as_str().unwrap().to_owned(),
24 )
25 })
26 .collect()
27}
28
29/// Every page of a section `Section::open` or `Section::unlock` reads, in order.
30fn pages(section: &mut Section<'_>) -> Vec<(ExGuid, Page)> {
31 section
32 .pages()
33 .unwrap()
34 .into_iter()
35 .map(|(space, ..)| (space, section.page(space).unwrap()))
36 .collect()
37}
38
39fn objects(pages: &[(ExGuid, Page)]) -> Vec<&Vec<onestore::page::PageObject>> {
40 pages.iter().map(|(_, page)| &page.objects).collect()
41}
42
43#[test]
44fn known_passwords_open_independent_native_fixtures() {
45 for ((bytes, password), count) in fixtures().into_iter().zip([1, 11]) {
46 let store = Store::parse(&bytes).unwrap();
47 let index = RevisionIndex::parse(&store).unwrap();
48 assert!(Document::parse(&index).unwrap().pages().unwrap().is_empty());
49 let unlocked = UnlockedSection::open(&index, &password, Limits::default()).unwrap();
50 let document = unlocked.document().unwrap();
51 assert_eq!(document.pages().unwrap().len(), count);
52 assert!(
53 document
54 .spaces
55 .values()
56 .flat_map(|s| s.revisions.values())
57 .flat_map(|r| r.nodes.values())
58 .all(|n| !matches!(n.kind, Kind::Encrypted { .. }))
59 );
60 // A section opened under its key reads as the document does.
61 let arena = Arena::default();
62 assert!(Section::open(&arena, bytes.clone()).is_err());
63 let key = Key::open(&bytes, &password).unwrap();
64 let mut section = Section::unlock(&arena, bytes.clone(), &key).unwrap();
65 let read = pages(&mut section);
66 assert_eq!(read.len(), count);
67 for (space, page) in &read {
68 assert_eq!(*page, Page::from_space(&document, *space).unwrap());
69 }
70 assert!(matches!(
71 Key::open(&bytes, "deliberately incorrect fixture password"),
72 Err(Error::PasswordMismatch)
73 ));
74 }
75}
76
77#[test]
78fn limits_and_password_bytes_are_explicit() {
79 let (bytes, password) = fixtures().remove(1);
80 let store = Store::parse(&bytes).unwrap();
81 let index = RevisionIndex::parse(&store).unwrap();
82 for limits in [
83 Limits {
84 kdf_rounds: 0,
85 ..Limits::default()
86 },
87 Limits {
88 decoded_bytes: 0,
89 ..Limits::default()
90 },
91 Limits {
92 object_visits: 0,
93 ..Limits::default()
94 },
95 ] {
96 assert!(matches!(
97 UnlockedSection::open(&index, &password, limits),
98 Err(Error::Limit)
99 ));
100 }
101 for changed in [
102 password.replace("e\u{301}", "é"),
103 format!("{password}\n"),
104 password.to_uppercase(),
105 ] {
106 assert!(matches!(
107 Key::open(&bytes, &changed),
108 Err(Error::PasswordMismatch)
109 ));
110 }
111 assert!(matches!(
112 Key::open(&bytes, &"x".repeat(65537)),
113 Err(Error::Limit)
114 ));
115 let ordinary = onestore::create_section("ordinary.one", "Fictitious", "Author").unwrap();
116 assert!(matches!(
117 Key::open(&ordinary, &password),
118 Err(Error::Unsupported)
119 ));
120 // A key opens only the section whose encryption data it came from.
121 let other = Key::new(&password).unwrap();
122 assert!(matches!(
123 Section::unlock(&Arena::default(), bytes, &other),
124 Err(Error::PasswordMismatch)
125 ));
126}
127
128/// Appends an attachment and text to the first text paragraph of `page`; none without one.
129fn edit(space: ExGuid, page: &Page) -> Option<(Edit, Page)> {
130 use onestore::page::{Attachment, PageObject, Paragraph, ParagraphContent};
131 let mut edited = page.clone();
132 let paragraphs = edited.objects.iter_mut().find_map(|object| match object {
133 PageObject::Outline(outline) if outline.paragraphs.iter().any(|p| p.text().is_some()) => {
134 Some(&mut outline.paragraphs)
135 }
136 _ => None,
137 })?;
138 let at = paragraphs.iter().position(|p| p.text().is_some()).unwrap();
139 let mut file = paragraphs[at].clone();
140 file.id = onestore::page::text::new_id().unwrap();
141 file.lists.clear();
142 file.tags.clear();
143 file.style = None;
144 file.format = Default::default();
145 file.content = ParagraphContent::Attachment(Attachment {
146 id: onestore::page::text::new_id().unwrap(),
147 filename: "sealed.txt".into(),
148 source_path: None,
149 size: Some([24.0, 24.0]),
150 layout: Default::default(),
151 bytes: Some(std::sync::Arc::from(&b"A payload that stays sealed"[..])),
152 preview: None,
153 recording: None,
154 tags: Vec::new(),
155 });
156 paragraphs.insert(at + 1, file);
157 let text = paragraphs[at].text_mut().unwrap();
158 let format = text.text.format_at(0).unwrap().clone();
159 text.text
160 .append(Paragraph::new(" still protected".to_owned(), format))
161 .unwrap();
162 let ops = onestore::op::lower_page(page, &edited).unwrap();
163 let edit = Edit {
164 at: 134_000_000_000_000_000,
165 ops: ops.into_iter().map(|op| Op::Page { space, op }).collect(),
166 };
167 Some((edit, edited))
168}
169
170/// Every revision of every space of `bytes` names its key, as MS-ONESTORE 2.5.19 asks.
171fn keyed(bytes: &[u8]) -> bool {
172 let store = Store::parse(bytes).unwrap();
173 let index = RevisionIndex::parse(&store).unwrap();
174 index
175 .spaces
176 .values()
177 .flat_map(|space| space.revisions.values())
178 .all(|revision| revision.encrypted && revision.nodes[0].id == 0x7c)
179}
180
181/// The first text paragraph of every page gains text and an attachment; each seal appends
182/// one revision to that page alone, stores none of it in the clear, and reads back as the
183/// edited model under the same password.
184#[test]
185fn a_protected_page_edit_is_stored_under_the_section_key() {
186 for (mut bytes, password) in fixtures() {
187 let key = Key::open(&bytes, &password).unwrap();
188 let mut expected =
189 pages(&mut Section::unlock(&Arena::default(), bytes.clone(), &key).unwrap());
190 let mut edited = 0;
191 for (space, page) in &mut expected {
192 let Some((edit, after)) = edit(*space, page) else {
193 continue;
194 };
195 *page = after;
196 edited += 1;
197 let arena = Arena::default();
198 let mut section = Section::unlock(&arena, bytes.clone(), &key).unwrap();
199 section.apply("Rust", &edit).unwrap();
200 let transaction = section.seal().unwrap().unwrap();
201 let mut written = bytes.clone();
202 transaction.apply(&mut written).unwrap();
203 assert_eq!(written, section.image());
204 for clear in [&b" still protected"[..], b"stays sealed"] {
205 let utf16: Vec<u8> = String::from_utf8_lossy(clear)
206 .encode_utf16()
207 .flat_map(u16::to_le_bytes)
208 .collect();
209 for clear in [clear, &utf16[..]] {
210 assert!(!written.windows(clear.len()).any(|w| w == clear));
211 }
212 }
213 let revisions = |bytes: &[u8]| {
214 let store = Store::parse(bytes).unwrap();
215 let index = RevisionIndex::parse(&store).unwrap();
216 index
217 .spaces
218 .iter()
219 .map(|(id, space)| (*id, space.revisions.len()))
220 .collect::<Vec<_>>()
221 };
222 let grown: Vec<_> = revisions(&bytes)
223 .into_iter()
224 .zip(revisions(&written))
225 .filter(|(before, after)| before != after)
226 .map(|(before, _)| before.0)
227 .collect();
228 assert_eq!(grown, [*space]);
229 // The section kept open reads its own seal back.
230 assert_eq!(section.page(*space).unwrap().objects, page.objects);
231 bytes = written;
232 }
233 assert!(edited > 0);
234 let stored = pages(&mut Section::unlock(&Arena::default(), bytes.clone(), &key).unwrap());
235 assert_eq!(objects(&stored), objects(&expected));
236 // The document reader agrees, under the password alone.
237 let store = Store::parse(&bytes).unwrap();
238 let index = RevisionIndex::parse(&store).unwrap();
239 let unlocked = UnlockedSection::open(&index, &password, Limits::default()).unwrap();
240 let document = unlocked.document().unwrap();
241 for (space, page) in &expected {
242 assert_eq!(
243 Page::from_space(&document, *space).unwrap().objects,
244 page.objects
245 );
246 }
247 }
248}
249
250/// OneNote's revisions that depend on an earlier one carry no key node of their own.
251#[test]
252fn a_native_revision_inherits_the_key_of_its_dependency() {
253 let bytes = fs::read("../../corpus/protected-edit/native-after/synthetic.one").unwrap();
254 let (_, password) = fixtures().remove(0);
255 let store = Store::parse(&bytes).unwrap();
256 let index = RevisionIndex::parse(&store).unwrap();
257 assert!(index.spaces.values().any(|space| {
258 space.revisions.values().any(|revision| {
259 revision.encrypted && revision.nodes.first().is_none_or(|node| node.id != 0x7c)
260 })
261 }));
262 let key = Key::open(&bytes, &password).unwrap();
263 let read = pages(&mut Section::unlock(&Arena::default(), bytes, &key).unwrap());
264 assert!(format!("{:?}", read[0].1.objects).contains("Native edit after Rust."));
265}
266
267/// Setting, changing and removing a password rewrites the section under new identities,
268/// keeping every page, version and payload; each password opens only its own image.
269#[test]
270fn a_password_is_set_changed_and_removed() {
271 let plain = fs::read("../../corpus/page-versions/native/step-09/notebook/History.one").unwrap();
272 let arena = Arena::default();
273 let mut section = Section::open(&arena, plain.clone()).unwrap();
274 let before = pages(&mut section);
275 let versions = section.versions().unwrap();
276 assert!(!versions.is_empty());
277 let identity = |bytes: &[u8]| {
278 let store = Store::parse(bytes).unwrap();
279 (
280 store.header.file_id,
281 RevisionIndex::parse(&store).unwrap().root,
282 )
283 };
284
285 let first = Key::new("first password").unwrap();
286 let protected = rekey(&plain, None, Some(&first)).unwrap();
287 assert!(keyed(&protected));
288 assert_eq!(protected[128..148], plain[128..148]);
289 assert_ne!(identity(&protected).0, identity(&plain).0);
290 assert_ne!(identity(&protected).1, identity(&plain).1);
291 assert!(Section::open(&Arena::default(), protected.clone()).is_err());
292 // The encryption data OneNote writes, which the password alone opens again.
293 let reopened = Key::open(&protected, "first password").unwrap();
294 assert_eq!(reopened.secret(), first.secret());
295 assert!(matches!(
296 Key::open(&protected, "First password"),
297 Err(Error::PasswordMismatch)
298 ));
299 let arena = Arena::default();
300 let mut section = Section::unlock(&arena, protected.clone(), &reopened).unwrap();
301 assert_eq!(objects(&pages(&mut section)), objects(&before));
302 assert_eq!(
303 section
304 .versions()
305 .unwrap()
306 .iter()
307 .map(|(_, versions)| versions.len())
308 .collect::<Vec<_>>(),
309 versions
310 .iter()
311 .map(|(_, versions)| versions.len())
312 .collect::<Vec<_>>()
313 );
314 let store = Store::parse(&protected).unwrap();
315 let index = RevisionIndex::parse(&store).unwrap();
316 let unlocked = UnlockedSection::open(&index, "first password", Limits::default()).unwrap();
317 assert_eq!(
318 unlocked.document().unwrap().pages().unwrap().len(),
319 before.len()
320 );
321
322 let second = Key::new("second password").unwrap();
323 assert!(matches!(
324 rekey(&protected, None, Some(&second)),
325 Err(Error::PasswordMismatch)
326 ));
327 let changed = rekey(&protected, Some(&first), Some(&second)).unwrap();
328 assert_ne!(first.secret(), second.secret());
329 assert!(matches!(
330 Key::open(&changed, "first password"),
331 Err(Error::PasswordMismatch)
332 ));
333 assert!(matches!(
334 Section::unlock(&Arena::default(), changed.clone(), &first),
335 Err(Error::PasswordMismatch)
336 ));
337 let arena = Arena::default();
338 let mut section = Section::unlock(&arena, changed.clone(), &second).unwrap();
339 assert_eq!(objects(&pages(&mut section)), objects(&before));
340
341 let removed = rekey(&changed, Some(&second), None).unwrap();
342 assert!(
343 RevisionIndex::parse(&Store::parse(&removed).unwrap())
344 .unwrap()
345 .spaces
346 .values()
347 .flat_map(|space| space.revisions.values())
348 .all(|revision| !revision.encrypted)
349 );
350 let arena = Arena::default();
351 let mut section = Section::open(&arena, removed).unwrap();
352 assert_eq!(objects(&pages(&mut section)), objects(&before));
353 assert_eq!(section.versions().unwrap().len(), versions.len());
354}
355
356/// Payloads survive a password of their own: every native attachment boundary length.
357#[test]
358fn payloads_survive_a_new_password() {
359 let (bytes, password) = fixtures().remove(1);
360 let key = Key::open(&bytes, &password).unwrap();
361 let before = pages(&mut Section::unlock(&Arena::default(), bytes.clone(), &key).unwrap());
362 let new = Key::new("another").unwrap();
363 let changed = rekey(&bytes, Some(&key), Some(&new)).unwrap();
364 let after = pages(&mut Section::unlock(&Arena::default(), changed, &new).unwrap());
365 assert_eq!(objects(&after), objects(&before));
366}
367
368/// What OneNote 2010 wrote setting, changing and removing a password in the lab, and its edit
369/// of a section Snowbound protected, edited and gave another password, each read here.
370#[test]
371fn onenote_protected_changed_and_removed_sections_read_back() {
372 let root = Path::new("../../corpus/protected-sections");
373 let manifest: serde_json::Value =
374 serde_json::from_slice(&fs::read(root.join("manifest.json")).unwrap()).unwrap();
375 let plain = fs::read(root.join("source/synthetic.one")).unwrap();
376 let titles = |section: &mut Section<'_>| -> Vec<String> {
377 section
378 .pages()
379 .unwrap()
380 .into_iter()
381 .map(|(_, title, _)| title)
382 .collect()
383 };
384 let expected = titles(&mut Section::open(&Arena::default(), plain).unwrap());
385 assert_eq!(expected.len(), 2);
386 for (file, password) in manifest["native"].as_object().unwrap() {
387 let bytes = fs::read(root.join("native").join(file)).unwrap();
388 let arena = Arena::default();
389 let mut section = match password.as_str() {
390 Some(password) => {
391 let key = Key::open(&bytes, password).unwrap();
392 assert!(Section::open(&Arena::default(), bytes.clone()).is_err());
393 Section::unlock(&arena, bytes, &key).unwrap()
394 }
395 None => Section::open(&arena, bytes).unwrap(),
396 };
397 let read = titles(&mut section);
398 assert_eq!(read.len(), 2, "{file}");
399 if file.starts_with("snowbound") {
400 let (space, ..) = section.pages().unwrap()[0];
401 let page = format!("{:?}", section.page(space).unwrap().objects);
402 assert!(page.contains("typed by OneNote"), "{file}");
403 } else {
404 assert_eq!(read, expected, "{file}");
405 }
406 }
407}