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(())
}