| 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 | |
| 7 | mod crypto; |
| 8 | |
| 9 | use crate::{ |
| 10 | Chunk, ExGuid, FileDataReference, ObjectData, Reference, RevisionIndex, Store, |
| 11 | document::Document, |
| 12 | }; |
| 13 | use bumpalo::Bump; |
| 14 | use std::{ |
| 15 | cell::RefCell, |
| 16 | collections::{BTreeMap, BTreeSet}, |
| 17 | fmt, |
| 18 | sync::Arc, |
| 19 | }; |
| 20 | use zeroize::Zeroizing; |
| 21 | |
| 22 | type Result<T> = std::result::Result<T, Error>; |
| 23 | type Format<T> = std::result::Result<T, crate::Error>; |
| 24 | |
| 25 | #[derive(Debug)] |
| 26 | pub enum Error { |
| 27 | PasswordMismatch, |
| 28 | Unsupported, |
| 29 | Limit, |
| 30 | Invalid(crate::Error), |
| 31 | } |
| 32 | |
| 33 | impl From<crate::Error> for Error { |
| 34 | fn from(value: crate::Error) -> Self { |
| 35 | Self::Invalid(value) |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | impl 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 | |
| 50 | impl 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 | |
| 62 | impl 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 | } |
| 70 | impl 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 | |
| 79 | fn invalid(message: &'static str) -> Error { |
| 80 | Error::Invalid(crate::Error { offset: 0, message }) |
| 81 | } |
| 82 | |
| 83 | #[derive(Debug, Clone, Copy)] |
| 84 | pub 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 | |
| 93 | impl 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 | /// ``` |
| 117 | pub 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 | |
| 124 | impl<'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)] |
| 292 | pub 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 | |
| 298 | impl fmt::Debug for Key { |
| 299 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 300 | f.write_str("Key") |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | impl 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`. |
| 338 | pub 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. |
| 448 | fn 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. |
| 466 | fn 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. |
| 492 | pub(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. |
| 501 | pub(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 | |
| 511 | impl<'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 | |
| 596 | impl 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. |
| 623 | fn 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 | } |