ordinary-api 0.6.0-pre.13

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

use crate::client::OrdinaryApiClient;
use anyhow::bail;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use chacha20poly1305::aead::Aead;
use chacha20poly1305::{AeadCore, KeyInit, XChaCha20Poly1305};
use ordinary_auth::OsRng;
use ordinary_config::{OrdinaryConfig, SecretSource};
use x25519_dalek::{EphemeralSecret, PublicKey};

const ZEROED_KEY: [u8; 32] = [0u8; 32];

pub async fn store(
    api_client: &OrdinaryApiClient<'_>,
    proj_path: &str,
    name: &str,
    secret: &[u8],
) -> anyhow::Result<()> {
    let correlation_id = api_client
        .correlation_id
        .unwrap_or(uuid::Uuid::new_v4())
        .to_string();

    let config = OrdinaryConfig::get(proj_path)?;
    let mut valid_secret = false;

    if let Some(secrets) = config.secrets {
        for secret_conf in secrets {
            if secret_conf.name == name
                && let SecretSource::Stored = secret_conf.source
            {
                let access_token = api_client
                    .get_access(None, Some(correlation_id.clone()))
                    .await?;

                tracing::info!("getting public key...");

                let res = api_client
                    .client
                    .get(format!("{}/v1/keys/dh/public", api_client.addr))
                    .query(&[("d", config.domain.clone())])
                    .header("x-correlation-id", correlation_id.as_str())
                    .header(
                        "Authorization",
                        format!("Bearer {}", b64.encode(&access_token)),
                    )
                    .send()
                    .await?;

                if !res.status().is_success() {
                    bail!("failed to get public key. status: {}", res.status());
                }

                let public_key: [u8; 32] = res.bytes().await?.as_ref().try_into()?;

                tracing::info!("public key got.");

                if public_key == ZEROED_KEY {
                    bail!("server public key should not be all zeroes");
                }

                let public_key = PublicKey::from(public_key);
                let ephemeral_secret = EphemeralSecret::random_from_rng(OsRng);
                let ephemeral_public_key = PublicKey::from(&ephemeral_secret);

                let shared_secret = ephemeral_secret.diffie_hellman(&public_key);

                if !shared_secret.was_contributory() {
                    bail!("non-contributory shared secret");
                }

                let mut payload = ephemeral_public_key.as_bytes().to_vec();

                let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
                let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
                payload.extend_from_slice(nonce.as_slice());

                match cipher.encrypt(&nonce, secret) {
                    Ok(encrypted) => {
                        payload.extend_from_slice(&encrypted);
                    }
                    Err(err) => {
                        bail!(err);
                    }
                }

                tracing::info!("storing secret on API server...");
                let res = api_client
                    .client
                    .put(format!("{}/v1/secrets", api_client.addr))
                    .body(payload)
                    .query(&[("d", config.domain.clone()), ("n", name.to_string())])
                    .header("x-correlation-id", correlation_id.as_str())
                    .header(
                        "Authorization",
                        format!("Bearer {}", b64.encode(&access_token)),
                    )
                    .send()
                    .await?;

                if !res.status().is_success() {
                    bail!("failed to store secret. status: {}", res.status());
                }

                tracing::info!("secret stored.");

                valid_secret = true;
                break;
            }
        }

        if !valid_secret {
            tracing::error!("provided name not in secrets");
        }
    } else {
        tracing::warn!("no \"secrets\" field in ordinary.json");
    }

    Ok(())
}