ordinary-storage 0.7.0

Storage for Ordinary
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use anyhow::bail;
use bytes::Bytes;
use chacha20poly1305::aead::{Aead, OsRng};
use chacha20poly1305::{AeadCore, KeyInit, XChaCha20Poly1305, XNonce};
use saferlmdb::{
    self as lmdb, Database, DatabaseOptions, Environment, ReadTransaction, WriteTransaction, put,
};
use std::sync::Arc;
use tracing::instrument;

pub struct SecretsStore {
    env: Arc<Environment>,

    /// stores application secrets.
    secrets_db: Arc<Database<'static>>,

    encryption_key: [u8; 32],
}

impl SecretsStore {
    pub fn new(env: &Arc<Environment>, encryption_key: [u8; 32]) -> anyhow::Result<Self> {
        let secrets_db = Arc::new(Database::open(
            env.clone(),
            Some("secrets"),
            &DatabaseOptions::new(lmdb::db::Flags::CREATE),
        )?);

        Ok(Self {
            env: env.clone(),
            secrets_db,
            encryption_key,
        })
    }

    /// Stores a secret for the application.
    #[instrument(skip_all, err)]
    pub fn put(&self, name: &str, value: &[u8]) -> anyhow::Result<()> {
        tracing::info!(name);

        let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);

        let txn = WriteTransaction::new(self.env.clone())?;

        {
            let mut access = txn.access();

            match cipher.encrypt(&nonce, value) {
                Ok(mut encrypted) => {
                    encrypted.extend_from_slice(&nonce);
                    access.put(
                        &self.secrets_db,
                        name.as_bytes(),
                        &encrypted,
                        &put::Flags::empty(),
                    )?;
                }
                Err(err) => bail!("{err}"),
            }
        }

        txn.commit()?;

        Ok(())
    }

    /// Retrieves a secret for the application.
    #[instrument(skip_all, err)]
    pub fn get(&self, name: &str) -> anyhow::Result<Bytes> {
        tracing::info!(name);

        let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());

        let txn = ReadTransaction::new(self.env.clone())?;
        let access = txn.access();

        let result = access.get::<[u8], [u8]>(&self.secrets_db, name.as_bytes())?;

        let ciphertext_len = result.len() - 24;

        match cipher.decrypt(
            XNonce::from_slice(&result[ciphertext_len..]),
            &result[..ciphertext_len],
        ) {
            Ok(plaintext) => Ok(Bytes::copy_from_slice(plaintext.as_ref())),
            Err(err) => bail!("{err}"),
        }
    }
}