rustolio-db 0.1.0

An DB extention for the rustolio HTTP-Server
Documentation
//
// SPDX-License-Identifier: MPL-2.0
//
// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//

mod ops;
// mod teams;

use rustolio_rpc::prelude::*;
use rustolio_utils::{
    bytes::{encoding::encode_to_bytes, hex},
    crypto,
    prelude::*,
};
use rustolio_web::prelude::*;

use crate::Value;

// pub use teams::Teams;

const DB_USERNAME: &str = "DB_USERNAME";
const DB_PRIVATE_KEY: &str = "DB_PRIVATE_KEY";
const DB_ENCRYPTION_KEY: &str = "DB_ENCRYPTION_KEY";
const DB_DECAPSULATION_KEY: &str = "DB_DECAPSULATION_KEY";

global! {
    static CLIENT: GlobalSignal<Option<Client>> = GlobalSignal::new(None);
    Effect::new(|| {
        tracing::info!("initializing DB-Client...");
        Client::init().unwrap();
    });
}

#[derive(Clone)]
pub struct Client {
    username: String,
    private_key: crypto::signature::PrivateKey,
    encryption_cipher: crypto::encryption::Cipher,
    decapsulation_key: crypto::encapsulation::DecapsulationKey,
}

impl Client {
    fn init() -> rustolio_utils::Result<()> {
        let storage = storage()?;
        let Some(username) = storage
            .get_item(DB_USERNAME)
            .context("Failed to get item from LocalStorage")?
        else {
            return Ok(());
        };
        let Some(private_key) = storage
            .get_item(DB_PRIVATE_KEY)
            .context("Failed to get item from LocalStorage")?
        else {
            return Ok(());
        };
        let Some(encryption_key) = storage
            .get_item(DB_ENCRYPTION_KEY)
            .context("Failed to get item from LocalStorage")?
        else {
            return Ok(());
        };
        let Some(decapsulation_key) = storage
            .get_item(DB_DECAPSULATION_KEY)
            .context("Failed to get item from LocalStorage")?
        else {
            return Ok(());
        };

        let private_key = hex::decode(&private_key).context("Failed to decode private key")?;
        let encryption_key =
            hex::decode(&encryption_key).context("Failed to decode encryption key")?;
        let decapsulation_key =
            hex::decode(&decapsulation_key).context("Failed to decode decapsulation key")?;

        let private_key = crypto::signature::PrivateKey::from_bytes(private_key)
            .context("Failed to construct key")?;
        let encryption_key = crypto::encryption::Key::from_bytes(&encryption_key)
            .context("Failed to construct key")?;
        let decapsulation_key =
            crypto::encapsulation::DecapsulationKey::from_bytes(decapsulation_key)
                .context("Failed to construct key")?;

        CLIENT.update(|c| {
            *c = Some(Client {
                username,
                private_key,
                encryption_cipher: crypto::encryption::Cipher::new(encryption_key),
                decapsulation_key,
            });
        });

        Ok(())
    }

    pub async fn create(username: &str, password: &str) -> rustolio_utils::Result<()> {
        let private_key =
            crypto::signature::PrivateKey::generate().context("Failed to create private key")?;
        let encryption_key =
            crypto::encryption::Key::generate().context("Failed to create encryption key")?;
        let decapsulation_key = crypto::encapsulation::DecapsulationKey::generate()
            .context("Failed to create decapsulation key")?;

        let (encrypted_private_key, encrypted_encryption_key, encrypted_decapsulation_key) = {
            let password_hash = crypto::hash::Hasher::once_raw(password.as_bytes());
            let password_key = crypto::encryption::Key::from_bytes(password_hash.as_ref())
                .context("Failed to create encryption key from password")?;
            let cipher = crypto::encryption::Cipher::new(password_key);
            (
                cipher
                    .encrypt(private_key.to_bytes())
                    .context("Failed to encrypt private key")?,
                cipher
                    .encrypt(encryption_key.to_bytes())
                    .context("Failed to encrypt encryption key")?,
                cipher
                    .encrypt(decapsulation_key.to_bytes())
                    .context("Failed to encrypt decapsulation key")?,
            )
        };

        {
            let storage = storage()?;

            let private_key = hex::encode(private_key.to_bytes());
            let encryption_key = hex::encode(encryption_key.to_bytes());
            let decapsulation_key = hex::encode(decapsulation_key.to_bytes());

            storage.set_item(DB_USERNAME, username)?;
            storage.set_item(DB_PRIVATE_KEY, &private_key)?;
            storage.set_item(DB_ENCRYPTION_KEY, &encryption_key)?;
            storage.set_item(DB_DECAPSULATION_KEY, &decapsulation_key)?;
        }

        let encapsulation_key = decapsulation_key.encapsulation_key().to_bytes();
        let client = Client {
            username: username.to_string(),
            private_key,
            encryption_cipher: crypto::encryption::Cipher::new(&encryption_key),
            decapsulation_key,
        };

        let encrypted_client = EncryptedClient {
            encrypted_decapsulation_key,
            encrypted_encryption_key,
            encrypted_private_key,
        };
        client
            .set(&["username", username], &encrypted_client)
            .await
            .context("Failed to create new user")?;
        client
            .set(&["shared", username], &encapsulation_key)
            .await
            .context("Failed to create shared user")?;

        CLIENT.update(|c| {
            *c = Some(client);
        });

        Ok(())
    }

    pub async fn login(username: &str, password: &str) -> rustolio_utils::Result<()> {
        let EncryptedClient {
            encrypted_private_key,
            encrypted_encryption_key,
            encrypted_decapsulation_key,
        } = ops::get(
            encode_to_bytes(&["username", username]).context("Failed to encode username")?,
        )
        .await
        .context("Failed to load user")?
        .context("User does not exists")?
        .into_value()?;

        let (private_key, encryption_key, decapsulation_key) = {
            let password_hash = crypto::hash::Hasher::once_raw(password.as_bytes());
            let password_key = crypto::encryption::Key::from_bytes(password_hash.as_ref())
                .context("Failed to create encryption key from password")?;
            let cipher = crypto::encryption::Cipher::new(password_key);
            (
                cipher
                    .decrypt(&encrypted_private_key)
                    .context("Failed to decrypt private key")?,
                cipher
                    .decrypt(&encrypted_encryption_key)
                    .context("Failed to decrypt encryption key")?,
                cipher
                    .decrypt(&encrypted_decapsulation_key)
                    .context("Failed to decrypt decapsulation key")?,
            )
        };

        let private_key = crypto::signature::PrivateKey::from_bytes(private_key)
            .context("Failed to construct key")?;
        let encryption_key = crypto::encryption::Key::from_bytes(&encryption_key)
            .context("Failed to construct key")?;
        let decapsulation_key =
            crypto::encapsulation::DecapsulationKey::from_bytes(decapsulation_key)
                .context("Failed to construct key")?;

        {
            let storage = storage()?;

            let private_key = hex::encode(private_key.to_bytes());
            let encryption_key = hex::encode(encryption_key.to_bytes());
            let decapsulation_key = hex::encode(decapsulation_key.to_bytes());

            storage.set_item(DB_USERNAME, username)?;
            storage.set_item(DB_PRIVATE_KEY, &private_key)?;
            storage.set_item(DB_ENCRYPTION_KEY, &encryption_key)?;
            storage.set_item(DB_DECAPSULATION_KEY, &decapsulation_key)?;
        }

        let client = Client {
            username: username.to_string(),
            private_key,
            encryption_cipher: crypto::encryption::Cipher::new(encryption_key),
            decapsulation_key,
        };

        CLIENT.update(|c| {
            *c = Some(client);
        });

        Ok(())
    }

    pub fn logout() -> rustolio_utils::Result<()> {
        let storage = storage()?;
        // Teams::logout()?;
        storage.delete(DB_USERNAME)?;
        storage.delete(DB_PRIVATE_KEY)?;
        storage.delete(DB_ENCRYPTION_KEY)?;
        storage.delete(DB_DECAPSULATION_KEY)?;

        CLIENT.update(|c| {
            *c = None;
        });

        Ok(())
    }

    pub fn local() -> Option<Self> {
        CLIENT.value()
    }

    pub async fn get(&self, key: &impl Encode) -> Result<Option<Value>, ServerFnError<String>> {
        let key = encode_to_bytes(key).unwrap();
        ops::get(key).await
    }

    pub async fn set(
        &self,
        key: &impl Encode,
        value: &impl Encode,
    ) -> Result<(), ServerFnError<String>> {
        let key = encode_to_bytes(key).unwrap();
        let value = Value::from_value(value).unwrap();
        ops::set((key, value)).await
    }

    pub async fn secure_get(
        &self,
        key: &impl Encode,
    ) -> Result<Option<Value>, ServerFnError<String>> {
        let key = encode_to_bytes(key).unwrap();
        let signed_key = self.private_key.sign(key);
        ops::secure_get(signed_key).await
    }

    pub async fn secure_set(
        &self,
        key: &impl Encode,
        value: &impl Encode,
    ) -> Result<(), ServerFnError<String>> {
        let key = encode_to_bytes(key).unwrap();
        let value = Value::from_value(value).unwrap();
        let signed_msg = self.private_key.sign((key, value));
        ops::secure_set(signed_msg).await
    }
}

fn storage() -> rustolio_utils::Result<web_sys::Storage> {
    web_sys::window()
        .context("No window available")?
        .local_storage()?
        .context("No storage available")
}

#[derive(Debug, Clone, Encode, Decode)]
struct EncryptedClient {
    encrypted_private_key: crypto::encryption::Encypted,
    encrypted_encryption_key: crypto::encryption::Encypted,
    encrypted_decapsulation_key: crypto::encryption::Encypted,
}