1//! OneNote 2010's password-protected sections: an Office Agile password wrapper
2//! (MS-OFFCRYPTO; SHA-1, 100,000 rounds) around an AES-128 key that encrypts every
3//! property object (CBC, an IV of its own) and payload (CBC, one IV from the key data's
4//! salt). Unknown wrappers stay opaque. CBC authenticates nothing; read-only hashes and
5//! model checks catch structural damage, not every change to content.
6
7mod crypto;
8
9use crate::{
10 Chunk, ExGuid, FileDataReference, ObjectData, Reference, RevisionIndex, Store,
11 document::Document,
12};
13use bumpalo::Bump;
14use std::{
15 cell::RefCell,
16 collections::{BTreeMap, BTreeSet},
17 fmt,
18 sync::Arc,
19};
20use zeroize::Zeroizing;
21
22type Result<T> = std::result::Result<T, Error>;
23type Format<T> = std::result::Result<T, crate::Error>;
24
25#[derive(Debug)]
26pub enum Error {
27 PasswordMismatch,
28 Unsupported,
29 Limit,
30 Invalid(crate::Error),
31}
32
33impl From<crate::Error> for Error {
34 fn from(value: crate::Error) -> Self {
35 Self::Invalid(value)
36 }
37}
38
39impl Error {
40 fn message(&self) -> &'static str {
41 match self {
42 Self::PasswordMismatch => "The password did not match the section verifier",
43 Self::Unsupported => "This protection format is not supported",
44 Self::Limit => "Opening the protected section exceeded its work limit",
45 Self::Invalid(error) => error.message,
46 }
47 }
48}
49
50impl From<Error> for crate::Error {
51 fn from(error: Error) -> Self {
52 match error {
53 Error::Invalid(error) => error,
54 other => crate::Error {
55 offset: 0,
56 message: other.message(),
57 },
58 }
59 }
60}
61
62impl fmt::Display for Error {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::Invalid(error) => error.fmt(f),
66 other => f.write_str(other.message()),
67 }
68 }
69}
70impl std::error::Error for Error {
71 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
72 match self {
73 Self::Invalid(error) => Some(error),
74 _ => None,
75 }
76 }
77}
78
79fn invalid(message: &'static str) -> Error {
80 Error::Invalid(crate::Error { offset: 0, message })
81}
82
83#[derive(Debug, Clone, Copy)]
84pub struct Limits {
85 /// Total password iterations across distinct encryption metadata containers.
86 pub kdf_rounds: u64,
87 /// Total ciphertext/reference bytes materialized; temporary copies can double this.
88 pub decoded_bytes: usize,
89 /// Sum of object counts in distinct labeled revisions.
90 pub object_visits: usize,
91}
92
93impl Default for Limits {
94 fn default() -> Self {
95 Self {
96 kdf_rounds: 1_000_000,
97 decoded_bytes: 256 * 1024 * 1024,
98 object_visits: 1_000_000,
99 }
100 }
101}
102
103/// A protected section's labelled revisions decoded for reading as a `Document`, which
104/// cannot outlive it. Holds no password; dropping it clears its keys and decoded buffers,
105/// while strings and exports made from the `Document` are the caller's to dispose of.
106/// Edits go through `Section::unlock`.
107///
108/// ```compile_fail
109/// # use onestore::{RevisionIndex, protected::{Limits, UnlockedSection}};
110/// fn outlive<'a>(index: &'a RevisionIndex<'a>, password: &str) {
111/// let unlocked = UnlockedSection::open(index, password, Limits::default()).unwrap();
112/// let document = unlocked.document().unwrap();
113/// drop(unlocked);
114/// document.pages().unwrap();
115/// }
116/// ```
117pub struct UnlockedSection<'a> {
118 index: &'a RevisionIndex<'a>,
119 /// By object space and stored address.
120 objects: BTreeMap<(ExGuid, usize), Zeroizing<Vec<u8>>>,
121 files: BTreeMap<[u8; 16], Zeroizing<Vec<u8>>>,
122}
123
124impl<'a> UnlockedSection<'a> {
125 /// Verifies the password and every labeled revision before exposing a view.
126 /// Metadata and password input are each bounded to 64 KiB.
127 pub fn open(index: &'a RevisionIndex<'a>, password: &str, limits: Limits) -> Result<Self> {
128 Self::open_with(index, limits, |metadata, rounds| {
129 crypto::Key::open(metadata, password, rounds)
130 })
131 }
132
133 /// `open` with the key `Key::open` gave, which every space's encryption data must name.
134 pub fn unlock(index: &'a RevisionIndex<'a>, key: &Key, limits: Limits) -> Result<Self> {
135 Self::open_with(index, limits, |metadata, _| {
136 match metadata == &key.metadata[..] {
137 true => Ok(key.key.clone()),
138 false => Err(Error::PasswordMismatch),
139 }
140 })
141 }
142
143 fn open_with(
144 index: &'a RevisionIndex<'a>,
145 mut limits: Limits,
146 mut key: impl FnMut(&[u8], &mut u64) -> Result<crypto::Key>,
147 ) -> Result<Self> {
148 if index.store.header.file_type != crate::FileType::Section {
149 return Err(Error::Unsupported);
150 }
151 if !index.store.checksum_mismatches.is_empty() {
152 return Err(invalid("Protected document transaction checksum mismatch"));
153 }
154 let mut result = Self {
155 index,
156 objects: BTreeMap::new(),
157 files: BTreeMap::new(),
158 };
159 let mut keys = BTreeMap::new();
160 let mut file_keys = BTreeMap::new();
161 let hashes = hashes(index.store)?;
162 for (space_id, space) in &index.spaces {
163 let mut metadata = None;
164 for revision in space.revisions.values() {
165 if !revision.encrypted {
166 return Err(Error::Unsupported);
167 }
168 let Some(node) = revision.nodes.first().filter(|node| node.id == 0x7c) else {
169 continue;
170 };
171 let Some(Reference::Data(chunk)) = node.reference else {
172 return Err(invalid("Missing encryption key reference"));
173 };
174 let data = index.store.encryption_key(chunk)?;
175 if metadata.replace(data).is_some_and(|old| old != data) {
176 return Err(invalid("Object space changes its encryption metadata"));
177 }
178 }
179 let metadata =
180 metadata.ok_or_else(|| invalid("Protected object space has no revision"))?;
181 if !keys.contains_key(metadata) {
182 keys.insert(metadata, key(metadata, &mut limits.kdf_rounds)?);
183 }
184 let key = &keys[metadata];
185 for rid in space.labels.values().copied().collect::<BTreeSet<_>>() {
186 let revision = index.resolve(*space_id, rid)?;
187 limits.object_visits = limits
188 .object_visits
189 .checked_sub(revision.objects.len())
190 .ok_or(Error::Limit)?;
191 for object in revision.objects.values() {
192 match object.data {
193 ObjectData::Encrypted(bytes) => {
194 let identity = (*space_id, bytes.as_ptr().addr());
195 let expected = hashes.get(&identity.1);
196 if object.jcid & 0x100000 != 0 && expected.is_none() {
197 return Err(invalid("Read-only encrypted object has no hash"));
198 }
199 if result.objects.contains_key(&identity) {
200 continue;
201 }
202 limits.decoded_bytes = limits
203 .decoded_bytes
204 .checked_sub(bytes.len())
205 .ok_or(Error::Limit)?;
206 let decoded = key.property(bytes)?;
207 if expected.is_some_and(|expected| digest(&decoded) != *expected) {
208 return Err(invalid("Decrypted read-only object hash mismatch"));
209 }
210 result.objects.insert(identity, decoded);
211 }
212 ObjectData::File { .. } => {
213 if let Some(FileDataReference::Internal(guid)) =
214 object.file_reference()?
215 {
216 if file_keys
217 .insert(guid, metadata)
218 .is_some_and(|old| old != metadata)
219 {
220 return Err(invalid(
221 "File payload uses inconsistent encryption metadata",
222 ));
223 }
224 if result.files.contains_key(&guid) {
225 continue;
226 }
227 let bytes = index.store.file_data(guid)?;
228 limits.decoded_bytes = limits
229 .decoded_bytes
230 .checked_sub(bytes.len())
231 .ok_or(Error::Limit)?;
232 result.files.insert(guid, key.file(bytes)?);
233 }
234 }
235 ObjectData::Properties(_) => {
236 return Err(invalid("Protected revision contains clear properties"));
237 }
238 }
239 }
240 }
241 }
242 for (space, info) in &index.spaces {
243 for rid in info.labels.values().copied().collect::<BTreeSet<_>>() {
244 result.resolve(*space, rid)?.reachable()?;
245 }
246 }
247 result.document()?.pages()?;
248 Ok(result)
249 }
250
251 fn resolve(
252 &self,
253 space: ExGuid,
254 rid: ExGuid,
255 ) -> std::result::Result<crate::ResolvedRevision<'_>, crate::Error> {
256 let mut revision = self.index.resolve(space, rid)?;
257 for object in revision.objects.values_mut() {
258 if let ObjectData::Encrypted(bytes) = object.data {
259 let decoded =
260 self.objects
261 .get(&(space, bytes.as_ptr().addr()))
262 .ok_or(crate::Error {
263 offset: 0,
264 message: "Protected object was not decoded",
265 })?;
266 object.data = ObjectData::Properties(decoded);
267 }
268 }
269 Ok(revision)
270 }
271
272 pub fn document(&self) -> std::result::Result<Document<'_>, crate::Error> {
273 Document::parse_with(
274 self.index,
275 |space, rid| self.resolve(space, rid),
276 |id| {
277 self.files
278 .get(&id)
279 .map(|bytes| bytes.as_slice())
280 .ok_or(crate::Error {
281 offset: 0,
282 message: "Protected file payload was not decoded",
283 })
284 },
285 )
286 }
287}
288
289/// A protected section's key, opened with its password or made for a new one. Holds no
290/// password; the key is cleared when its last clone drops.
291#[derive(Clone)]
292pub struct Key {
293 key: crypto::Key,
294 /// The encryption data (MS-ONESTORE 2.5.19) that opens the key.
295 metadata: Arc<[u8]>,
296}
297
298impl fmt::Debug for Key {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 f.write_str("Key")
301 }
302}
303
304impl Key {
305 /// Opens the key of the protected section `image` with `password`.
306 pub fn open(image: &[u8], password: &str) -> Result<Self> {
307 let store = Store::parse(image)?;
308 let metadata = metadata(&store)?.ok_or(Error::Unsupported)?;
309 Ok(Self {
310 key: crypto::Key::open(metadata, password, &mut Limits::default().kdf_rounds)?,
311 metadata: Arc::from(metadata),
312 })
313 }
314
315 /// A fresh key for `password`, with OneNote 2010's encryption data.
316 pub fn new(password: &str) -> Result<Self> {
317 let (key, metadata) = crypto::Key::create(password)?;
318 Ok(Self {
319 key,
320 metadata: Arc::from(metadata),
321 })
322 }
323
324 /// The AES key the section's content is encrypted under, for keys derived from it.
325 pub fn secret(&self) -> &[u8; 16] {
326 self.key.value()
327 }
328
329 pub(crate) fn metadata(&self) -> &[u8] {
330 &self.metadata
331 }
332}
333
334/// The section `image` written anew, under `new` or in the clear, as OneNote 2010 rewrites
335/// a section whose password it sets, changes or removes: the file and its object spaces
336/// take new identities, each space keeps its labelled revisions, every one a checkpoint,
337/// and the payloads come along. A protected `image` opens under `key`.
338pub fn rekey(image: &[u8], key: Option<&Key>, new: Option<&Key>) -> Result<Vec<u8>> {
339 let store = Store::parse(image)?;
340 if !store.checksum_mismatches.is_empty() {
341 return Err(invalid(
342 "Cannot write a file with transaction checksum damage",
343 ));
344 }
345 let index = RevisionIndex::parse(&store)?;
346 let opened = match (key, metadata(&store)?) {
347 (Some(key), Some(_)) => Some(Opened::new(&store, key)?),
348 (None, None) => None,
349 (None, Some(_)) => return Err(Error::PasswordMismatch),
350 (Some(_), None) => return Err(invalid("The section is not protected")),
351 };
352 let arena = Bump::new();
353 let mut fresh = BTreeMap::new();
354 for space in index.spaces.keys() {
355 if let std::collections::btree_map::Entry::Vacant(entry) = fresh.entry(space.guid) {
356 entry.insert(crate::write::fresh_guid()?);
357 }
358 }
359 // OneNote gives the payloads new identities too.
360 let mut files = BTreeMap::new();
361 for guid in crate::write::declared_payloads(&store) {
362 files.insert(guid, crate::write::fresh_guid()?);
363 }
364 let rename = |id: ExGuid| ExGuid {
365 guid: fresh.get(&id.guid).copied().unwrap_or(id.guid),
366 n: id.n,
367 };
368 let mut spaces = Vec::new();
369 for (space, info) in &index.spaces {
370 let mut labelled: Vec<(ExGuid, Vec<(ExGuid, u32)>)> = Vec::new();
371 // The current revision first, as OneNote writes it.
372 let mut labels: Vec<_> = info.labels.iter().collect();
373 labels.sort_by_key(|(label, _)| **label != (ExGuid::default(), 1));
374 for (label, rid) in labels {
375 match labelled.iter_mut().find(|(known, _)| known == rid) {
376 Some((_, names)) => names.push(*label),
377 None => labelled.push((*rid, vec![*label])),
378 }
379 }
380 let mut revisions = Vec::new();
381 for (rid, labels) in labelled {
382 let mut resolved = index.resolve(*space, rid)?;
383 if let Some(opened) = &opened {
384 for object in resolved.objects.values_mut() {
385 if let ObjectData::Encrypted(stored) = object.data {
386 object.data = ObjectData::Properties(opened.property(&arena, stored)?);
387 }
388 }
389 }
390 let mut objects = BTreeMap::new();
391 for id in resolved.reachable()? {
392 let mut object = resolved.objects[&id].clone();
393 if let (Some(FileDataReference::Internal(guid)), ObjectData::File { extension, .. }) =
394 (object.file_reference()?, object.data)
395 && let Some(renamed) = files.get(&guid)
396 {
397 let reference: Vec<u8> = crate::op::content::payload_reference(*renamed)
398 .encode_utf16()
399 .flat_map(u16::to_le_bytes)
400 .collect();
401 object.data = ObjectData::File {
402 reference: arena.alloc_slice_copy(&reference),
403 extension,
404 };
405 }
406 object.global_ids = Arc::new(
407 object
408 .global_ids
409 .iter()
410 .map(|(entry, guid)| (*entry, fresh.get(guid).copied().unwrap_or(*guid)))
411 .collect(),
412 );
413 objects.insert(rename(id), object);
414 }
415 let roots = resolved
416 .roots
417 .iter()
418 .map(|(role, id)| (*role, rename(*id)))
419 .collect();
420 revisions.push(crate::write::Labelled {
421 labels,
422 revision: crate::ResolvedRevision { roots, objects },
423 });
424 }
425 spaces.push((rename(*space), revisions));
426 }
427 let mut payloads = Vec::new();
428 for (guid, renamed) in &files {
429 let stored = store.file_data(*guid)?;
430 let clear = match &opened {
431 Some(opened) => opened.file(&arena, stored)?,
432 None => stored,
433 };
434 payloads.push((*renamed, clear));
435 }
436 let placement = image[128..148].try_into().unwrap();
437 let skeleton = crate::create::skeleton(rename(index.root), placement)?;
438 let written = crate::write::rewrite(skeleton, &spaces, &payloads, new)?;
439 let arena = crate::Arena::default();
440 match new {
441 Some(new) => drop(crate::Section::unlock(&arena, written.clone(), new)?),
442 None => drop(crate::Section::open(&arena, written.clone())?),
443 }
444 Ok(written)
445}
446
447/// The encryption data every revision of `store` names; none in a section without any.
448fn metadata<'a>(store: &Store<'a>) -> Result<Option<&'a [u8]>> {
449 let mut found = None;
450 for node in store.lists.values().flat_map(|list| &list.nodes) {
451 if node.id != 0x7c {
452 continue;
453 }
454 let Some(Reference::Data(chunk)) = node.reference else {
455 return Err(invalid("Missing encryption key reference"));
456 };
457 let data = store.encryption_key(chunk)?;
458 if found.replace(data).is_some_and(|old| old != data) {
459 return Err(Error::Unsupported);
460 }
461 }
462 Ok(found)
463}
464
465/// The hashes read-only declarations give their stored bytes, by address.
466fn hashes(store: &Store<'_>) -> Result<BTreeMap<usize, [u8; 16]>> {
467 let mut hashes = BTreeMap::new();
468 for node in store.lists.values().flat_map(|list| &list.nodes) {
469 if !matches!(node.id, 0xc4 | 0xc5) {
470 continue;
471 }
472 let Some(Reference::Data(chunk)) = node.reference else {
473 return Err(invalid("Read-only object has no data reference"));
474 };
475 let bytes = store.chunk_data(chunk)?;
476 let expected: [u8; 16] = node
477 .payload
478 .last_chunk::<16>()
479 .copied()
480 .ok_or_else(|| invalid("Missing read-only object hash"))?;
481 if hashes
482 .insert(bytes.as_ptr().addr(), expected)
483 .is_some_and(|old| old != expected)
484 {
485 return Err(invalid("Inconsistent read-only hashes for one payload"));
486 }
487 }
488 Ok(hashes)
489}
490
491/// A protected read-only declaration's hash: MD5 of the clear bytes zero-padded to 8.
492pub(crate) fn digest(clear: &[u8]) -> [u8; 16] {
493 let mut hash = md5::Context::new();
494 hash.consume(clear);
495 hash.consume(&[0; 7][..(8 - clear.len() % 8) % 8]);
496 hash.finalize().0
497}
498
499/// A `Section`'s key, with what it decoded under it: clear bytes by the address of the
500/// stored bytes they decode, and stored bytes by the address of their clear bytes.
501pub(crate) struct Opened<'a> {
502 key: Key,
503 /// The encryption data each revision names.
504 chunk: Chunk,
505 /// Read-only declarations' hashes by the address of their stored bytes.
506 hashes: BTreeMap<usize, [u8; 16]>,
507 clear: RefCell<BTreeMap<usize, &'a [u8]>>,
508 stored: RefCell<BTreeMap<usize, &'a [u8]>>,
509}
510
511impl<'a> Opened<'a> {
512 /// `key` for the section `store` holds, every object space of which it must encrypt.
513 pub(crate) fn new(store: &Store<'_>, key: &Key) -> Result<Self> {
514 if metadata(store)?.ok_or(Error::Unsupported)? != &key.metadata[..] {
515 return Err(Error::PasswordMismatch);
516 }
517 let index = RevisionIndex::parse(store)?;
518 if index
519 .spaces
520 .values()
521 .flat_map(|space| space.revisions.values())
522 .any(|revision| !revision.encrypted)
523 {
524 return Err(invalid(
525 "A protected section stores a revision in the clear",
526 ));
527 }
528 let chunk = store
529 .lists
530 .values()
531 .flat_map(|list| &list.nodes)
532 .find_map(|node| match node.reference {
533 Some(Reference::Data(chunk)) if node.id == 0x7c => Some(chunk),
534 _ => None,
535 })
536 .ok_or(Error::Unsupported)?;
537 Ok(Self {
538 key: key.clone(),
539 chunk,
540 hashes: hashes(store)?,
541 clear: RefCell::default(),
542 stored: RefCell::default(),
543 })
544 }
545
546 /// `key` for writing a new image whose encryption data lies at `chunk`.
547 pub(crate) fn sealing(key: &Key, chunk: Chunk) -> Self {
548 Self {
549 key: key.clone(),
550 chunk,
551 hashes: BTreeMap::new(),
552 clear: RefCell::default(),
553 stored: RefCell::default(),
554 }
555 }
556
557 /// The clear bytes of a stored object, decoded once into `arena`.
558 pub(crate) fn property(&self, arena: &'a Bump, stored: &'a [u8]) -> Format<&'a [u8]> {
559 if let Some(clear) = self.clear.borrow().get(&stored.as_ptr().addr()) {
560 return Ok(clear);
561 }
562 let decoded = self.key.key.property(stored).map_err(crate::Error::from)?;
563 if let Some(expected) = self.hashes.get(&stored.as_ptr().addr())
564 && digest(&decoded) != *expected
565 {
566 return Err(crate::Error {
567 offset: 0,
568 message: "Decrypted read-only object hash mismatch",
569 });
570 }
571 let clear: &'a [u8] = arena.alloc_slice_copy(&decoded);
572 self.sealed(clear, stored);
573 Ok(clear)
574 }
575
576 /// The clear bytes of a stored payload, in `arena`.
577 pub(crate) fn file(&self, arena: &'a Bump, stored: &[u8]) -> Format<&'a [u8]> {
578 Ok(arena.alloc_slice_copy(&self.key.key.file(stored).map_err(crate::Error::from)?))
579 }
580
581 pub(crate) fn key(&self) -> &Key {
582 &self.key
583 }
584
585 /// Records that `clear` is stored as `stored`.
586 pub(crate) fn sealed(&self, clear: &'a [u8], stored: &'a [u8]) {
587 self.clear
588 .borrow_mut()
589 .insert(stored.as_ptr().addr(), clear);
590 self.stored
591 .borrow_mut()
592 .insert(clear.as_ptr().addr(), stored);
593 }
594}
595
596impl crate::write::Protection for Opened<'_> {
597 fn stored(&self, clear: &[u8]) -> Option<&[u8]> {
598 self.stored.borrow().get(&clear.as_ptr().addr()).copied()
599 }
600
601 fn seal_property(&self, clear: &[u8]) -> Format<Vec<u8>> {
602 seal_property(&self.key, clear)
603 }
604
605 fn seal_file(&self, clear: &[u8]) -> Format<Vec<u8>> {
606 self.key.key.seal_file(clear).map_err(crate::Error::from)
607 }
608
609 fn open_property(&self, stored: &[u8]) -> Format<Zeroizing<Vec<u8>>> {
610 self.key.key.property(stored).map_err(crate::Error::from)
611 }
612
613 fn open_file(&self, stored: &[u8]) -> Format<Zeroizing<Vec<u8>>> {
614 self.key.key.file(stored).map_err(crate::Error::from)
615 }
616
617 fn metadata(&self) -> Chunk {
618 self.chunk
619 }
620}
621
622/// A property object's stored form under `key`, with a fresh IV.
623fn seal_property(key: &Key, clear: &[u8]) -> Format<Vec<u8>> {
624 let mut iv = [0; 16];
625 getrandom::fill(&mut iv).map_err(|_| crate::Error {
626 offset: 0,
627 message: "System random source failed",
628 })?;
629 key.key.seal_property(clear, iv).map_err(crate::Error::from)
630}