Skip to main content

ordinary_storage/stores/
secrets.rs

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