nstilt1 / crypto-on-the-edge

A WIP Rust crate for generating private keys from IDs based on an HKDF, and eliminates the need to store private keys.
Apache License 2.0
0 stars 0 forks source link

Securely erasing memory #2

Open nstilt1 opened 4 months ago

nstilt1 commented 4 months ago

Currently, there are a few aspects of this crate that can't be easily erased from memory:

  1. temporary values and other stack-based values containing key material when creating private keys may be copied at will by the compiler
  2. hkdf might use some temporary values as well

This library doesn't perform some of the more large operations to be able to sufficiently implement "stack bleaching", such as signing data with private keys. This library merely generates the key, so a library that uses this library might be more suited for stack bleaching.

My question is: is it even worth it to eliminate all temporary variables, or should people try to use stack bleaching until there is a new development with Rust and/or the LLVM that allows for the developer to indicate that the compiler should not make certain stack data persist longer than required?

nstilt1 commented 3 months ago

There is more trouble in this regard in the initialization methods for the key_generator. It is also leaving some memory behind, but besides stack bleaching, I could add some members on the key_generator that contain the MAC key and the HKDF key and RNG seed, but I would also want to add something for the symmetric resource encryption key, which would mean that I would need to add the ResourceEncryptor as a generic parameter for the key_manager

nstilt1 commented 3 months ago

It seems like the only way to do this without stack bleaching would involve having several members of the structs as Option<T>, and perhaps making a separate struct to simplify any sort of .unwrap() and .is_some() shenanigans. I wouldn't mind (much) doing this, but there are still some underlying crates that do not buffer their intermediate outputs... likely for a good reason, because it seems pretty tedious and imperfect. There's a lot of meticulous aspects of this, such as initialization. Calling Box::new(SomeStruct::new(...)) seems to only allocate the struct to the heap once the struct is returned from the new method. So to take that into account, the code might look something like this (kind of nasty):

#[cfg(feature = "boxed")]
type Boxed<T> = Box<T>;
#[cfg(not(feature = "boxed"))]
type Boxed<T> = T;

pub fn new(arr: &[u8]) -> Boxed<Self> {
    let mut s: Boxed<Self> = Boxed::new(Self {
        hkdf: Hkdf::from_prk(arr).expect("should be long enough"),
        hkdf_2: None,
        kdf_key: Default::default(),
    });
    s.hkdf.expand(b"level 2 kdf", &mut s.kdf_key);
    s.hkdf_2 = Some(Hkdf::from_prk(&s.kdf_key).expect("long enough"));
    s
}

Keep in mind that there would likely need to be a trait called Boxed that has a new method, or maybe a macro. Not entirely sure, but this seems like it would be nasty enough to consider stack bleaching instead. I tried to store intermediate values in a previous, unreleased version of this crate, and it got extremely hectic, especially once I had to start dealing with the Option<T> types. I believe I resorted to a helper function that called Option<T>::as_mut_slice()[0], which would fail on a None value, but should return a mutable reference if it was Some<T>.