1 /*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 //! A module for writing to a file from a trusted world to an untrusted storage.
18 //!
19 //! Architectural Model:
20 //! * Trusted world: the writer, a signing secret, has some memory, but NO persistent storage.
21 //! * Untrusted world: persistent storage, assuming untrusted.
22 //! * IPC mechanism between trusted and untrusted world
23 //!
24 //! Use cases:
25 //! * In the trusted world, we want to generate a large file, sign it, and share the signature for
26 //! a third party to verify the file.
27 //! * In the trusted world, we want to read a previously signed file back with signature check
28 //! without having to touch the whole file.
29 //!
30 //! Requirements:
31 //! * Communication between trusted and untrusted world is not cheap, and files can be large.
32 //! * A file write pattern may not be sequential, neither does read.
33 //!
34 //! Considering the above, a technique similar to fs-verity is used. fs-verity uses an alternative
35 //! hash function, a Merkle tree, to calculate the hash of file content. A file update at any
36 //! location will propagate the hash update from the leaf to the root node. Unlike fs-verity, which
37 //! assumes static files, to support write operation, we need to allow the file (thus tree) to
38 //! update.
39 //!
40 //! For the trusted world to generate a large file with random write and hash it, the writer needs
41 //! to hold some private information and update the Merkle tree during a file write (or even when
42 //! the Merkle tree needs to be stashed to the untrusted storage).
43 //!
44 //! A write to a file must update the root hash. In order for the root hash to update, a tree
45 //! walk to update from the write location to the root node is necessary. Importantly, in case when
46 //! (part of) the Merkle tree needs to be read from the untrusted storage (e.g. not yet verified in
47 //! cache), the original path must be verified by the trusted signature before the update to happen.
48 //!
49 //! Denial-of-service is a known weakness if the untrusted storage decides to simply remove the
50 //! file. But there is nothing we can do in this architecture.
51 //!
52 //! Rollback attack is another possible attack, but can be addressed with a rollback counter when
53 //! possible.
54
55 use std::io;
56 use std::sync::{Arc, RwLock};
57
58 use super::builder::MerkleLeaves;
59 use crate::common::{ChunkedSizeIter, CHUNK_SIZE};
60 use crate::crypto::{CryptoError, Sha256Hash, Sha256Hasher};
61 use crate::file::{ChunkBuffer, RandomWrite, ReadByChunk};
62
63 // Implement the conversion from `CryptoError` to `io::Error` just to avoid manual error type
64 // mapping below.
65 impl From<CryptoError> for io::Error {
from(error: CryptoError) -> Self66 fn from(error: CryptoError) -> Self {
67 io::Error::new(io::ErrorKind::Other, error)
68 }
69 }
70
debug_assert_usize_is_u64()71 fn debug_assert_usize_is_u64() {
72 // Since we don't need to support 32-bit CPU, make an assert to make conversion between
73 // u64 and usize easy below. Otherwise, we need to check `divide_roundup(offset + buf.len()
74 // <= usize::MAX` or handle `TryInto` errors.
75 debug_assert!(usize::MAX as u64 == u64::MAX, "Only 64-bit arch is supported");
76 }
77
78 /// VerifiedFileEditor provides an integrity layer to an underlying read-writable file, which may
79 /// not be stored in a trusted environment. Only new, empty files are currently supported.
80 pub struct VerifiedFileEditor<F: ReadByChunk + RandomWrite> {
81 file: F,
82 merkle_tree: Arc<RwLock<MerkleLeaves>>,
83 }
84
85 impl<F: ReadByChunk + RandomWrite> VerifiedFileEditor<F> {
86 /// Wraps a supposedly new file for integrity protection.
new(file: F) -> Self87 pub fn new(file: F) -> Self {
88 Self { file, merkle_tree: Arc::new(RwLock::new(MerkleLeaves::new())) }
89 }
90
91 /// Returns the fs-verity digest size in bytes.
get_fsverity_digest_size(&self) -> usize92 pub fn get_fsverity_digest_size(&self) -> usize {
93 Sha256Hasher::HASH_SIZE
94 }
95
96 /// Calculates the fs-verity digest of the current file.
calculate_fsverity_digest(&self) -> io::Result<Sha256Hash>97 pub fn calculate_fsverity_digest(&self) -> io::Result<Sha256Hash> {
98 let merkle_tree = self.merkle_tree.read().unwrap();
99 merkle_tree.calculate_fsverity_digest().map_err(|e| io::Error::new(io::ErrorKind::Other, e))
100 }
101
read_backing_chunk_unverified( &self, chunk_index: u64, buf: &mut ChunkBuffer, ) -> io::Result<usize>102 fn read_backing_chunk_unverified(
103 &self,
104 chunk_index: u64,
105 buf: &mut ChunkBuffer,
106 ) -> io::Result<usize> {
107 self.file.read_chunk(chunk_index, buf)
108 }
109
read_backing_chunk_verified( &self, chunk_index: u64, buf: &mut ChunkBuffer, merkle_tree_locked: &MerkleLeaves, ) -> io::Result<usize>110 fn read_backing_chunk_verified(
111 &self,
112 chunk_index: u64,
113 buf: &mut ChunkBuffer,
114 merkle_tree_locked: &MerkleLeaves,
115 ) -> io::Result<usize> {
116 debug_assert_usize_is_u64();
117
118 if merkle_tree_locked.is_index_valid(chunk_index as usize) {
119 let size = self.read_backing_chunk_unverified(chunk_index, buf)?;
120
121 // Ensure the returned buffer matches the known hash.
122 let hash = Sha256Hasher::new()?.update(buf)?.finalize()?;
123 if !merkle_tree_locked.is_consistent(chunk_index as usize, &hash) {
124 return Err(io::Error::new(io::ErrorKind::InvalidData, "Inconsistent hash"));
125 }
126 Ok(size)
127 } else {
128 Ok(0)
129 }
130 }
131
new_hash_for_incomplete_write( &self, source: &[u8], offset_from_alignment: usize, output_chunk_index: usize, merkle_tree: &mut MerkleLeaves, ) -> io::Result<Sha256Hash>132 fn new_hash_for_incomplete_write(
133 &self,
134 source: &[u8],
135 offset_from_alignment: usize,
136 output_chunk_index: usize,
137 merkle_tree: &mut MerkleLeaves,
138 ) -> io::Result<Sha256Hash> {
139 // The buffer is initialized to 0 purposely. To calculate the block hash, the data is
140 // 0-padded to the block size. When a chunk read is less than a chunk, the initial value
141 // conveniently serves the padding purpose.
142 let mut orig_data = [0u8; CHUNK_SIZE as usize];
143
144 // If previous data exists, read back and verify against the known hash (since the
145 // storage / remote server is not trusted).
146 if merkle_tree.is_index_valid(output_chunk_index) {
147 self.read_backing_chunk_unverified(output_chunk_index as u64, &mut orig_data)?;
148
149 // Verify original content
150 let hash = Sha256Hasher::new()?.update(&orig_data)?.finalize()?;
151 if !merkle_tree.is_consistent(output_chunk_index, &hash) {
152 return Err(io::Error::new(io::ErrorKind::InvalidData, "Inconsistent hash"));
153 }
154 }
155
156 Ok(Sha256Hasher::new()?
157 .update(&orig_data[..offset_from_alignment])?
158 .update(source)?
159 .update(&orig_data[offset_from_alignment + source.len()..])?
160 .finalize()?)
161 }
162
new_chunk_hash( &self, source: &[u8], offset_from_alignment: usize, current_size: usize, output_chunk_index: usize, merkle_tree: &mut MerkleLeaves, ) -> io::Result<Sha256Hash>163 fn new_chunk_hash(
164 &self,
165 source: &[u8],
166 offset_from_alignment: usize,
167 current_size: usize,
168 output_chunk_index: usize,
169 merkle_tree: &mut MerkleLeaves,
170 ) -> io::Result<Sha256Hash> {
171 if current_size as u64 == CHUNK_SIZE {
172 // Case 1: If the chunk is a complete one, just calculate the hash, regardless of
173 // write location.
174 Ok(Sha256Hasher::new()?.update(source)?.finalize()?)
175 } else {
176 // Case 2: For an incomplete write, calculate the hash based on previous data (if
177 // any).
178 self.new_hash_for_incomplete_write(
179 source,
180 offset_from_alignment,
181 output_chunk_index,
182 merkle_tree,
183 )
184 }
185 }
186
size(&self) -> u64187 pub fn size(&self) -> u64 {
188 self.merkle_tree.read().unwrap().file_size()
189 }
190 }
191
192 impl<F: ReadByChunk + RandomWrite> RandomWrite for VerifiedFileEditor<F> {
write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize>193 fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
194 debug_assert_usize_is_u64();
195
196 // The write range may not be well-aligned with the chunk boundary. There are various cases
197 // to deal with:
198 // 1. A write of a full 4K chunk.
199 // 2. A write of an incomplete chunk, possibly beyond the original EOF.
200 //
201 // Note that a write beyond EOF can create a hole. But we don't need to handle it here
202 // because holes are zeros, and leaves in MerkleLeaves are hashes of 4096-zeros by
203 // default.
204
205 // Now iterate on the input data, considering the alignment at the destination.
206 for (output_offset, current_size) in
207 ChunkedSizeIter::new(buf.len(), offset, CHUNK_SIZE as usize)
208 {
209 // Lock the tree for the whole write for now. There may be room to improve to increase
210 // throughput.
211 let mut merkle_tree = self.merkle_tree.write().unwrap();
212
213 let offset_in_buf = (output_offset - offset) as usize;
214 let source = &buf[offset_in_buf as usize..offset_in_buf as usize + current_size];
215 let output_chunk_index = (output_offset / CHUNK_SIZE) as usize;
216 let offset_from_alignment = (output_offset % CHUNK_SIZE) as usize;
217
218 let new_hash = match self.new_chunk_hash(
219 source,
220 offset_from_alignment,
221 current_size,
222 output_chunk_index,
223 &mut merkle_tree,
224 ) {
225 Ok(hash) => hash,
226 Err(e) => {
227 // Return early when any error happens before the right. Even if the hash is not
228 // consistent for the current chunk, we can still consider the earlier writes
229 // successful. Note that nothing persistent has been done in this iteration.
230 let written = output_offset - offset;
231 if written > 0 {
232 return Ok(written as usize);
233 }
234 return Err(e);
235 }
236 };
237
238 // A failed, partial write here will make the backing file inconsistent to the (old)
239 // hash. Nothing can be done within this writer, but at least it still maintains the
240 // (original) integrity for the file. To matches what write(2) describes for an error
241 // case (though it's about direct I/O), "Partial data may be written ... should be
242 // considered inconsistent", an error below is propagated.
243 self.file.write_all_at(source, output_offset)?;
244
245 // Update the hash only after the write succeeds. Note that this only attempts to keep
246 // the tree consistent to what has been written regardless the actual state beyond the
247 // writer.
248 let size_at_least = offset.saturating_add(buf.len() as u64);
249 merkle_tree.update_hash(output_chunk_index, &new_hash, size_at_least);
250 }
251 Ok(buf.len())
252 }
253
resize(&self, size: u64) -> io::Result<()>254 fn resize(&self, size: u64) -> io::Result<()> {
255 debug_assert_usize_is_u64();
256
257 let mut merkle_tree = self.merkle_tree.write().unwrap();
258 // In case when we are truncating the file, we may need to recalculate the hash of the (new)
259 // last chunk. Since the content is provided by the untrusted backend, we need to read the
260 // data back first, verify it, then override the truncated portion with 0-padding for
261 // hashing. As an optimization, we only need to read the data back if the new size isn't a
262 // multiple of CHUNK_SIZE (since the hash is already correct).
263 //
264 // The same thing does not need to happen when the size is growing. Since the new extended
265 // data is always 0, we can just resize the `MerkleLeaves`, where a new hash is always
266 // calculated from 4096 zeros.
267 if size < merkle_tree.file_size() && size % CHUNK_SIZE > 0 {
268 let new_tail_size = (size % CHUNK_SIZE) as usize;
269 let chunk_index = size / CHUNK_SIZE;
270 if new_tail_size > 0 {
271 let mut buf: ChunkBuffer = [0; CHUNK_SIZE as usize];
272 let s = self.read_backing_chunk_verified(chunk_index, &mut buf, &merkle_tree)?;
273 debug_assert!(new_tail_size <= s);
274
275 let zeros = vec![0; CHUNK_SIZE as usize - new_tail_size];
276 let new_hash = Sha256Hasher::new()?
277 .update(&buf[..new_tail_size])?
278 .update(&zeros)?
279 .finalize()?;
280 merkle_tree.update_hash(chunk_index as usize, &new_hash, size);
281 }
282 }
283
284 self.file.resize(size)?;
285 merkle_tree.resize(size as usize);
286
287 Ok(())
288 }
289 }
290
291 impl<F: ReadByChunk + RandomWrite> ReadByChunk for VerifiedFileEditor<F> {
read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize>292 fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
293 let merkle_tree = self.merkle_tree.read().unwrap();
294 self.read_backing_chunk_verified(chunk_index, buf, &merkle_tree)
295 }
296 }
297
298 #[cfg(test)]
299 mod tests {
300 // Test data below can be generated by:
301 // $ perl -e 'print "\x{00}" x 6000' > foo
302 // $ perl -e 'print "\x{01}" x 5000' >> foo
303 // $ fsverity digest foo
304 use super::*;
305 use anyhow::Result;
306 use std::cell::RefCell;
307 use std::convert::TryInto;
308
309 struct InMemoryEditor {
310 data: RefCell<Vec<u8>>,
311 fail_read: bool,
312 }
313
314 impl InMemoryEditor {
new() -> InMemoryEditor315 pub fn new() -> InMemoryEditor {
316 InMemoryEditor { data: RefCell::new(Vec::new()), fail_read: false }
317 }
318 }
319
320 impl RandomWrite for InMemoryEditor {
write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize>321 fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
322 let begin: usize =
323 offset.try_into().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
324 let end = begin + buf.len();
325 if end > self.data.borrow().len() {
326 self.data.borrow_mut().resize(end, 0);
327 }
328 self.data.borrow_mut().as_mut_slice()[begin..end].copy_from_slice(buf);
329 Ok(buf.len())
330 }
331
resize(&self, size: u64) -> io::Result<()>332 fn resize(&self, size: u64) -> io::Result<()> {
333 let size: usize =
334 size.try_into().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
335 self.data.borrow_mut().resize(size, 0);
336 Ok(())
337 }
338 }
339
340 impl ReadByChunk for InMemoryEditor {
read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize>341 fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
342 if self.fail_read {
343 return Err(io::Error::new(io::ErrorKind::Other, "test!"));
344 }
345
346 let borrowed = self.data.borrow();
347 let chunk = &borrowed
348 .chunks(CHUNK_SIZE as usize)
349 .nth(chunk_index as usize)
350 .ok_or_else(|| {
351 io::Error::new(
352 io::ErrorKind::InvalidInput,
353 format!("read_chunk out of bound: index {}", chunk_index),
354 )
355 })?;
356 buf[..chunk.len()].copy_from_slice(chunk);
357 Ok(chunk.len())
358 }
359 }
360
361 #[test]
test_writer() -> Result<()>362 fn test_writer() -> Result<()> {
363 let writer = InMemoryEditor::new();
364 let buf = [1; 4096];
365 assert_eq!(writer.data.borrow().len(), 0);
366
367 assert_eq!(writer.write_at(&buf, 16384)?, 4096);
368 assert_eq!(writer.data.borrow()[16384..16384 + 4096], buf);
369
370 assert_eq!(writer.write_at(&buf, 2048)?, 4096);
371 assert_eq!(writer.data.borrow()[2048..2048 + 4096], buf);
372
373 assert_eq!(writer.data.borrow().len(), 16384 + 4096);
374 Ok(())
375 }
376
377 #[test]
test_verified_writer_no_write() -> Result<()>378 fn test_verified_writer_no_write() -> Result<()> {
379 // Verify fs-verity hash without any write.
380 let file = VerifiedFileEditor::new(InMemoryEditor::new());
381 assert_eq!(
382 file.calculate_fsverity_digest()?,
383 to_u8_vec("3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95")
384 .as_slice()
385 );
386 Ok(())
387 }
388
389 #[test]
test_verified_writer_from_zero() -> Result<()>390 fn test_verified_writer_from_zero() -> Result<()> {
391 // Verify a write of a full chunk.
392 let file = VerifiedFileEditor::new(InMemoryEditor::new());
393 assert_eq!(file.write_at(&[1; 4096], 0)?, 4096);
394 assert_eq!(
395 file.calculate_fsverity_digest()?,
396 to_u8_vec("cd0875ca59c7d37e962c5e8f5acd3770750ac80225e2df652ce5672fd34500af")
397 .as_slice()
398 );
399
400 // Verify a write of across multiple chunks.
401 let file = VerifiedFileEditor::new(InMemoryEditor::new());
402 assert_eq!(file.write_at(&[1; 4097], 0)?, 4097);
403 assert_eq!(
404 file.calculate_fsverity_digest()?,
405 to_u8_vec("2901b849fda2d91e3929524561c4a47e77bb64734319759507b2029f18b9cc52")
406 .as_slice()
407 );
408
409 // Verify another write of across multiple chunks.
410 let file = VerifiedFileEditor::new(InMemoryEditor::new());
411 assert_eq!(file.write_at(&[1; 10000], 0)?, 10000);
412 assert_eq!(
413 file.calculate_fsverity_digest()?,
414 to_u8_vec("7545409b556071554d18973a29b96409588c7cda4edd00d5586b27a11e1a523b")
415 .as_slice()
416 );
417 Ok(())
418 }
419
420 #[test]
test_verified_writer_unaligned() -> Result<()>421 fn test_verified_writer_unaligned() -> Result<()> {
422 // Verify small, unaligned write beyond EOF.
423 let file = VerifiedFileEditor::new(InMemoryEditor::new());
424 assert_eq!(file.write_at(&[1; 5], 3)?, 5);
425 assert_eq!(
426 file.calculate_fsverity_digest()?,
427 to_u8_vec("a23fc5130d3d7b3323fc4b4a5e79d5d3e9ddf3a3f5872639e867713512c6702f")
428 .as_slice()
429 );
430
431 // Verify bigger, unaligned write beyond EOF.
432 let file = VerifiedFileEditor::new(InMemoryEditor::new());
433 assert_eq!(file.write_at(&[1; 6000], 4000)?, 6000);
434 assert_eq!(
435 file.calculate_fsverity_digest()?,
436 to_u8_vec("d16d4c1c186d757e646f76208b21254f50d7f07ea07b1505ff48b2a6f603f989")
437 .as_slice()
438 );
439 Ok(())
440 }
441
442 #[test]
test_verified_writer_with_hole() -> Result<()>443 fn test_verified_writer_with_hole() -> Result<()> {
444 // Verify an aligned write beyond EOF with holes.
445 let file = VerifiedFileEditor::new(InMemoryEditor::new());
446 assert_eq!(file.write_at(&[1; 4096], 4096)?, 4096);
447 assert_eq!(
448 file.calculate_fsverity_digest()?,
449 to_u8_vec("4df2aefd8c2a9101d1d8770dca3ede418232eabce766bb8e020395eae2e97103")
450 .as_slice()
451 );
452
453 // Verify an unaligned write beyond EOF with holes.
454 let file = VerifiedFileEditor::new(InMemoryEditor::new());
455 assert_eq!(file.write_at(&[1; 5000], 6000)?, 5000);
456 assert_eq!(
457 file.calculate_fsverity_digest()?,
458 to_u8_vec("47d5da26f6934484e260630a69eb2eebb21b48f69bc8fbf8486d1694b7dba94f")
459 .as_slice()
460 );
461
462 // Just another example with a small write.
463 let file = VerifiedFileEditor::new(InMemoryEditor::new());
464 assert_eq!(file.write_at(&[1; 5], 16381)?, 5);
465 assert_eq!(
466 file.calculate_fsverity_digest()?,
467 to_u8_vec("8bd118821fb4aff26bb4b51d485cc481a093c68131b7f4f112e9546198449752")
468 .as_slice()
469 );
470 Ok(())
471 }
472
473 #[test]
test_verified_writer_various_writes() -> Result<()>474 fn test_verified_writer_various_writes() -> Result<()> {
475 let file = VerifiedFileEditor::new(InMemoryEditor::new());
476 assert_eq!(file.write_at(&[1; 2048], 0)?, 2048);
477 assert_eq!(file.write_at(&[1; 2048], 4096 + 2048)?, 2048);
478 assert_eq!(
479 file.calculate_fsverity_digest()?,
480 to_u8_vec("4c433d8640c888b629dc673d318cbb8d93b1eebcc784d9353e07f09f0dcfe707")
481 .as_slice()
482 );
483 assert_eq!(file.write_at(&[1; 2048], 2048)?, 2048);
484 assert_eq!(file.write_at(&[1; 2048], 4096)?, 2048);
485 assert_eq!(
486 file.calculate_fsverity_digest()?,
487 to_u8_vec("2a476d58eb80394052a3a783111e1458ac3ecf68a7878183fed86ca0ff47ec0d")
488 .as_slice()
489 );
490 assert_eq!(file.write_at(&[0; 2048], 2048)?, 2048);
491 assert_eq!(file.write_at(&[0; 2048], 4096)?, 2048);
492 assert_eq!(
493 file.calculate_fsverity_digest()?,
494 to_u8_vec("4c433d8640c888b629dc673d318cbb8d93b1eebcc784d9353e07f09f0dcfe707")
495 .as_slice()
496 );
497 assert_eq!(file.write_at(&[1; 4096], 2048)?, 4096);
498 assert_eq!(
499 file.calculate_fsverity_digest()?,
500 to_u8_vec("2a476d58eb80394052a3a783111e1458ac3ecf68a7878183fed86ca0ff47ec0d")
501 .as_slice()
502 );
503 assert_eq!(file.write_at(&[1; 2048], 8192)?, 2048);
504 assert_eq!(file.write_at(&[1; 2048], 8192 + 2048)?, 2048);
505 assert_eq!(
506 file.calculate_fsverity_digest()?,
507 to_u8_vec("23cbac08371e6ee838ebcc7ae6512b939d2226e802337be7b383c3e046047d24")
508 .as_slice()
509 );
510 Ok(())
511 }
512
513 #[test]
test_verified_writer_inconsistent_read() -> Result<()>514 fn test_verified_writer_inconsistent_read() -> Result<()> {
515 let file = VerifiedFileEditor::new(InMemoryEditor::new());
516 assert_eq!(file.write_at(&[1; 8192], 0)?, 8192);
517
518 // Replace the expected hash of the first/0-th chunk. An incomplete write will fail when it
519 // detects the inconsistent read.
520 {
521 let mut merkle_tree = file.merkle_tree.write().unwrap();
522 let overriding_hash = [42; Sha256Hasher::HASH_SIZE];
523 merkle_tree.update_hash(0, &overriding_hash, 8192);
524 }
525 assert!(file.write_at(&[1; 1], 2048).is_err());
526
527 // A write of full chunk can still succeed. Also fixed the inconsistency.
528 assert_eq!(file.write_at(&[1; 4096], 4096)?, 4096);
529
530 // Replace the expected hash of the second/1-th chunk. A write range from previous chunk can
531 // still succeed, but returns early due to an inconsistent read but still successfully. A
532 // resumed write will fail since no bytes can be written due to the same inconsistency.
533 {
534 let mut merkle_tree = file.merkle_tree.write().unwrap();
535 let overriding_hash = [42; Sha256Hasher::HASH_SIZE];
536 merkle_tree.update_hash(1, &overriding_hash, 8192);
537 }
538 assert_eq!(file.write_at(&[10; 8000], 0)?, 4096);
539 assert!(file.write_at(&[10; 8000 - 4096], 4096).is_err());
540 Ok(())
541 }
542
543 #[test]
test_verified_writer_failed_read_back() -> Result<()>544 fn test_verified_writer_failed_read_back() -> Result<()> {
545 let mut writer = InMemoryEditor::new();
546 writer.fail_read = true;
547 let file = VerifiedFileEditor::new(writer);
548 assert_eq!(file.write_at(&[1; 8192], 0)?, 8192);
549
550 // When a read back is needed, a read failure will fail to write.
551 assert!(file.write_at(&[1; 1], 2048).is_err());
552 Ok(())
553 }
554
555 #[test]
test_resize_to_same_size() -> Result<()>556 fn test_resize_to_same_size() -> Result<()> {
557 let file = VerifiedFileEditor::new(InMemoryEditor::new());
558 assert_eq!(file.write_at(&[1; 2048], 0)?, 2048);
559
560 assert!(file.resize(2048).is_ok());
561 assert_eq!(file.size(), 2048);
562
563 assert_eq!(
564 file.calculate_fsverity_digest()?,
565 to_u8_vec("fef1b4f19bb7a2cd944d7cdee44d1accb12726389ca5b0f61ac0f548ae40876f")
566 .as_slice()
567 );
568 Ok(())
569 }
570
571 #[test]
test_resize_to_grow() -> Result<()>572 fn test_resize_to_grow() -> Result<()> {
573 let file = VerifiedFileEditor::new(InMemoryEditor::new());
574 assert_eq!(file.write_at(&[1; 2048], 0)?, 2048);
575
576 // Resize should grow with 0s.
577 assert!(file.resize(4096).is_ok());
578 assert_eq!(file.size(), 4096);
579
580 assert_eq!(
581 file.calculate_fsverity_digest()?,
582 to_u8_vec("9e0e2745c21e4e74065240936d2047340d96a466680c3c9d177b82433e7a0bb1")
583 .as_slice()
584 );
585 Ok(())
586 }
587
588 #[test]
test_resize_to_shrink() -> Result<()>589 fn test_resize_to_shrink() -> Result<()> {
590 let file = VerifiedFileEditor::new(InMemoryEditor::new());
591 assert_eq!(file.write_at(&[1; 4096], 0)?, 4096);
592
593 // Truncate.
594 file.resize(2048)?;
595 assert_eq!(file.size(), 2048);
596
597 assert_eq!(
598 file.calculate_fsverity_digest()?,
599 to_u8_vec("fef1b4f19bb7a2cd944d7cdee44d1accb12726389ca5b0f61ac0f548ae40876f")
600 .as_slice()
601 );
602 Ok(())
603 }
604
605 #[test]
test_resize_to_shrink_with_read_failure() -> Result<()>606 fn test_resize_to_shrink_with_read_failure() -> Result<()> {
607 let mut writer = InMemoryEditor::new();
608 writer.fail_read = true;
609 let file = VerifiedFileEditor::new(writer);
610 assert_eq!(file.write_at(&[1; 4096], 0)?, 4096);
611
612 // A truncate needs a read back. If the read fail, the resize should fail.
613 assert!(file.resize(2048).is_err());
614 Ok(())
615 }
616
617 #[test]
test_resize_to_shirink_to_chunk_boundary() -> Result<()>618 fn test_resize_to_shirink_to_chunk_boundary() -> Result<()> {
619 let mut writer = InMemoryEditor::new();
620 writer.fail_read = true;
621 let file = VerifiedFileEditor::new(writer);
622 assert_eq!(file.write_at(&[1; 8192], 0)?, 8192);
623
624 // Truncate to a chunk boundary. A read error doesn't matter since we won't need to
625 // recalcuate the leaf hash.
626 file.resize(4096)?;
627 assert_eq!(file.size(), 4096);
628
629 assert_eq!(
630 file.calculate_fsverity_digest()?,
631 to_u8_vec("cd0875ca59c7d37e962c5e8f5acd3770750ac80225e2df652ce5672fd34500af")
632 .as_slice()
633 );
634 Ok(())
635 }
636
to_u8_vec(hex_str: &str) -> Vec<u8>637 fn to_u8_vec(hex_str: &str) -> Vec<u8> {
638 assert!(hex_str.len() % 2 == 0);
639 (0..hex_str.len())
640 .step_by(2)
641 .map(|i| u8::from_str_radix(&hex_str[i..i + 2], 16).unwrap())
642 .collect()
643 }
644 }
645