diff --git a/libraries/persistent_store/Cargo.toml b/libraries/persistent_store/Cargo.toml index 5fe7c9f0..b01cfdbf 100644 --- a/libraries/persistent_store/Cargo.toml +++ b/libraries/persistent_store/Cargo.toml @@ -7,5 +7,8 @@ edition = "2018" [dependencies] +[dev-dependencies] +tempfile = "3" + [features] std = [] diff --git a/libraries/persistent_store/src/file.rs b/libraries/persistent_store/src/file.rs new file mode 100644 index 00000000..552cc7ea --- /dev/null +++ b/libraries/persistent_store/src/file.rs @@ -0,0 +1,193 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! File-backed persistent flash storage for virtual authenticator. +//! +//! [`FileStorage`] implements the flash [`Storage`] interface but doesn't interface with an +//! actual flash storage. Instead it uses a host-based file to persist the storage state. + +use crate::{Storage, StorageIndex, StorageResult}; +use alloc::borrow::Cow; +use core::cell::RefCell; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::Path; + +/// Simulates a flash storage using a host-based file. +/// +/// This is usable for emulating authenticator hardware on VM hypervisor's host OS. +pub struct FileStorage { + // Options of the storage. + options: FileOptions, + + /// File for persisting contents of the storage. + /// + /// Reading data from File requires mutable reference, as seeking and reading data + /// changes file's current position. + /// + /// All operations on backing file internally always first seek to needed position, + /// so it's safe to borrow mutable reference to backing file for the time of operation. + file: RefCell, +} + +/// Options for file-backed storage. +pub struct FileOptions { + /// Size of a word in bytes. + pub word_size: usize, + + /// Size of a page in bytes. + pub page_size: usize, + + /// Number of pages in storage. + pub num_pages: usize, +} + +impl FileStorage { + pub fn new(path: &Path, options: FileOptions) -> StorageResult { + let mut file_ref = RefCell::new( + OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(path)?, + ); + let file = file_ref.get_mut(); + let file_len = file.metadata()?.len(); + let store_len: u64 = (options.page_size * options.num_pages) as u64; + + if file_len == 0 { + file.seek(SeekFrom::Start(0))?; + let buf = vec![0xff; options.page_size]; + for _ in 0..options.num_pages { + file.write(&buf)?; + } + } else if file_len != store_len { + // FileStorage buffer should be of fixed size, opening previously saved file + // from storage of different size is not supported + panic!("Invalid file size {}, should be {}", file_len, store_len); + } + Ok(FileStorage { + options, + file: file_ref, + }) + } +} + +impl Storage for FileStorage { + fn word_size(&self) -> usize { + self.options.word_size + } + + fn page_size(&self) -> usize { + self.options.page_size + } + + fn num_pages(&self) -> usize { + self.options.num_pages + } + + fn max_word_writes(&self) -> usize { + // We can write an unlimited amount of times in a file, but the store arithmetic + // uses `Nat` so the value should fit in a `Nat`. + u32::MAX as usize + } + + fn max_page_erases(&self) -> usize { + // We can "erase" an unlimited amount of times in a file, but the store format + // encodes the number of erase cycles on 16 bits. + u16::MAX as usize + } + + fn read_slice(&self, index: StorageIndex, length: usize) -> StorageResult> { + let mut file = self.file.borrow_mut(); + file.seek(SeekFrom::Start(index.range(length, self)?.start as u64))?; + let mut buf = vec![0u8; length]; + file.read_exact(&mut buf)?; + Ok(Cow::Owned(buf)) + } + + fn write_slice(&mut self, index: StorageIndex, value: &[u8]) -> StorageResult<()> { + let mut file = self.file.borrow_mut(); + file.seek(SeekFrom::Start( + index.range(value.len(), self)?.start as u64, + ))?; + file.write_all(value)?; + Ok(()) + } + + fn erase_page(&mut self, page: usize) -> StorageResult<()> { + let mut file = self.file.borrow_mut(); + let index = StorageIndex { page, byte: 0 }; + file.seek(SeekFrom::Start( + index.range(self.page_size(), self)?.start as u64, + ))?; + file.write_all(&vec![0xff; self.page_size()][..])?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + use tempfile::TempDir; + + const BLANK_WORD: &[u8] = &[0xff, 0xff, 0xff, 0xff]; + const DATA_WORD: &[u8] = &[0xee, 0xdd, 0xbb, 0x77]; + + const FILE_NAME: &str = "opensk_storage.bin"; + + const OPTIONS: FileOptions = FileOptions { + word_size: 4, + page_size: 0x1000, + num_pages: 20, + }; + + fn make_tmp_dir() -> PathBuf { + let tmp_dir = TempDir::new().unwrap(); + tmp_dir.into_path() + } + + fn remove_tmp_dir(tmp_dir: PathBuf) { + std::fs::remove_dir_all(tmp_dir).unwrap(); + } + + fn temp_storage(tmp_dir: &PathBuf) -> FileStorage { + let mut tmp_file = tmp_dir.clone(); + tmp_file.push(FILE_NAME); + FileStorage::new(&tmp_file, OPTIONS).unwrap() + } + + #[test] + fn read_write_persist_ok() { + let index = StorageIndex { page: 0, byte: 0 }; + let next_index = StorageIndex { page: 0, byte: 4 }; + + let tmp_dir = make_tmp_dir(); + { + let mut file_storage = temp_storage(&tmp_dir); + assert_eq!(file_storage.read_slice(index, 4).unwrap(), BLANK_WORD); + file_storage.write_slice(index, DATA_WORD).unwrap(); + assert_eq!(file_storage.read_slice(index, 4).unwrap(), DATA_WORD); + assert_eq!(file_storage.read_slice(next_index, 4).unwrap(), BLANK_WORD); + } + // Reload and check the data from previously persisted storage + { + let file_storage = temp_storage(&tmp_dir); + assert_eq!(file_storage.read_slice(index, 4).unwrap(), DATA_WORD); + assert_eq!(file_storage.read_slice(next_index, 4).unwrap(), BLANK_WORD); + } + remove_tmp_dir(tmp_dir); + } +} diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index ca82a475..75eed03c 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -365,6 +365,8 @@ extern crate alloc; mod buffer; #[cfg(feature = "std")] mod driver; +#[cfg(feature = "std")] +mod file; mod format; pub mod fragment; #[cfg(feature = "std")] @@ -381,6 +383,8 @@ pub use self::driver::{ StoreDriver, StoreDriverOff, StoreDriverOn, StoreInterruption, StoreInvariant, }; #[cfg(feature = "std")] +pub use self::file::{FileOptions, FileStorage}; +#[cfg(feature = "std")] pub use self::model::{StoreModel, StoreOperation}; pub use self::storage::{Storage, StorageError, StorageIndex, StorageResult}; pub use self::store::{ diff --git a/libraries/persistent_store/src/storage.rs b/libraries/persistent_store/src/storage.rs index 696c2584..473965b2 100644 --- a/libraries/persistent_store/src/storage.rs +++ b/libraries/persistent_store/src/storage.rs @@ -36,6 +36,13 @@ pub enum StorageError { CustomError, } +#[cfg(feature = "std")] +impl From for StorageError { + fn from(_: std::io::Error) -> Self { + Self::CustomError + } +} + pub type StorageResult = Result; /// Abstracts a flash storage.