Răsfoiți Sursa

Added encrypted metadata to blocks and used this to store
POSIX file system metadata.

Matthew Carr 2 ani în urmă
părinte
comite
91c5e1765d

+ 14 - 6
TODO.txt

@@ -20,15 +20,12 @@ Remove TryCompose?
 !- 5, 1, mdcarr941@gmail.com, bd6904, mdcarr941@gmail.com, bd6904
 Move crypto::{encrypt, decrypt} into corresponding {EncrypterExt, DecrypterExt}.
 
-- 6
-Create an enum to eliminate the use of Block trait objects?
-
 !- 7, 2, mdcarr941@gmail.com, ?, mdcarr941@gmail.com, fd4356
 Add a ser_sign_into method to SignerExt which serializes a value into a provided Vec<u8> and returns
 a signature over this data. Update BlockStream::flush_integ to use this method.
 
 - 8
-Convert all sector sizes to u64 for portability.
+Convert all sector sizes to u32 for portability.
 
 - 9
 Create an extension trait for u64 with a method for adding an i64 to it. Use this in
@@ -58,7 +55,7 @@ and helper functions are visible to.
 - 15, 13, mdcarr941@gmail.com, 58d1f6, 
 Create a new crate which implements a FUSE daemon.
 
-- 16, 5, mdcarr941@gmail.com, 866533,
+!- 16, 5, mdcarr941@gmail.com, 866533,
 Add the inherit field, which contains the crypto link from the parent block key to the current
 block key, to the block metadata.
 
@@ -66,7 +63,7 @@ block key, to the block metadata.
 SECURITY: Design and implement a mechanism to protect the keys in block's metadata dictionary from
 being correlated with one another. This mechanism must allow a principal with a readcap to be able
 to find their readcap and to rotate the block and create new readcaps for each of the principals in
-the dictionary, but prevent an attacker from being able to identify when two blocks contains
+the dictionary, but prevent an attacker from being able to identify when two blocks contain
 readcaps for the same principal.
 
 - 18, 3, mdcarr941@gmail.com, 8665339,
@@ -85,3 +82,14 @@ Create a type to hold a pipeline of streams. New traits will need to be added fo
 components of the pipeline to implement. Require that the last pipeline component be a particular
 sub-type of this trait. This will allow me to get rid of the dyn Block objects, as they can be
 replaced by instances of this pipeline type.
+
+!- 22, 8, mdcarr941@gmail.com, fe2ffc, mdcarr941@gmail.com, fe2ffc
+Add a new fields to BlockMeta which stores data encrypted using the block key. This information must
+include:
+  * mode bits as u32
+  * Unix timestamps
+  * owner UID and GID
+  * size of block data in bytes as u64
+  * number of hardlinks to the block
+Also include a dictionary for user data, which is indexed using a String and whose values are
+Vec<u8> structs.

+ 154 - 44
crates/btlib/src/blocktree.rs

@@ -5,7 +5,7 @@ mod private {
     use fuse_backend_rs::{
         abi::fuse_abi::{Attr, CreateIn},
         api::filesystem::{
-            Context, Entry, FileSystem, OpenOptions, ZeroCopyReader, ZeroCopyWriter,
+            Context, Entry, FileSystem, FsOptions, OpenOptions, ZeroCopyReader, ZeroCopyWriter,
         },
     };
     use serde::{Deserialize, Serialize};
@@ -16,13 +16,15 @@ mod private {
         io::{self, SeekFrom, Write},
         path::{Path, PathBuf},
         sync::{
-            atomic::{AtomicU64, Ordering},
+            atomic::{AtomicU64, AtomicUsize, Ordering},
             RwLock,
         },
         time::Duration,
     };
 
-    use crate::{crypto::Creds, Block, BlockOpenOptions, BoxInIoErr, Directory, Error, Result};
+    use crate::{
+        crypto::Creds, Block, BlockOpenOptions, BoxInIoErr, DirEntry, Directory, Error, Result,
+    };
 
     type Inode = u64;
 
@@ -39,7 +41,7 @@ mod private {
         }
     }
 
-    type InodeTableValue = Box<dyn Block>;
+    type InodeTableValue = (AtomicUsize, Box<dyn Block>);
     type InodeTable = HashMap<Inode, InodeTableValue>;
     type InodeTableEntry<'a> = hash_map::Entry<'a, Inode, InodeTableValue>;
 
@@ -104,8 +106,11 @@ mod private {
             creds: C,
         ) -> Result<Blocktree<C>> {
             let mut inodes = HashMap::with_capacity(1);
-            inodes.insert(SpecInodes::Sb.into(), sb_block);
-            inodes.insert(SpecInodes::RootDir.into(), root_block);
+            inodes.insert(SpecInodes::Sb.into(), (AtomicUsize::new(1), sb_block));
+            inodes.insert(
+                SpecInodes::RootDir.into(),
+                (AtomicUsize::new(1), root_block),
+            );
             Ok(Blocktree {
                 path: btdir,
                 inodes: RwLock::new(inodes),
@@ -151,15 +156,6 @@ mod private {
                 .open()
         }
 
-        fn inode_release(&self, inode: Inode) -> io::Result<()> {
-            let mut inodes = self
-                .inodes
-                .write()
-                .map_err(|err| Error::custom(err.to_string()))?;
-            inodes.remove(&inode);
-            Ok(())
-        }
-
         fn access_entry<T, F: FnOnce(InodeTableEntry) -> io::Result<T>>(
             &self,
             inode: Inode,
@@ -180,30 +176,43 @@ mod private {
         ) -> io::Result<T> {
             self.access_entry(inode, |mut entry| match &mut entry {
                 InodeTableEntry::Vacant(_) => Err(Error::custom("Inode is not open").into()),
-                InodeTableEntry::Occupied(entry) => cb(entry.get_mut()),
+                InodeTableEntry::Occupied(entry) => cb(&mut entry.get_mut().1),
             })
         }
 
-        /// Updates the superblock file with the next valid inode value.
-        fn update_sb(&self) -> io::Result<()> {
-            let sb = Superblock {
-                generation: self.generation,
-                next_inode: self.next_inode.load(Ordering::SeqCst),
-            };
-            self.ser_write(SpecInodes::Sb.into(), &sb)
-        }
-
-        fn open(&self, inode: Inode) -> io::Result<()> {
+        fn inode_open(&self, inode: Inode) -> io::Result<()> {
             self.access_entry(inode, |entry| match entry {
                 InodeTableEntry::Vacant(entry) => {
                     let block = Self::open_block(&self.path, inode, self.creds.clone())?;
-                    entry.insert(block);
+                    entry.insert((AtomicUsize::new(1), block));
+                    Ok(())
+                }
+                InodeTableEntry::Occupied(entry) => {
+                    entry.get().0.fetch_add(1, Ordering::SeqCst);
                     Ok(())
                 }
-                InodeTableEntry::Occupied(_) => Ok(()),
             })
         }
 
+        fn inode_release(&self, inode: Inode) -> io::Result<()> {
+            let prev = {
+                let inodes = self
+                    .inodes
+                    .read()
+                    .map_err(|err| Error::custom(err.to_string()))?;
+                let pair = inodes.get(&inode).unwrap();
+                pair.0.fetch_sub(1, Ordering::SeqCst)
+            };
+            if 1 == prev {
+                let mut inodes = self
+                    .inodes
+                    .write()
+                    .map_err(|err| Error::custom(err.to_string()))?;
+                inodes.remove(&inode);
+            }
+            Ok(())
+        }
+
         fn inode_write(
             &self,
             inode: Inode,
@@ -216,14 +225,6 @@ mod private {
             })
         }
 
-        fn ser_write<T: Serialize>(&self, inode: Inode, value: &T) -> io::Result<()> {
-            self.access_block(inode, |block| {
-                block.seek(SeekFrom::Start(0))?;
-                write_to(&value, block)?;
-                Ok(())
-            })
-        }
-
         fn inode_read(
             &self,
             inode: Inode,
@@ -235,12 +236,90 @@ mod private {
                 writer.write_from(&mut block.as_mut(), count, off)
             })
         }
+
+        fn ser_write<T: Serialize>(&self, inode: Inode, value: &T) -> io::Result<()> {
+            self.access_block(inode, |block| {
+                block.seek(SeekFrom::Start(0))?;
+                write_to(&value, block)?;
+                Ok(())
+            })
+        }
+
+        fn directory_read(&self, inode: Inode) -> io::Result<(Directory, Attr)> {
+            self.access_block(inode, |block| {
+                block.seek(SeekFrom::Start(0))?;
+                let dir = read_from(block)?;
+                let attr = block
+                    .mut_meta_body()
+                    .access_secrets(|secrets| Ok(secrets.attr()))?;
+                Ok((dir, attr))
+            })
+        }
+
+        /// Updates the superblock file with the next valid inode value.
+        fn update_sb(&self) -> io::Result<()> {
+            let sb = Superblock {
+                generation: self.generation,
+                next_inode: self.next_inode.load(Ordering::SeqCst),
+            };
+            self.ser_write(SpecInodes::Sb.into(), &sb)
+        }
+
+        fn attr_timeout(&self) -> Duration {
+            Duration::from_secs(5)
+        }
+
+        fn entry_timeout(&self) -> Duration {
+            Duration::from_secs(5)
+        }
     }
 
     impl<C: Creds + Clone + 'static> FileSystem for Blocktree<C> {
         type Inode = Inode;
         type Handle = u64;
 
+        fn init(&self, _capable: FsOptions) -> io::Result<FsOptions> {
+            let options = FsOptions::empty()
+                .union(FsOptions::ASYNC_READ.complement())
+                .union(FsOptions::BIG_WRITES)
+                .union(FsOptions::FLOCK_LOCKS)
+                .union(FsOptions::WRITEBACK_CACHE)
+                .union(FsOptions::POSIX_ACL.complement());
+            Ok(options)
+        }
+
+        fn lookup(&self, _ctx: &Context, parent: Self::Inode, name: &CStr) -> io::Result<Entry> {
+            let (dir, ..) = self.directory_read(parent)?;
+            let name = name.to_str().box_err()?;
+            let entry = dir.entries.get(name).ok_or_else(|| {
+                io::Error::new(
+                    io::ErrorKind::NotFound,
+                    format!("no entry for {name} found"),
+                )
+            })?;
+            let inode = match entry {
+                DirEntry::File(entry) => entry.inode,
+                DirEntry::Directory(entry) => entry.inode,
+                DirEntry::Server(_) => {
+                    return Err(io::Error::new(
+                        io::ErrorKind::Unsupported,
+                        "can't lookup server entry",
+                    ))
+                }
+            };
+            let mut stat =
+                self.access_block(inode, |block| Ok(block.meta_body().secrets()?.stat()))?;
+            stat.st_ino = inode;
+            Ok(Entry {
+                inode,
+                generation: self.generation,
+                attr: stat,
+                attr_flags: 0,
+                attr_timeout: self.attr_timeout(),
+                entry_timeout: self.entry_timeout(),
+            })
+        }
+
         fn open(
             &self,
             _ctx: &Context,
@@ -248,7 +327,7 @@ mod private {
             _flags: u32,
             _fuse_flags: u32,
         ) -> io::Result<(Option<Self::Handle>, OpenOptions)> {
-            self.open(inode)?;
+            self.inode_open(inode)?;
             Ok((None, OpenOptions::empty()))
         }
 
@@ -266,10 +345,10 @@ mod private {
             self.update_sb()?;
 
             // Create the new file block.
-            self.open(inode)?;
+            self.inode_open(inode)?;
 
             // Add a directory entry to the parent for the new inode.
-            self.access_block(parent, |dir_block| {
+            let attr = self.access_block(parent, |dir_block| {
                 dir_block.seek(SeekFrom::Start(0))?;
                 let mut dir: Directory = read_from(dir_block).box_err()?;
                 let name_str = name
@@ -278,17 +357,19 @@ mod private {
                 dir.add_file(name_str.to_owned(), inode)?;
                 dir_block.seek(SeekFrom::Start(0))?;
                 write_to(&dir, dir_block).box_err()?;
-                Ok(())
+                dir_block.flush()?;
+                Ok(dir_block
+                    .mut_meta_body()
+                    .access_secrets(|secrets| Ok(secrets.attr()))?)
             })?;
 
             let entry = Entry {
                 inode,
                 generation: self.generation,
-                // TODO: These fields need to be initialized properly.
-                attr: Attr::default().into(),
+                attr: attr.into(),
                 attr_flags: 0,
-                attr_timeout: Duration::from_secs(5),
-                entry_timeout: Duration::from_secs(5),
+                attr_timeout: self.attr_timeout(),
+                entry_timeout: self.entry_timeout(),
             };
             // TODO: What is OpenOptions used for?
             let options = OpenOptions::empty();
@@ -410,4 +491,33 @@ mod tests {
 
         assert_eq!(expected, actual)
     }
+
+    #[test]
+    fn lookup() {
+        let dir = TempDir::new("fuse").expect("failed to create temp dir");
+        let creds = test_helpers::NODE_CREDS.clone();
+        let bt = Blocktree::new_empty(dir.path().to_owned(), 0, creds)
+            .expect("failed to create empty blocktree");
+        let name = CString::new("README.md").unwrap();
+        let (expected, ..) = bt
+            .create(
+                &Default::default(),
+                SpecInodes::RootDir.into(),
+                name.as_c_str(),
+                Default::default(),
+            )
+            .expect("failed to create file");
+
+        let actual = bt
+            .lookup(&Default::default(), SpecInodes::RootDir.into(), &name)
+            .expect("lookup failed");
+
+        assert_eq!(expected.generation, actual.generation);
+        assert_eq!(expected.inode, actual.inode);
+        assert_eq!(expected.attr.st_size, actual.attr.st_size);
+        assert_eq!(expected.attr.st_uid, actual.attr.st_uid);
+        assert_eq!(expected.attr.st_gid, actual.attr.st_gid);
+        assert_eq!(expected.attr.st_blksize, actual.attr.st_blksize);
+        assert_eq!(expected.attr.st_blocks, actual.attr.st_blocks);
+    }
 }

+ 11 - 18
crates/btlib/src/crypto/merkle_stream.rs

@@ -5,10 +5,9 @@ pub use private::{
 
 mod private {
     use crate::{
-        crypto::{Encrypter, Error, HashKind, Result, SymKey},
+        crypto::{Error, HashKind, Result},
         trailered::Trailered,
-        BlockPath, BoxInIoErr, Decompose, MetaAccess, Principal, Sectored, TryCompose, WriteInteg,
-        SECTOR_SZ_DEFAULT,
+        BoxInIoErr, Decompose, MetaAccess, Sectored, TryCompose, WriteInteg, SECTOR_SZ_DEFAULT,
     };
     use fuse_backend_rs::file_traits::FileReadWriteVolatile;
     use serde::{Deserialize, Serialize};
@@ -554,15 +553,17 @@ mod private {
     }
 
     impl<T: MetaAccess> MerkleStream<T> {
-        /// Asserts that the root merkle node contains the integrity value given by the inner stream.
+        /// Asserts that the root merkle node contains the integrity value given by the inner
+        /// stream.
         pub fn assert_root_integrity(&mut self) -> Result<()> {
-            let hash_data = self.trailered.integrity();
+            let hash_data = self.trailered.meta_body().integrity();
             self.tree.assert_root_contains(hash_data)
         }
     }
 
     impl<T: Read + Seek> MerkleStream<T> {
-        /// Reads a `MerkleTree` from the end of the given stream and returns a stream which uses it.
+        /// Reads a `MerkleTree` from the end of the given stream and returns a stream which uses
+        /// it.
         pub fn new(inner: T) -> Result<MerkleStream<T>> {
             let (trailered, tree) = Trailered::new(inner)?;
             Ok(MerkleStream {
@@ -645,20 +646,12 @@ mod private {
     }
 
     impl<T: MetaAccess> MetaAccess for MerkleStream<T> {
-        fn block_key(&self) -> crate::Result<SymKey> {
-            self.trailered.block_key()
+        fn meta(&self) -> &crate::BlockMeta {
+            self.trailered.meta()
         }
 
-        fn add_readcap_for(&mut self, owner: Principal, key: &dyn Encrypter) -> crate::Result<()> {
-            self.trailered.add_readcap_for(owner, key)
-        }
-
-        fn integrity(&self) -> Option<&[u8]> {
-            self.trailered.integrity()
-        }
-
-        fn set_path(&mut self, path: BlockPath) {
-            self.trailered.set_path(path)
+        fn mut_meta(&mut self) -> &mut crate::BlockMeta {
+            self.trailered.mut_meta()
         }
     }
 

+ 9 - 2
crates/btlib/src/crypto/mod.rs

@@ -592,6 +592,14 @@ impl VarHash {
     pub fn kind(&self) -> HashKind {
         self.into()
     }
+
+    pub fn as_slice(&self) -> &[u8] {
+        self.as_ref()
+    }
+
+    pub fn as_mut_slice(&mut self) -> &mut [u8] {
+        self.as_mut()
+    }
 }
 
 impl From<HashKind> for VarHash {
@@ -2071,8 +2079,7 @@ mod tests {
         const SECT_SZ: usize = 16;
         const SECT_CT: usize = 8;
         let creds = make_key_pair();
-        let readcap = make_readcap_for(&creds);
-        let mut block = make_block_with(readcap);
+        let mut block = make_block_with(&creds);
         write_fill(&mut block, SECT_SZ, SECT_CT);
         block.seek(SeekFrom::Start(0)).expect("seek failed");
         read_check(block, SECT_SZ, SECT_CT);

+ 6 - 14
crates/btlib/src/crypto/secret_stream.rs

@@ -4,8 +4,8 @@ mod private {
     use fuse_backend_rs::file_traits::FileReadWriteVolatile;
 
     use crate::{
-        crypto::{Encrypter, Error, Result, SymKey},
-        Block, BlockPath, Decompose, MetaAccess, Principal, Sectored, TryCompose,
+        crypto::{Error, Result, SymKey},
+        Block, Decompose, MetaAccess, Sectored, TryCompose,
     };
     use std::io::{self, Read, Seek, SeekFrom, Write};
 
@@ -167,20 +167,12 @@ mod private {
     }
 
     impl<T: MetaAccess> MetaAccess for SecretStream<T> {
-        fn block_key(&self) -> crate::Result<SymKey> {
-            self.inner.block_key()
+        fn meta(&self) -> &crate::BlockMeta {
+            self.inner.meta()
         }
 
-        fn add_readcap_for(&mut self, owner: Principal, key: &dyn Encrypter) -> crate::Result<()> {
-            self.inner.add_readcap_for(owner, key)
-        }
-
-        fn integrity(&self) -> Option<&[u8]> {
-            self.inner.integrity()
-        }
-
-        fn set_path(&mut self, path: BlockPath) {
-            self.inner.set_path(path)
+        fn mut_meta(&mut self) -> &mut crate::BlockMeta {
+            self.inner.mut_meta()
         }
     }
 

+ 303 - 45
crates/btlib/src/lib.rs

@@ -26,16 +26,19 @@ use crypto::{
     MerkleStream, PublicCreds, SecretStream, Sign, Signature, Signer, SymKey, SymKeyKind, VarHash,
 };
 
-use fuse_backend_rs::file_traits::FileReadWriteVolatile;
+use fuse_backend_rs::{
+    abi::fuse_abi::{stat64, Attr},
+    file_traits::FileReadWriteVolatile,
+};
 use log::error;
 use sectored_buf::SectoredBuf;
 use serde::{Deserialize, Serialize};
 use serde_big_array::BigArray;
 use std::{
-    collections::{btree_map, BTreeMap, HashMap},
+    collections::{btree_map, hash_map::DefaultHasher, BTreeMap, HashMap},
     convert::{Infallible, TryFrom},
     fmt::{self, Display, Formatter},
-    hash::Hash as Hashable,
+    hash::{Hash as Hashable, Hasher},
     io::{self, Read, Seek, SeekFrom, Write},
     net::SocketAddr,
     ops::{Add, Sub},
@@ -51,6 +54,7 @@ pub enum Error {
     Serde(btserde::Error),
     Crypto(crypto::Error),
     IncorrectSize { expected: usize, actual: usize },
+    NoBlockKey,
     Custom(Box<dyn std::fmt::Debug + Send + Sync>),
 }
 
@@ -70,6 +74,7 @@ impl Display for Error {
             Error::IncorrectSize { expected, actual } => {
                 write!(f, "incorrect size {actual}, expected {expected}")
             }
+            Error::NoBlockKey => write!(f, "no block key is present"),
             Error::Custom(err) => err.fmt(f),
         }
     }
@@ -184,12 +189,18 @@ impl<T, U: Decompose<T>, S: TryCompose<T, U, Error = Infallible>> Compose<T, U>
     }
 }
 
+/// Trait for accessing the metadata associated with a block.
 pub trait MetaAccess {
-    fn block_key(&self) -> Result<SymKey>;
-    fn add_readcap_for(&mut self, owner: Principal, key: &dyn Encrypter) -> Result<()>;
-    /// Returns the integrity value used to protect the contents of the block.
-    fn integrity(&self) -> Option<&[u8]>;
-    fn set_path(&mut self, path: BlockPath);
+    fn meta(&self) -> &BlockMeta;
+    fn mut_meta(&mut self) -> &mut BlockMeta;
+
+    fn meta_body(&self) -> &BlockMetaBody {
+        self.meta().body()
+    }
+
+    fn mut_meta_body(&mut self) -> &mut BlockMetaBody {
+        self.mut_meta().mut_body()
+    }
 }
 
 /// Extensions to the `Read` trait.
@@ -225,6 +236,108 @@ trait ReadExt: Read {
 
 impl<T: Read> ReadExt for T {}
 
+trait HashExt: Hashable {
+    /// Returns the hash produced by the `DefaultHasher`.
+    fn default_hash(&self) -> u64 {
+        let mut hasher = DefaultHasher::new();
+        self.hash(&mut hasher);
+        hasher.finish()
+    }
+}
+
+impl<T: Hashable> HashExt for T {}
+
+/// Metadata that is encrypted.
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Hash)]
+pub struct BlockMetaSecrets {
+    /// The inode number of the file.
+    inode: u64,
+    /// Mode of file.
+    mode: u32,
+    /// Owner UID of file.
+    uid: u32,
+    /// Owner GID of file.
+    gid: u32,
+    /// Last access time.
+    atime: Epoch,
+    /// Last data modification.
+    mtime: Epoch,
+    /// Last status change.
+    ctime: Epoch,
+    /// Size of the file in bytes.
+    size: u64,
+    /// Number of hard links to the file.
+    nlink: u32,
+    /// The sector size used by the block.
+    sect_sz: u32,
+    tags: BTreeMap<String, Vec<u8>>,
+}
+
+impl BlockMetaSecrets {
+    pub fn new() -> BlockMetaSecrets {
+        Self::default()
+    }
+
+    pub fn attr(&self) -> Attr {
+        self.into()
+    }
+
+    pub fn stat(&self) -> stat64 {
+        self.attr().into()
+    }
+
+    /// Returns the number of sectors occupied by the block's data.
+    pub fn sectors(&self) -> u64 {
+        let sect_sz = self.sect_sz as u64;
+        if self.size % sect_sz == 0 {
+            self.size / sect_sz
+        } else {
+            self.size / sect_sz + 1
+        }
+    }
+}
+
+impl Default for BlockMetaSecrets {
+    fn default() -> Self {
+        Self {
+            inode: 0,
+            mode: 0,
+            uid: 0,
+            gid: 0,
+            atime: Epoch::default(),
+            mtime: Epoch::default(),
+            ctime: Epoch::default(),
+            size: 0,
+            nlink: 0,
+            sect_sz: SECTOR_SZ_DEFAULT.try_into().unwrap(),
+            tags: BTreeMap::new(),
+        }
+    }
+}
+
+impl From<&BlockMetaSecrets> for Attr {
+    fn from(value: &BlockMetaSecrets) -> Self {
+        Attr {
+            ino: value.inode,
+            size: value.size,
+            atime: value.atime.value(),
+            mtime: value.mtime.value(),
+            ctime: value.ctime.value(),
+            atimensec: 0,
+            mtimensec: 0,
+            ctimensec: 0,
+            mode: value.mode,
+            nlink: value.nlink,
+            uid: value.uid,
+            gid: value.gid,
+            rdev: 0,
+            blksize: value.sect_sz,
+            blocks: value.sectors(),
+            flags: 0,
+        }
+    }
+}
+
 /// This struct contains all of the metadata fields associated with a block, except for its
 /// signature. Since this struct implements `Serialize`, this allows for convenient signature
 /// calculations.
@@ -240,33 +353,142 @@ pub struct BlockMetaBody {
     integrity: Option<VarHash>,
     /// The public key that corresponds to the private key used to sign these metadata.
     signing_key: AsymKeyPub<Sign>,
+    /// Additional metadata which is subject to confidentiality protection.
+    secrets: Ciphertext<BlockMetaSecrets>,
+
+    #[serde(skip)]
+    /// The cleartext block key.
+    block_key: Option<SymKey>,
+    #[serde(skip)]
+    /// The clear text secret metadata.
+    secrets_struct: Option<BlockMetaSecrets>,
 }
 
 impl BlockMetaBody {
-    fn new<C: Creds>(creds: &C) -> BlockMetaBody {
-        BlockMetaBody {
+    fn new<C: Creds>(creds: &C) -> Result<BlockMetaBody> {
+        let block_key = SymKey::generate(SymKeyKind::default())?;
+        let mut readcaps = BTreeMap::new();
+        readcaps.insert(creds.principal(), creds.ser_encrypt(&block_key)?);
+        Ok(BlockMetaBody {
             path: BlockPath::default(),
             inherit: None,
-            readcaps: BTreeMap::new(),
+            readcaps,
             writecap: creds.writecap().map(|e| e.to_owned()),
             integrity: None,
             signing_key: creds.public_sign().to_owned(),
+            secrets: block_key.ser_encrypt(&BlockMetaSecrets::default())?,
+            block_key: Some(block_key),
+            secrets_struct: None,
+        })
+    }
+
+    pub fn unlock_block_key<C: Creds>(&mut self, creds: &C) -> Result<()> {
+        if self.block_key.is_some() {
+            return Ok(());
+        }
+        let readcap = self
+            .readcaps
+            .get(&creds.principal())
+            .ok_or(crypto::Error::NoReadCap)?;
+        self.block_key = Some(creds.ser_decrypt(readcap)?);
+        Ok(())
+    }
+
+    pub fn access_secrets<T, F: FnOnce(&mut BlockMetaSecrets) -> Result<T>>(
+        &mut self,
+        accessor: F,
+    ) -> Result<T> {
+        let secrets = match self.secrets_struct.as_mut() {
+            Some(secrets) => secrets,
+            None => {
+                let block_key = self.block_key()?;
+                self.secrets_struct = Some(block_key.ser_decrypt(&self.secrets)?);
+                self.secrets_struct.as_mut().unwrap()
+            }
+        };
+        let prev = secrets.default_hash();
+        let output = accessor(secrets)?;
+        if prev != secrets.default_hash() {
+            self.secrets = self
+                .block_key
+                .as_ref()
+                .ok_or(Error::NoBlockKey)?
+                .ser_encrypt(secrets)?;
         }
+        Ok(output)
+    }
+
+    pub fn secrets(&self) -> Result<&BlockMetaSecrets> {
+        self.secrets_struct
+            .as_ref()
+            .ok_or_else(|| Error::custom("secrets have not been decrypted"))
+    }
+
+    pub fn block_key(&self) -> Result<&SymKey> {
+        self.block_key.as_ref().ok_or(Error::NoBlockKey)
+    }
+
+    pub fn use_block_key_for<C: Creds>(&mut self, creds: &C) -> Result<&SymKey> {
+        let readcap = self
+            .readcaps
+            .get(&creds.principal())
+            .ok_or(crypto::Error::NoReadCap)?;
+        let block_key = creds.ser_decrypt(readcap)?;
+        self.block_key = Some(block_key);
+        self.block_key()
+    }
+
+    pub fn mut_block_key(&mut self) -> &mut Option<SymKey> {
+        &mut self.block_key
+    }
+
+    pub fn integrity(&self) -> Option<&[u8]> {
+        self.integrity.as_ref().map(|hash| hash.as_slice())
+    }
+
+    pub fn add_readcap_for<E: Encrypter>(&mut self, owner: Principal, key: &E) -> Result<()> {
+        let block_key = self.block_key()?;
+        self.readcaps.insert(owner, key.ser_encrypt(block_key)?);
+        Ok(())
+    }
+
+    pub fn set_path(&mut self, path: BlockPath) {
+        self.path = path
     }
 }
 
 /// Signed metadata associated with a block.
 #[derive(Serialize, Deserialize)]
-struct BlockMeta {
+pub struct BlockMeta {
     body: BlockMetaBody,
     sig: Signature,
 }
 
 impl BlockMeta {
-    fn new<C: Creds>(creds: &C) -> BlockMeta {
-        let body = BlockMetaBody::new(creds);
+    fn new<C: Creds>(creds: &C) -> Result<BlockMeta> {
+        let body = BlockMetaBody::new(creds)?;
         let sig = Signature::empty(body.signing_key.scheme());
-        BlockMeta { body, sig }
+        Ok(BlockMeta { body, sig })
+    }
+
+    pub fn body(&self) -> &BlockMetaBody {
+        self.as_ref()
+    }
+
+    pub fn mut_body(&mut self) -> &mut BlockMetaBody {
+        self.as_mut()
+    }
+}
+
+impl AsRef<BlockMetaBody> for BlockMeta {
+    fn as_ref(&self) -> &BlockMetaBody {
+        &self.body
+    }
+}
+
+impl AsMut<BlockMetaBody> for BlockMeta {
+    fn as_mut(&mut self) -> &mut BlockMetaBody {
+        &mut self.body
     }
 }
 
@@ -286,14 +508,12 @@ impl<T: Read + Seek, C: Creds> BlockStream<T, C> {
                 // We need to use the writecap and signing_key provided by the current credentials.
                 meta.body.writecap = creds.writecap().map(|e| e.to_owned());
                 meta.body.signing_key = creds.public_sign().to_owned();
+                meta.body.use_block_key_for(&creds)?;
                 meta
             }
             None => {
-                let mut meta = BlockMeta::new(&creds);
-                let block_key = SymKey::generate(SymKeyKind::default())?;
-                meta.body
-                    .readcaps
-                    .insert(creds.principal(), creds.ser_encrypt(&block_key)?);
+                let mut meta = BlockMeta::new(&creds)?;
+                meta.mut_body().add_readcap_for(creds.principal(), &creds)?;
                 meta.body.writecap = creds.writecap().map(|e| e.to_owned());
                 meta
             }
@@ -349,29 +569,12 @@ impl<T: Seek, C> Seek for BlockStream<T, C> {
 }
 
 impl<T, C: Decrypter + Principaled> MetaAccess for BlockStream<T, C> {
-    fn block_key(&self) -> Result<SymKey> {
-        let readcap = self
-            .meta
-            .body
-            .readcaps
-            .get(&self.creds.principal())
-            .ok_or(Error::Crypto(crypto::Error::NoReadCap))?;
-        Ok(self.creds.ser_decrypt(readcap)?)
+    fn meta(&self) -> &BlockMeta {
+        &self.meta
     }
 
-    fn add_readcap_for(&mut self, owner: Principal, key: &dyn Encrypter) -> Result<()> {
-        let block_key = self.block_key()?;
-        let readcap = key.ser_encrypt(&block_key)?;
-        self.meta.body.readcaps.insert(owner, readcap);
-        Ok(())
-    }
-
-    fn integrity(&self) -> Option<&[u8]> {
-        self.meta.body.integrity.as_ref().map(|hash| hash.as_ref())
-    }
-
-    fn set_path(&mut self, path: BlockPath) {
-        self.meta.body.path = path;
+    fn mut_meta(&mut self) -> &mut BlockMeta {
+        &mut self.meta
     }
 }
 
@@ -465,9 +668,14 @@ impl<T: Read + Write + Seek + FileReadWriteVolatile + 'static, C: Creds + 'stati
 {
     pub fn open(self) -> Result<Box<dyn Block>> {
         let stream = BlockStream::new(self.inner, self.creds)?;
-        let block_key = stream.block_key()?;
+        let block_key = stream.meta_body().block_key().map(|e| e.to_owned())?;
         let mut stream = MerkleStream::new(stream)?;
         stream.assert_root_integrity()?;
+        let sect_sz = stream.sector_sz();
+        stream.mut_meta_body().access_secrets(|secrets| {
+            secrets.sect_sz = sect_sz.try_into()?;
+            Ok(())
+        })?;
         if self.encrypt {
             let stream = SecretStream::new(block_key).try_compose(stream)?;
             let stream = SectoredBuf::new().try_compose(stream)?;
@@ -721,21 +929,41 @@ pub trait Principaled {
 }
 
 /// An instant in time represented by the number of seconds since January 1st 1970, 00:00:00 UTC.
-#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
 pub struct Epoch(u64);
 
 impl Epoch {
     /// Returns the current epoch time.
-    fn now() -> Epoch {
+    pub fn now() -> Epoch {
         let now = SystemTime::now();
         // If the system clock is before the unix epoch, just panic.
         let epoch = now.duration_since(SystemTime::UNIX_EPOCH).unwrap();
         Epoch(epoch.as_secs())
     }
+
+    pub fn from_value(value: u64) -> Self {
+        value.into()
+    }
+
+    pub fn value(self) -> u64 {
+        self.into()
+    }
 }
 
 impl Copy for Epoch {}
 
+impl From<u64> for Epoch {
+    fn from(value: u64) -> Self {
+        Self(value)
+    }
+}
+
+impl From<Epoch> for u64 {
+    fn from(value: Epoch) -> Self {
+        value.0
+    }
+}
+
 impl Add<Duration> for Epoch {
     type Output = Self;
     fn add(self, other: Duration) -> Self {
@@ -786,6 +1014,7 @@ mod tests {
     };
 
     use super::*;
+    use btserde::{from_vec, to_vec};
     use tempdir::TempDir;
     use test_helpers::*;
 
@@ -826,6 +1055,32 @@ mod tests {
             .expect("failed to open block");
     }
 
+    /// Tests that the `BlockMetaBody` struct has an updated secrets struct after it is modified
+    /// in the `access_secrets` method.
+    #[test]
+    fn block_meta_body_secrets_updated_after_access() {
+        const UID: u32 = 1000;
+        let creds = test_helpers::NODE_CREDS.clone();
+
+        let vec = {
+            let mut body = BlockMetaBody::new(&creds).expect("failed to create meta body");
+            body.access_secrets(|secrets| {
+                secrets.uid = UID;
+                Ok(())
+            })
+            .expect("access secrets failed");
+            to_vec(&body).expect("to_vec failed")
+        };
+
+        let mut body = from_vec::<BlockMetaBody>(&vec).expect("from_vec failed");
+        body.unlock_block_key(&creds)
+            .expect("unlock_block_key failed");
+        let actual_uid = body
+            .access_secrets(|secrets| Ok(secrets.uid))
+            .expect("access_secrets failed");
+        assert_eq!(UID, actual_uid);
+    }
+
     struct BlockTestCase {
         root_creds: TpmCreds,
         node_creds: TpmCreds,
@@ -881,7 +1136,9 @@ mod tests {
                 .with_encrypt(true)
                 .open()
                 .expect("failed to open block");
-            block.set_path(self.node_creds.writecap().unwrap().body.path.clone());
+            block
+                .mut_meta_body()
+                .set_path(self.node_creds.writecap().unwrap().body.path.clone());
             block
         }
 
@@ -968,6 +1225,7 @@ mod tests {
         {
             let mut block = case.open_new(&path);
             block
+                .mut_meta_body()
                 .add_readcap_for(app_creds.principal(), &app_creds)
                 .expect("failed to add readcap");
             block.write(&EXPECTED[..MID]).expect("first write failed");

+ 5 - 15
crates/btlib/src/sectored_buf.rs

@@ -8,9 +8,7 @@ mod private {
     use btserde::{read_from, write_to};
 
     use crate::{
-        crypto::{Encrypter, SymKey},
-        Block, BlockPath, BoxInIoErr, Decompose, Error, MetaAccess, Principal, ReadExt, Result,
-        Sectored, TryCompose,
+        Block, BoxInIoErr, Decompose, Error, MetaAccess, ReadExt, Result, Sectored, TryCompose,
     };
 
     /// A stream which buffers writes and read such that the inner stream only sees reads and writes
@@ -312,20 +310,12 @@ mod private {
     }
 
     impl<T: MetaAccess> MetaAccess for SectoredBuf<T> {
-        fn block_key(&self) -> Result<SymKey> {
-            self.inner.block_key()
+        fn meta(&self) -> &crate::BlockMeta {
+            self.inner.meta()
         }
 
-        fn add_readcap_for(&mut self, owner: Principal, key: &dyn Encrypter) -> Result<()> {
-            self.inner.add_readcap_for(owner, key)
-        }
-
-        fn integrity(&self) -> Option<&[u8]> {
-            self.inner.integrity()
-        }
-
-        fn set_path(&mut self, path: BlockPath) {
-            self.inner.set_path(path)
+        fn mut_meta(&mut self) -> &mut crate::BlockMeta {
+            self.inner.mut_meta()
         }
     }
 

+ 6 - 12
crates/btlib/src/test_helpers.rs

@@ -196,18 +196,10 @@ pub(crate) fn make_self_signed_writecap_with<C: Creds>(key: &C) -> Writecap {
     writecap
 }
 
-pub(crate) fn make_readcap_for<C: Encrypter + Principaled>(creds: &C) -> Readcap {
-    Readcap {
-        issued_to: creds.principal(),
-        key: creds
-            .ser_encrypt(&BLOCK_KEY)
-            .expect("failed to encrypt block key"),
-    }
-}
-
-pub(crate) fn make_block_with(readcap: Readcap) -> Box<dyn Block> {
+pub(crate) fn make_block_with<C: CredsPub>(creds: &C) -> Box<dyn Block> {
+    let block_key = SymKey::generate(SymKeyKind::default()).unwrap();
     let mut readcaps = BTreeMap::new();
-    readcaps.insert(readcap.issued_to, readcap.key);
+    readcaps.insert(creds.principal(), creds.ser_encrypt(&block_key).unwrap());
     // Notice that the writecap path contains the block path. If this were not the case, the block
     // would be invalid.
     let (writecap, creds) = make_writecap_and_creds(vec!["apps"]);
@@ -219,13 +211,15 @@ pub(crate) fn make_block_with(readcap: Readcap) -> Box<dyn Block> {
         writecap: Some(writecap),
         integrity: Some(VarHash::from(HashKind::Sha2_256)),
         signing_key: creds.public_sign().to_owned(),
+        secrets: block_key.ser_encrypt(&BlockMetaSecrets::default()).unwrap(),
+        block_key: Some(block_key.clone()),
+        secrets_struct: None,
     };
     let sig = Signature::copy_from(Sign::RSA_PSS_3072_SHA_256, &SIGNATURE);
     let mut stream =
         BlockStream::new(BtCursor::new(Vec::new()), creds).expect("create block stream failed");
     stream.meta.body = header;
     stream.meta.sig = sig;
-    let block_key = stream.block_key().expect("get block key failed");
     let stream = MerkleStream::new(stream).expect("create merkle stream failed");
     let stream = SecretStream::new(block_key)
         .try_compose(stream)

+ 4 - 16
crates/btlib/src/trailered.rs

@@ -142,24 +142,12 @@ mod private {
     }
 
     impl<T: MetaAccess, D> MetaAccess for Trailered<T, D> {
-        fn add_readcap_for(
-            &mut self,
-            owner: crate::Principal,
-            key: &dyn crate::crypto::Encrypter,
-        ) -> Result<()> {
-            self.inner.add_readcap_for(owner, key)
-        }
-
-        fn block_key(&self) -> Result<crate::crypto::SymKey> {
-            self.inner.block_key()
-        }
-
-        fn integrity(&self) -> Option<&[u8]> {
-            self.inner.integrity()
+        fn meta(&self) -> &crate::BlockMeta {
+            self.inner.meta()
         }
 
-        fn set_path(&mut self, path: crate::BlockPath) {
-            self.inner.set_path(path)
+        fn mut_meta(&mut self) -> &mut crate::BlockMeta {
+            self.inner.mut_meta()
         }
     }