Back again, this time because the local database stuff is pretty much ready to go, and then the rest of the logic mostly happens in memory (and making sure that everything that doesn’t need to be in memory is out of memory as fast as reasonably possible. balance performance with security when it comes to how long certain pieces of information are allowed to live in memory).

Okay, so keep in mind that a lot of this is WIP - even if I speak very highly of my WIP work - so it’s not gonna be perfect, but this is generally all the local database access that will need to be done. You can see that we have created a struct called KvStore which holds only a set and get method (the reason for this odd pattern and odd initialization pattern is that we need to make this usable with JavaScript callbacks, and as a recovering JavaScript developer, I couldn’t help but think ahead).

So, all we need are keys, and all we need to do is decrypt the values. What I hope can be inferred from the code (and what I am seeing about my code is accurate), is that this is actually very secure and given the “batched” range of messages that a given key is useful for, this can actually scale because content can be decrypted incrementally.

The only gap I have currently left to close (and I’m just thinking of this now so it’s possible I’ll come back and update this post in the next few hours) is that the time range that a given public key (used to create shared secrets with the one-time private key of the sender) is valid for, or was likely to have been used in. That way a time range and a key (or set of keys) can be provided so that a user can get all their messages for their public keys for that time range.

Maybe the list needs to be its own message type that specifies a set of “pairs” for the time ranges and a list of keys for those time ranges? Or Should we just say that you only can do one time range per query? (I think it’s that last one, sorry the weird part of my brain that thought that might be useful for a sec).

this is just about the encrypted storage. a separate (not necessary to encrypt) table needs to be created for public key -> time-range relationships

Okay, so here is the - without (time-range + public key) management - this is what we have for “Random Public Key Access” (and also private signature access):

use parking_lot::Mutex;
use std::sync::Arc;

use once_cell::sync::OnceCell;

pub static KV_STORE: OnceCell<KvStore> = OnceCell::new();

pub static DB_ENCRYPTION_KEY_ADDRESS: &'static str = "db-encryption-key";
pub static SIGNING_KEY_ADDRESS: &'static str = "signing-key";

pub struct KvStore {
    pub set_cb: fn(String, Vec<u8>),
    pub get_cb: fn(String, Box<dyn Fn(Vec<u8>)>),
}

impl KvStore {
    pub fn set(&self, key: String, val: Vec<u8>) {
        (self.set_cb)(key, val)
    }

    pub fn get(&self, key: String, cb: Box<dyn Fn(Vec<u8>)>) {
        (self.get_cb)(key, cb)
    }
}

fn get_kv_store() -> &'static KvStore {
    KV_STORE.get().unwrap()
}

pub fn get_priv_signing_key(passcode: [u8; 32]) -> [u8; 32] {
    let kv_store = get_kv_store();

    let out_key: Arc<Mutex<[u8; 32]>> = Arc::new(Mutex::new([0; 32]));

    let out_key_clone = Arc::clone(&out_key);

    kv_store.get(
        SIGNING_KEY_ADDRESS.to_string(),
        Box::new(move |encrypted_signing_key| {
            kv_store.get(
                DB_ENCRYPTION_KEY_ADDRESS.to_string(),
                Box::new({
                    let out_key_clone = Arc::clone(&out_key_clone);

                    move |encrypted_db_encryption_key| {
                        let db_encryption_key =
                            cryptography::block_decrypt(passcode, encrypted_db_encryption_key);

                        let db_encryption_key_as_bytes: &[u8] = &db_encryption_key;
                        let db_encryption_key_as_fixed_bytes: [u8; 32] = db_encryption_key_as_bytes
                            .try_into()
                            .expect("failed to convert db encryption key to fixed bytes");

                        let signing_key = cryptography::block_decrypt(
                            db_encryption_key_as_fixed_bytes,
                            encrypted_signing_key.clone(),
                        );

                        let signing_key_as_bytes: &[u8] = &signing_key;
                        let signing_key_as_fixed_bytes: [u8; 32] = signing_key_as_bytes
                            .try_into()
                            .expect("failed to convert priv exchange key to fixed bytes");

                        let mut out_key_lock = out_key_clone.lock();
                        *out_key_lock = signing_key_as_fixed_bytes;
                    }
                }),
            );
        }),
    );

    let out_key = *out_key.lock();
    out_key
}

pub fn write_key_pair(
    pub_key: [u8; 32],
    priv_key: [u8; 32],
    passcode: [u8; 32],
) -> Result<(), String> {
    let kv_store = get_kv_store();

    kv_store.get(
        DB_ENCRYPTION_KEY_ADDRESS.to_string(),
        Box::new(move |encrypted_db_encryption_key| {
            let db_encryption_key =
                cryptography::block_decrypt(passcode, encrypted_db_encryption_key);

            let db_encryption_key_as_bytes: &[u8] = &db_encryption_key;
            let db_encryption_key_as_fixed_bytes: [u8; 32] = db_encryption_key_as_bytes
                .try_into()
                .expect("failed to convert db encryption key to fixed bytes");

            let encrypted_priv_key =
                cryptography::block_encrypt(db_encryption_key_as_fixed_bytes, priv_key.to_vec());

            kv_store.set(hex::encode(pub_key), encrypted_priv_key);
        }),
    );

    Ok(())
}

pub fn get_priv_key_from_pub_key(pub_key: [u8; 32], passcode: [u8; 32]) -> [u8; 32] {
    let kv_store = get_kv_store();

    let out_key: Arc<Mutex<[u8; 32]>> = Arc::new(Mutex::new([0; 32]));

    let out_key_clone = Arc::clone(&out_key);

    kv_store.get(
        hex::encode(pub_key),
        Box::new(move |encrypted_priv_key| {
            kv_store.get(
                DB_ENCRYPTION_KEY_ADDRESS.to_string(),
                Box::new({
                    let out_key_clone = Arc::clone(&out_key_clone);

                    move |encrypted_db_encryption_key| {
                        let db_encryption_key =
                            cryptography::block_decrypt(passcode, encrypted_db_encryption_key);

                        let db_encryption_key_as_bytes: &[u8] = &db_encryption_key;
                        let db_encryption_key_as_fixed_bytes: [u8; 32] = db_encryption_key_as_bytes
                            .try_into()
                            .expect("failed to convert db encryption key to fixed bytes");

                        let priv_key = cryptography::block_decrypt(
                            db_encryption_key_as_fixed_bytes,
                            encrypted_priv_key.clone(),
                        );

                        let priv_key_as_bytes: &[u8] = &priv_key;
                        let priv_key_as_fixed_bytes: [u8; 32] = priv_key_as_bytes
                            .try_into()
                            .expect("failed to convert priv exchange key to fixed bytes");

                        let mut out_key_lock = out_key_clone.lock();
                        *out_key_lock = priv_key_as_fixed_bytes;
                    }
                }),
            );
        }),
    );

    let out_key = *out_key.lock();
    out_key
}

Conclusion

So that’s kinda how the on-device key encryption works, and that cryptography::block_decryption and cryptography::block_encryption is from the posts earlier about cryptography. It’s just AES256 encryption tho. All private keys are stored in that AES256 encrypted fashion, and the key used for the database is itself AES256 encrypted, where the passcode (or hopefully in the future biometrics or some other kind of key) is its key.

So everything should be pretty secure (famous last words).

Caveats

Don’t forget to set up the time-frame table for your public keys. That’s what you’ll need to hit as a “frecency” cache to grab messages from your pods. You have to grab all the public keys for a time range and send those keys with the time range to grab your content. All of that data can be public and that makes for fast access. We can determine whether or not it would be respectful to the lowest end device user to just store the whole thing in memory (but yo I think that shit’s not that big at all and you can restrict its size while still preserving its usefulness, otherwise it’s just more load to hit the storage… anyway, it’s benchmark-able and for now I just have to make a decision - we’ll see).