add first cut at basis key derivation function

This commit is contained in:
bunnie 2021-11-28 20:42:26 +08:00
parent af9f4cbde3
commit 5a12e83ff2
7 changed files with 165 additions and 9 deletions

3
Cargo.lock generated
View file

@ -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",

View file

@ -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"}

View file

@ -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,

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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.
*/