Skip to main content

ordinary_storage/stores/
secrets.rs

1// Copyright (C) 2026 Ordinary Labs, LLC.
2//
3// SPDX-License-Identifier: AGPL-3.0-only
4
5use anyhow::bail;
6use bytes::Bytes;
7use chacha20poly1305::aead::{Aead, OsRng};
8use chacha20poly1305::{AeadCore, KeyInit, XChaCha20Poly1305, XNonce};
9use saferlmdb::{
10    self as lmdb, Database, DatabaseOptions, Environment, ReadTransaction, WriteTransaction, put,
11};
12use std::sync::Arc;
13use tracing::instrument;
14
15pub struct SecretsStore {
16    env: Arc<Environment>,
17
18    /// stores application secrets.
19    secrets_db: Arc<Database<'static>>,
20
21    encryption_key: [u8; 32],
22}
23
24impl SecretsStore {
25    pub fn new(env: &Arc<Environment>, encryption_key: [u8; 32]) -> anyhow::Result<Self> {
26        let secrets_db = Arc::new(Database::open(
27            env.clone(),
28            Some("secrets"),
29            &DatabaseOptions::new(lmdb::db::Flags::CREATE),
30        )?);
31
32        Ok(Self {
33            env: env.clone(),
34            secrets_db,
35            encryption_key,
36        })
37    }
38
39    /// Stores a secret for the application.
40    #[instrument(skip_all, err)]
41    pub fn put(&self, name: &str, value: &[u8]) -> anyhow::Result<()> {
42        tracing::info!(name);
43
44        let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
45        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
46
47        let txn = WriteTransaction::new(self.env.clone())?;
48
49        {
50            let mut access = txn.access();
51
52            match cipher.encrypt(&nonce, value) {
53                Ok(mut encrypted) => {
54                    encrypted.extend_from_slice(&nonce);
55                    access.put(
56                        &self.secrets_db,
57                        name.as_bytes(),
58                        &encrypted,
59                        &put::Flags::empty(),
60                    )?;
61                }
62                Err(err) => bail!("{err}"),
63            }
64        }
65
66        txn.commit()?;
67
68        Ok(())
69    }
70
71    /// Retrieves a secret for the application.
72    #[instrument(skip_all, err)]
73    pub fn get(&self, name: &str) -> anyhow::Result<Bytes> {
74        tracing::info!(name);
75
76        let cipher = XChaCha20Poly1305::new(&self.encryption_key.into());
77
78        let txn = ReadTransaction::new(self.env.clone())?;
79        let access = txn.access();
80
81        let result = access.get::<[u8], [u8]>(&self.secrets_db, name.as_bytes())?;
82
83        let ciphertext_len = result.len() - 24;
84
85        match cipher.decrypt(
86            XNonce::from_slice(&result[ciphertext_len..]),
87            &result[..ciphertext_len],
88        ) {
89            Ok(plaintext) => Ok(Bytes::copy_from_slice(plaintext.as_ref())),
90            Err(err) => bail!("{err}"),
91        }
92    }
93}