From 5a12e83ff2df42da7811acb06a6db8e01fc79992 Mon Sep 17 00:00:00 2001 From: bunnie Date: Sun, 28 Nov 2021 20:42:26 +0800 Subject: [PATCH] add first cut at basis key derivation function --- Cargo.lock | 3 ++ services/pddb/Cargo.toml | 8 +++ services/pddb/src/api.rs | 5 ++ services/pddb/src/backend.rs | 3 +- services/pddb/src/backend/bcrypt.rs | 76 +++++++++++++++++++++++++++++ services/pddb/src/backend/hw.rs | 76 ++++++++++++++++++++++++++--- services/pddb/src/main.rs | 3 +- 7 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 services/pddb/src/backend/bcrypt.rs diff --git a/Cargo.lock b/Cargo.lock index bad9eb01f..945f594e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1430,7 +1430,9 @@ dependencies = [ "aes-gcm-siv", "bitfield", "bitflags", + "blowfish", "cipher", + "digest", "llio", "log", "log-server", @@ -1440,6 +1442,7 @@ dependencies = [ "rand_core 0.5.1", "rkyv", "root-keys", + "sha2", "spinor", "susres", "ticktimer-server", diff --git a/services/pddb/Cargo.toml b/services/pddb/Cargo.toml index 499541508..38eddf25a 100644 --- a/services/pddb/Cargo.toml +++ b/services/pddb/Cargo.toml @@ -27,6 +27,14 @@ bitfield = "0.13.2" aes-gcm-siv = "0.10.3" llio = {path="../llio"} +# passwords +sha2 = {path = "../engine-sha512"} +digest = "0.9" + +# bcrypt +blowfish = { version = "0.8", features = ["bcrypt"] } + + [target.'cfg(not(any(windows,unix)))'.dependencies] utralib = { path = "../../utralib"} diff --git a/services/pddb/src/api.rs b/services/pddb/src/api.rs index 80646609f..5084cab32 100644 --- a/services/pddb/src/api.rs +++ b/services/pddb/src/api.rs @@ -38,6 +38,11 @@ pub(crate) const PDDB_DEFAULT_SYSTEM_BASIS: &'static str = ".System"; #[allow(dead_code)] pub(crate) const PDDB_FAST_SPACE_SYSTEM_BASIS: &'static str = ".FastSpace"; +#[allow(dead_code)] +// TODO: add hardware acceleration for BCRYPT so we can hit the OWASP target without excessive UX delay +pub(crate) const BCRYPT_COST: u32 = 7; // 10 is the minimum recommended by OWASP; takes 5696 ms to verify @ 10 rounds; 804 ms to verify 7 rounds + + #[derive(num_derive::FromPrimitive, num_derive::ToPrimitive, Debug)] pub(crate) enum Opcode { KeyRequest, diff --git a/services/pddb/src/backend.rs b/services/pddb/src/backend.rs index 12b851e9b..8efc56667 100644 --- a/services/pddb/src/backend.rs +++ b/services/pddb/src/backend.rs @@ -10,7 +10,8 @@ mod fastspace; pub use fastspace::*; mod types; pub use types::*; - +mod bcrypt; +pub use bcrypt::*; // local to the backend mod murmur3; diff --git a/services/pddb/src/backend/bcrypt.rs b/services/pddb/src/backend/bcrypt.rs new file mode 100644 index 000000000..10c7266b5 --- /dev/null +++ b/services/pddb/src/backend/bcrypt.rs @@ -0,0 +1,76 @@ + +// this code is based on https://github.com/Keats/rust-bcrypt +// the original crate is not used directly because the base64 wrappers cause the crate +// to be incompatible with a no-std environment. We are storing binary data directly +// in our use case. + +// Cost constants +#[allow(dead_code)] +const MIN_COST: u32 = 4; +#[allow(dead_code)] +const MAX_COST: u32 = 31; + +use blowfish::Blowfish; + +fn setup(cost: u32, salt: &[u8], key: &[u8]) -> Blowfish { + assert!(cost < 32); + let mut state = Blowfish::bc_init_state(); + + state.salted_expand_key(salt, key); + for _ in 0..1u32 << cost { + state.bc_expand_key(key); + state.bc_expand_key(salt); + } + + state +} + +pub fn bcrypt(cost: u32, salt: &[u8], pw: &str, output: &mut [u8]) { + assert!(salt.len() == 16); + assert!(output.len() == 24); + + let pw_len = if pw.len() > 72 { + log::warn!("password of length {} is truncated to 72 bytes [reason: bcrypt limitation]", pw.len()); + 72 + } else { + pw.len() + 1 + }; + let mut plaintext_copy: [u8; 73] = [0; 73]; + for (src, dst) in pw.bytes().zip(plaintext_copy.iter_mut()) { + *dst = src; + } + plaintext_copy[72] = 0; // always null terminate + + // this function takes the plaintext key and uses it to prime a ~4k region of stack with an s-box + // that's used for the round function. The upstream Rust crypto crate does not wipe the sbox after use. + // however, it seems non-trivial to reverse the original password from the s-boxes. + let state = setup(cost, salt, &plaintext_copy[..pw_len]); + + // erase the plaintext copy as soon as we're done with it + // an unsafe method is used because the compiler will correctly reason that plaintext_copy goes out of scope + // and these writes are never read, and therefore they may be optimized out. + let pt_ptr = plaintext_copy.as_mut_ptr(); + for i in 0..plaintext_copy.len() { + unsafe{pt_ptr.add(i).write_volatile(core::mem::zeroed());} + } + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + + // OrpheanBeholderScryDoubt + #[allow(clippy::unreadable_literal)] + let mut ctext = [ + 0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274, + ]; + for i in 0..3 { + let i: usize = i * 2; + for _ in 0..64 { + let (l, r) = state.bc_encrypt(ctext[i], ctext[i + 1]); + ctext[i] = l; + ctext[i + 1] = r; + } + + let buf = ctext[i].to_be_bytes(); + output[i * 4..][..4].copy_from_slice(&buf); + let buf = ctext[i + 1].to_be_bytes(); + output[(i + 1) * 4..][..4].copy_from_slice(&buf); + } +} diff --git a/services/pddb/src/backend/hw.rs b/services/pddb/src/backend/hw.rs index 31ddbcd24..0d96fab31 100644 --- a/services/pddb/src/backend/hw.rs +++ b/services/pddb/src/backend/hw.rs @@ -16,7 +16,6 @@ use std::cmp::Reverse; use std::io::{Result, Error, ErrorKind}; /// Implementation-specific PDDB structures: for Precursor/Xous OS pair - pub(crate) const MBBB_PAGES: usize = 10; pub(crate) const FSCB_PAGES: usize = 16; @@ -30,9 +29,7 @@ pub(crate) struct StaticCryptoData { /// aes-256 key of the system basis, encrypted with the User0 root key pub(crate) system_key: [u8; AES_KEYSIZE], /// a pool of fixed data used to pick salts, based on a hash of the basis name - pub(crate) salt_base: [u8; 2048], - /// also random data, but no specific purpose - pub(crate) reserved: [u8; 2016], + pub(crate) salt_base: [u8; 4064], } impl Deref for StaticCryptoData { type Target = [u8]; @@ -1175,15 +1172,13 @@ impl PddbOs { } let mut crypto_keys = StaticCryptoData { system_key: [0; AES_KEYSIZE], - salt_base: [0; 2048], - reserved: [0; 2016], + salt_base: [0; 4064], }; // copy the encrypted key into the data structure for commit to Flash for (&src, dst) in basis_key_aes.iter().zip(crypto_keys.system_key.iter_mut()) { *dst = src; } self.entropy.borrow_mut().get_slice(&mut crypto_keys.salt_base); - self.entropy.borrow_mut().get_slice(&mut crypto_keys.reserved); self.patch_keys(crypto_keys.deref(), 0); // now we have a copy of the AES key necessary to encrypt the default System basis that we created in step 2. @@ -1342,6 +1337,73 @@ impl PddbOs { // for now, do nothing -- just indicate success with a returned empty set Some(Vec::<([u8; AES_KEYSIZE], String)>::new()) } + + /// Derives a 256-bit AES encryption key for a basis given a basis name and its password. + /// You will also need to derive the AAD for the basis using the basis_name. + pub(crate) fn basis_derive_key(&self, basis_name: &str, password: &str) -> [u8; AES_KEYSIZE] { + use sha2::{FallbackStrategy, Sha512, Sha512Trunc256}; + use digest::Digest; + use backend::bcrypt::*; + + // 1. derive the salt from the "key" region. First step is to create the salt lookup + // table, which is done by hashing the name and password together with SHA-512 + // manage the allocation of the data for the basis & password explicitly so that we may wipe them later + let mut bname_copy = [0u8; BASIS_NAME_LEN]; + for (src, dst) in basis_name.bytes().zip(bname_copy.iter_mut()) { + *dst = src; + } + let mut plaintext_pw: [u8; 73] = [0; 73]; + for (src, dst) in password.bytes().zip(plaintext_pw.iter_mut()) { + *dst = src; + } + plaintext_pw[72] = 0; // always null terminate + + let mut hasher = Sha512::new_with_strategy(FallbackStrategy::SoftwareOnly); + hasher.update(&bname_copy); + hasher.update(&plaintext_pw); + let digest = hasher.finalize(); + // 2. now derive the actual salt by using indices from the digest to progressively + // index into the salt table. + let scd = self.static_crypto_data_get(); + let mut salt = [0u8; 16]; + for (index_bytes, dst) in digest.chunks(2).zip(salt.iter_mut()) { + let index = u16::from_le_bytes(index_bytes.try_into().unwrap()); + *dst = scd.salt_base[index as usize % scd.salt_base.len()]; + log::info!("index: {}, dst: {}", index as usize % scd.salt_base.len(), *dst); + } + log::info!("derived salt: {:x?}", salt); + + // 3. use the salt + password and run bcrypt on it to derive a key. + let mut hashed_password: [u8; 24] = [0; 24]; + let start_time = self.timestamp_now(); + bcrypt(BCRYPT_COST, &salt, password, &mut hashed_password); // note: this internally makes a copy of the password, and destroys it + let elapsed = self.timestamp_now() - start_time; + log::info!("derived bcrypt password in {}ms", elapsed); + + // 4. take the resulting 24-byte password and expand it to 32 bytes using sha512trunc256 + let mut expander = Sha512Trunc256::new_with_strategy(FallbackStrategy::SoftwareOnly); + expander.update(hashed_password); + let final_key = expander.finalize(); + let mut key = [0u8; AES_KEYSIZE]; + for (&src, dst) in final_key.iter().zip(key.iter_mut()) { + *dst = src; + } + + // 5. erase extra plaintext copies made of the basis name and password using a routine that + // shouldn't be optimized out or re-ordered + let bn_ptr = bname_copy.as_mut_ptr(); + for i in 0..bname_copy.len() { + unsafe{bn_ptr.add(i).write_volatile(core::mem::zeroed());} + } + let pt_ptr = plaintext_pw.as_mut_ptr(); + for i in 0..plaintext_pw.len() { + unsafe{pt_ptr.add(i).write_volatile(core::mem::zeroed());} + } + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + + //6. return the key + key + } } pub(crate) fn cstr_to_string(cstr: &[u8]) -> String { diff --git a/services/pddb/src/main.rs b/services/pddb/src/main.rs index f1f7c6937..2da633dda 100644 --- a/services/pddb/src/main.rs +++ b/services/pddb/src/main.rs @@ -517,7 +517,8 @@ fn xmain() -> ! { - [done] in-place update consistency: general integrity then patch all keys with a new test pattern - [done] extend update consistency: general integrity then patch all keys with a longer test pattern - [done] key deletion torture test: delete every other key in a dictionary, then regenerate some of them with new data. - - fast space exhaustion test: allocate and delete a bunch of stuff. trigger a fast-space regenerate. + - [done] fast space exhaustion test: allocate and delete a bunch of stuff. trigger a fast-space regenerate. + note: for faster stress-testing, we dialed the FSCB_PAGES to 4 and the FASTSPACE_PAGES to 1. - basis search: create basis A, populate with general integrity. create basis B, add test entries. hide basis B, confirm original A; mount basis B, confirm B overlay. */