From 472d60ae95b7192c0e1781195fcd1523b04701d6 Mon Sep 17 00:00:00 2001 From: bunnie Date: Tue, 23 Nov 2021 15:55:27 +0800 Subject: [PATCH] update a comment in the readme. --- .vscode/settings.json | 1 - services/pddb/README.md | 6 +- services/pddb/src/lib.rs | 360 --------------------------------------- 3 files changed, 4 insertions(+), 363 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4312bfe05..58887c6f6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,3 @@ { "rust-analyzer.checkOnSave.allTargets": false, - "rust-analyzer.cargo.target":"riscv32imac-unknown-xous-elf" } diff --git a/services/pddb/README.md b/services/pddb/README.md index 46df67b58..8f54c6483 100644 --- a/services/pddb/README.md +++ b/services/pddb/README.md @@ -28,5 +28,7 @@ the bindings are "what you expect" (principle of least surprise). Howeve, you're probably looking to fix a PDDB bug, audit the code, or generally figure out what the hell is going on. This means you want to look at the PDDB as a "binary" and not a "library" -- something RustDoc can't -help you with. As the issue above notes, the fallback is for you to consult the README -file. +help you with. As the issue above notes, the fallback is for you to consult this README +file, and literally, read the source (or rather, the paragraphs of plaintext documentation +we've embedded in the source files, waiting for the day when the rustdoc issue is fixed +and these can be turned into something more user-friendly). diff --git a/services/pddb/src/lib.rs b/services/pddb/src/lib.rs index 25b2fc0e7..601f6742e 100644 --- a/services/pddb/src/lib.rs +++ b/services/pddb/src/lib.rs @@ -1,365 +1,5 @@ #![cfg_attr(target_os = "none", no_std)] -/// # PDDB - Plausibly Deniable DataBase -/// -/// ## Glossary: -/// * Basis - a unionizeable dictionary that can be unlocked with a password-protected key. -/// * Dictionary - similar to a "volume" on a typical filesystem. Contains a group of k/v pairs -/// that share attributes such as access permissions -/// * Key - a unique string that identifies a piece of data in the PDDB -/// * Path - Rust's notion of how to locate a file, except we interpret it as a PDDB key, where -/// the "root" directory is the dictionary, and the remainder is the "key" -/// * Value - the value associated with a key, isomorphic ot a "file" in Rust -/// * Open Basis - A Basis that is decryptable and known to the system. -/// * Closed Basis - A Basis that is not decryptable. It's unknown to the system and potentially treated -/// identically to free disk space, e.g., it could be partially overwritten. -/// * FSCB - Free Space Commit Buffer. A few pages allocated to tracking a subset of free space, -/// meant to accelerate PDDB operations. Creates a side-channel that can reveal that some activity -/// has happened, but without disclosing what and why. Contains FastSpace and SpaceUpdate records. -/// Frequently updated, so the buffer is slightly oversized, and which sector is "hot" is randomized -/// for wear-levelling. -/// * MBBB - Make Before Break Buffer. A set of randomly allocated pages that are a shadow copy -/// of a page table page. If any data exists, its contents override those of a corrupted page table. -/// * FastSpace - a collection of random pages that are known to be empty. The number of pages in FastSpace -/// is reduced from the absolute amount of free space available by at least a factor of FSCB_FILL_COEFFICIENT. -/// * SpaceUpdate - encrypted patches to the FastSpace table. The FastSpace table is "heavyweight", and would -/// be too expensive to update on every page allocation, so SpaceUpdate is used to patch the FastSpace table. -/// -/// A `Path` like the following is deconstructed as follows by the PDDB: -/// -/// ```Text -/// Dictionary -/// | Key -/// | | -/// --+- --------+--------------------- -/// logs:matrix/alice/oct30_2021/bob.txt -/// ``` -/// -/// It could equally have an arbitrary name like `logs:Matrix - alice to bob Oct 30 2021`; -/// as long as the string that identifies a Key is unique, it's stored in the database -/// all the same. Any valid utf-8 unicode characters are acceptable. -/// -/// Likewise, something like this: -/// `settings:wifi/Kosagi.json` -/// Would be deconstructed into the "settings" dictionary with a key of `wifi/Kosagi.json`. -/// -/// ## Code Organization: -/// Accurate as of Nov 2021. May be subject to charge, ymmv. -/// -/// ### `frontend.rs` -/// Defines a set of modules that plug the PDDB into applications (in particular, they -/// attemp to provide a Rust-compatible `read`/`write`/`open` abstraction layer, -/// torturing the notion of `Prefix`, `Path` and `File` in Rust to fit the basis/dict/key -/// format of the PDDB). -/// -/// This is the `lib`-facing set of operations. -/// -/// ### `backend.rs` -/// Defines a set of modules that implement the PDDB itself. This is the hardware-facing set -/// of operations. -/// -/// #### `basis.rs` -/// The set of known `Basis` are tracked in the `BasisCache` structure. This is the "entry point" -/// for most operations on the PDDB, and thus most externally-visible API calls will be revealed -/// on that structure; in fact many calls on that object are just pass-through of lower level calls. -/// -/// A `BasisCach`e consists of one or more `BasisCacheEntries`. This structure manages one or more -/// `DictCacheEntry` within a `BasisCacheEntry`'s dictionary cache. -/// -/// #### `dictionary.rs` -/// The `DictCacheEntry` is defined in this file, along with the on-disk `Dictionary` -/// storage stucture. Most of the methods on `DictCacheEntry` are concerned with managing -/// keys contained within the dictionary cache. -/// -/// #### `keys.rs` -/// The `KeyCacheEntry` is defined in this file. It's a bookkeeping record for the -/// `DictCacheEntry` structure. Keys themselves are split into metadata and data -/// records. The `KeyDescriptor` is the on-disk format for key metadata. The key -/// storage is either described in `KeySmallPool` for keys less than one `VPAGE_SIZE` -/// (just shy of 4kiB), or written directly to disk as fully allocated blocks of -/// `VPAGE_SIZE` for keys larger than one `VPAGE_SIZE` unit. -/// -/// ## Threat model: -/// The user is forced to divulge "all the Basis passwords" on the device, through -/// coercion, blackmail, subpoena, customs checkpoint etc. The adversary has physical -/// access to the device, and is able to take a static disk image; they may even have -/// the opportunity to take several disk images over time and diff the images. -/// The adversary may be able to decrypt the root key of the cryptographic enclave. -/// The adversary may also be able to observe the contents of encrypted data within the -/// "system" basis, which includes some of the bookkeeping information for the PDDB, -/// as the user will have at least been forced to divulge the system basis password as -/// this is a password that every device requires and they cannot deny its existence. -/// -/// Under these conditions, it should be impossible for the adversary to conclusively prove or -/// disprove that every Basis password has been presented and unlocked for inspection -/// through forensic analysis of the PDDB alone (significantly, we cannot prevent disclosure -/// by poorly constructed end-user applications storing things like "last opened Basis" -/// convenience lists, or if the user themselves wrote a note referring to a secret -/// Basis in a less secret area). Furthermore, if a device is permanently seized by -/// the adversary for extensive analysis, any Basis whose password that has not been -/// voluntarily disclosed should be "as good as deleted". -/// -/// The PDDB also cannot protect against key loggers or surveillance cameras recording -/// key strokes as the user operates the device. Resistance to key loggers is instead -/// a problem handled by the OS, and it is up to the user to not type secret passwords -/// in areas that may be under camera surveillance. -/// -/// ## Auditor Notices: -/// There's probably a lot of things the PDDB does wrong. Here's a list of some things -/// to worry about: -/// - The device RootKeys shares one salt across all of its keys, instead of a per-key salt. -/// Currently there are only two keys in the RootKey store sharing the one salt. The concern -/// is that a migration to an ASIC base design would make eFuse bits scarce, so, the initial -/// draft of the RootKeys shares one salt to maximize key capacity. The per-key salt is slightly -/// modified by adding the key index to the salt. This isn't meant to be a robust mitigation: -/// it just prevents a naive rainbow attack from re-using its table. -/// - The bcrypt() implementation is vendored in from a Rust bcrypt crate. It hasn't been audited. -/// - The COST of 7 for bcrypt is relatively low by today's standards (should be 10). However, -/// we can't raise the cost to 10 because our CPU is slower than most modern x86 devices. There is -/// an open issue to try to improve this with hardware acceleration. The mitigation is to use -/// a longer passphrase instead of a 12 or 14-character password. -/// - The RootKey is used to decrypt a locally stored System Basis key. The key is encrypted using -/// straight AES-256 with no authentication. -/// - Secret basis keys are not stored anywhere on the device. They are all derived from a password -/// using bcrypt. The salt for the password is drawn from a "salt pool", whose index is derived from -/// a weak hash of the password itself. This means there is a chance that a salt gets re-used. However, -/// we do not store per-password salts because the existence of the salt would betray the existence of -/// a password. -/// - Disk blocks are encrypted & authenticated on a page-by-page basis using AES-GCM-SIV, with a 256-bit key. -/// - Page table entries and space update entries are salted & hinted using a 64-bit nonce + weak checksum. -/// There is a fairly high chance of a checksum collision, thus PTE decrypts are regarded as advisory and -/// not final; the PTE is not accepted as authentic until the AES-GCM-SIV behind it checks out. Free space -/// entries have no such protection, which means there is a slight chance of data loss due to a checksum -/// collision on the free space entries. -/// -/// ## General Operation: -/// The initial Basis is known as the "System" Basis. It's a low-security framework basis -/// onto which all other Basis overlay. The initial set of Dictionaries, along with their -/// Key/Value pairs, are available to any and all processes. -/// -/// Each new Basis opened will contain zero or more dictionaries. If the dictionary within -/// the new Basis has the same name as an existing dictionary, the Keys are searched in -/// the reverse order of opening. In other words, a new Basis can temporarily override or -/// mask existing Keys. When updating a Key, one may specify the following modes of -/// operation: -/// -/// - UpdateLatest: updates only the copy in the latest Basis to be opened -/// - UpdateOpened: updates copies in other currently opened Basis -/// -/// Note that `UpdateOpened` is dangerous in the sense that if you have a Basis that is -/// is currently Closed, it cannot update copies in the closed Basis. Thus any global -/// update of database schema requires a user to open any and all knows Basis so that -/// synchronization can be maintained. -/// -/// Furthermore, it is possible for a Basis to be closed on a "File" that is currently open. -/// In this case, two things happen: -/// - If a notification callback is registered, it's pinged by the PDDB. The notification -/// of closure callback is an additional feature to the typical Rust File interface. -/// - If the client attempts to read or write to any keys that span a Basis modification, -/// the now-ambiguous key operation will return a `BrokenPipe` error to the caller. -/// -/// ## General flash->key structure -/// -/// The basic unit of memory is a page (4k). Keys shorter than 4k will be packed into a single -/// page, but no allocation will ever be smaller than 4k due to the erase block size of the FLASH. -/// -/// Because the flash is always encrypted, there is also no such thing as "incremental writing of data" -/// to a page, as the presence of free space (all 1's regions) are considered a side-channel. Thus, -/// the PDDB relies heavily on caching data structures in RAM, with periodic commits of encrypted data -/// back to the flash. This means it is also very easy to lose data when the system has a panic and -/// reboots ungracefully. Thus, the structure of data at rest is journaled, so that we have some -/// chance of recovering an older snapshot of the data if there is an ungraceful power-down event. -/// -/// I believe this relative unreliability compared to more traditional storage is just a fact of life for -/// an encrypted, plausibly deniable filesystem, as you have to trade-off the side channel of partially -/// erased blocks versus the finite write lifetime of flash storage. -/// -/// When a Basis is created, it starts with 64k of space allocated to it. Most of this is blank (but encrypted -/// in flash), but it is important in a PD filesystem to claim some freespace for day-to-day operations. This -/// is because when you need to allocate more data, you have to make a guess as to what is free, and what -/// previously allocated but currently denied to exist (locked Basis look identical to free space when locked). -/// -/// Each Dictionary within a Basis starts with a 4k allocation as well, although most of it is free space. -/// Keys are then populated into dictionaries, with additional pages allocated to dictionaries as the keys -/// grow. -/// -/// Blocks on disk are mapped into a virtual PDDB space via a page table. The virtual PDDB space is 64-bit. -/// Each Basis gets its own 48-bit address space, allowing for up to 64k Basis. -/// The PDDB page table guarantees that no matter the order of allocation in FLASH, a Basis can linearly -/// access its blocks through a page table lookup. A reverse-lookup structure is constructed on boot -/// by scanning the entire PDDB, and creating a HashMap of (basis, offset) -> pddb_addr tuples. -/// -/// Physical addresses can be 32 or 64 bit based upon the specific implementation. The physical address -/// type is defined in types.rs. For Precursor, it's defined as a u32, which limits the physical address -/// size to 32 bits. This is important because Precursor operates out of a very small amount of RAM, -/// and doubling the size of the overhead bookkeeping structures to handle disk sizes that are well -/// beyond the current implementation has a real impact on the system's free memory footprint. However, -/// the PDDB is coded in a way such that one "should" be able to swap out the u32 for a u64 in the -/// "newtype" definition for a PhysAddr, and the system will work. -/// -/// Page table entries don't define themselves with a Basis -- every time a Basis is opened, the entire -/// table must be scanned, brute-force decrypting every entry against the Basis key, and seeing if the -/// checksum in the table entry matches the address. If there's a match, then that entry is recorded as -/// belonging to a given Basis. -/// -/// When a system boots, it starts with two default Basis: FastSpace, and System. These Basis are protected -/// with the "device unlock" key. System is a set of low-security data that are used -/// to configure "everyday details". FastSpace Basis is a special-case Basis that is not structured as -/// a Key/Value store. Instead, it tracks a small amount of pre-allocated "free" space, that can be -/// pulled from to grow Dictionaries without requesting that the user unlock every known Basis in the system -/// to ensure none of the valuable data is overwritten. -/// -/// The .FastSpace basis should never be equal to the total amount of free space in the system, as it -/// would betray the actual amount of data available and destroy plausible deniability. The system can -/// operate with .FastSpace set to the nil set, but it would also operate without confidentiality of hidden -/// Basis, because in order to allocate new blocks it must prompt the user to enter all known Basis passwords, -/// to avoid accidentally overwriting data that should be retained (as locked Basis would appear as free space -/// and risk being overwritten). However, in order to accelerate bookkeeping, the .FastSpace basis operates -/// with an encrypted record that rotates through a set of "clean" pages (that is, pages that have been set -/// to all 1's) in a circular buffer basis. The state of where the .FastSpace record is in the "clean" pages -/// disclose nothing other than the fact that the system has been used. -/// -/// ## Basis Deniability -/// -/// The existence of a Basis itself must be confidential; if we stored a list of encrypted Basis passwords -/// somewhere, a rubber-hose attacker would simply need to count the number of encrypted entries and commence -/// the beatings until the number of passwords that have been disclosed under duress match the number of -/// encrypted password entries. -/// -/// Herein lies a challenge. Standard password storage techniques demand that each password be stored -/// with a unique salt. The existence of this salt betrays that a password must, in essence, exist. -/// There are at least two methods to counter this: -/// -/// 1. Dummy salt entries. In this approach, each system is initialized with a random set of additional, dummy -/// salt/encrypted password combos. In the case of a rubber-hose attack, the attacker cannot know precisely -/// when to stop the beatings, as the few remaining entries could be dummies with absolutely no meaning. -/// 2. A single salt entry. In this approach, a single shared salt is used by /all/ passwords, and there is no -/// encrypted password list stored; instead, the the password as presented is directly used to decrypt -/// the Basis page table and if no valid entries are found we may conclude that the password provided has a typo. -/// -/// The advantage of (1) is that the usage of salt falls precisely within the traditional cryptographic -/// specification of bcrypt(). The disadvantages of (1) include: one still has to have a method to match -/// a presented password to a given salt entry. In a traditional user/pass login database, the username is -/// the plaintext key to match the salt. In this case, we would use a nickname used to refer to each Basis -/// to correlate to its salt entry. Thus, we'd have to come up with "garbage" plaintext nicknames -/// for a Basis that are not trivial for a rubber-hose attacker to dismiss as chaffe. I think this is Hard. -/// The alternative is to "brute force" the entire list of salts, guessing each one in sequence until one -/// is found to decrypt entries in the Basis page table. The problem with this is that bcrypt is designed to -/// be computationally slow, even for valid passwords. The complexity parameter is chosen so that it takes -/// about 1 second per iteration through the password function. Note that a faster CPU does not solve this -/// problem: if you CPU gets faster, you should increase your complexity, so one is always suffering the 1 -/// second penalty to try a password. This means brute forcing a list of salts necessarily incurs at -/// least a 1 second penalty per entry; thus each dummy basis adds a 1 second minimum overhead to unlocking -/// a new Basis database. This puts a downward pressure on the number of dummies to be included; however, the -/// number of dummies needs to be at least as large as the number of Basis we wish to plausibly deny. This -/// creates a negative incentive to using plausible deniability. -/// -/// The advantage of (2) is that when a user presents a password, we only need to run the bcrypt routine -/// once, and then we can immediately start checking the Basis page table for valid entries. The disadvantage -/// of (2) is that we are re-using a salt across all of a given user's passwords. Note that the salt itself -/// is from a TRNG, so between user devices, the salt performs its role. However, it means that an attacker -/// can generate a single rainbow table specific to a given user to reverse all of their passwords; and furthermore, -/// re-used passwords are trivially discoverable with the rainbow table. -/// -/// Approach (1) is probably what most crypto purists would adopt: you, dear user, /should/ understand that -/// cryptography is worth the wait. However, a second principle states that "users always pick convenience over -/// security". A 15-second wait is an eternity in the UX world, and would act as an effective deterrent to any -/// user ever using the PD function because it is too slow. Thus for v1 of the PDDB, we're going to try approach -/// (2), where a common salt is used across all the PDDB passwords, but the salt is /not/ re-used between devices. -/// This choice diminishes the overall security of the system in the case that a user chooses weak passwords, or re-uses -/// passwords, but in exchange for a great improvement in responsivity of the implementation. -/// -/// The final implementation uses a slight mod on (2), where the 128-bit common salt stored on disk is XOR'd -/// with the user-provided "basis name". Users are of course allowed to pick crappy names for their basis, and -/// re-use the names, but hopefully this adds a modicum of robustness against rainbow table attacks. -/// -/// -/// ## Basis Unlock Procedure -/// -/// Each Basis has a name and a passcode associated with it. The default Basis name is `.System`. -/// In addition to that, a `.FastSpace` structure is unlocked along side the default Basis. -/// These are both associated with the default system unlock passcode. -/// -/// A newly created Basis will request a name for the Basis, and a password. It is a requirement -/// that the combination of `(name, password)` be unique; this property is enforced and a system will -/// reject attempts to create identically named, identially passworded Basis. However, one can have -/// same-named Basis that have a different password, or diffently-named Basis with the same password -/// (this is generally not recommended, but it's not prohibited). -/// -/// The `name` field is XOR'd with the device-local `salt` field to form the salt for the password, -/// and the plaintext of the password itself is used as the password which is fed into the bcrypt() -/// algorithm to generate a 192-bit encrypted password, which is expanded to 256-bits using SHA-512/256, -/// and then used as the AES key for the given `(name, password)` Basis. -/// -/// -/// ## Journaling -/// -/// A major issue with the implementation is that even a small change in a data structure turns into -/// the mutation of at least an entire sector of data, with a sector being 4k. The reason is that -/// we can't take advantage of FLASH memory's property where 1's (erased) can be set to 0's on a byte-by-byte -/// basis: in order to mask free space signatures, the entire storage area is filled with random bytes. -/// Furthermore, any update to a part of data within a block should necessarily propagate to the entire -/// block changing, in order to avoid attacks where observations on which portion of ciphertext has changed -/// leading to a definitive conclusion about the state of the database records. Therefore, a large portion -/// of the database data will have to persist in RAM, and be updated only at regular, but widely-spaced-out -/// (to avoid FLASH wear-out), intervals. -/// -/// In the case that power is lost during an update, the system uses a 32-bit `journal_rev` number at the -/// top of every major database structure. If two competing copies of data are found, the one with the -/// highest `journal_rev` number wins. It may be possible to overflow the 32-bit number, so at some point a -/// garbage collection step needs to be made where any errant, lower journal rev blocks are overwritten -/// with random data; all the journal revs are confirmed to be the same; and then all set to 0 again. Power -/// should not be lost during this process; but it should be rare, and likely never needed. -/// -/// When writing to disk, a write-then-erase method is used: -/// 1. Required blocks for the update are taken out of the .FastSpace pool -/// 2. Detached data sections of structural records are written first (only database structural records can have detached data; -/// all stored data have individual journal revision fields associated with them), and into blocks -/// taken out of the .FastSpace pool. -/// 3. The head of the updated structural record is written, with the new latest journal rev noted. -/// 4. The page table is updated using the procedure outlined below. -/// 5. The old blocks are erased and overwritten with random data. -/// 6. The .FastSpace pool is updated with the newly freed blocks -/// -/// When updating the page table, it's important not to lose an entire page's worth of entries in case -/// power is lost when updating it. The following process is used to update a page table entry: -/// 0. At the end of the page table there is a circular buffer of blank pages (they are truly blank, -/// but this will not leak metadata about the contents of the PDDB, as it only does bookkeeping -/// on page table updates as a whole). This is known as the make-before-break buffer (mbbb). -/// 1. The previous mbbb entry is erased, if there is any, and the next entry is noted. -/// 2. The new page table entry is written into the mbbb -/// 3. The old page table entry in the page table itself is erased. -/// 4. The old page table entry is populated with the updated page table records. -/// -/// Detecting and Repairing a broken page table: -/// The special case of long run's of 1's (more than 32 bits in a row) is checked during the page table scan. -/// If a long run of 1's is detected, then a suspected corrupted page table update is flagged, and for that page the data -/// is pulled from the mbbb record. -/// -/// ## Precursor's Implementation-Specific Flash Memory Organization: -/// -/// ```Text -/// offset from | -/// pt_phys_base| contents (example for total PDDB len of 0x6f8_0000 or 111 MiB) -/// ------------|--------------------------------------------------- -/// 0x0000_0000 | virtual map for page at (0x0000 + data_phys_base) -/// 0x0000_0010 | virtual map for page at (0x1000 + data_phys_base) -/// 0x0000_0020 | virtual map for page at (0x2000 + data_phys_base) -/// ... -/// 0x0006_F7F0 | virtual map for page at (0x06F7_F000 + data_phys_base) -/// 0x0006_F800 | unused -/// ... -/// 0x0007_0000 | key page -/// ... -/// 0x0007_1000 | mbbb start (example of 10 pages) -/// ... -/// 0x0007_B000 | fscb start (example of 10 pages) -/// ... -/// 0x0008_5000 | data_phys_base - start of basis + dictionary + key data region -/// ``` - -// historical note: 389 hours, 11 mins elapsed since the start of the PDDB coding, and the first attempt at a hardware test -- as evidenced by the uptime of the hardware validation unit. - pub mod api; pub use api::*; pub mod frontend;