use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const DEFAULT_SECRET_ALG: &str = "xchacha20-poly1305";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretsFile {
pub version: u32,
#[serde(default)]
pub secrets: Vec<SecretRecord>,
}
impl Default for SecretsFile {
fn default() -> Self {
Self {
version: 2,
secrets: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretRecord {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alg: Option<String>,
pub data: String,
#[serde(default)]
pub updated_at: i64,
#[serde(default)]
pub version: u64,
#[serde(default)]
pub kind: SecretKind,
}
impl SecretRecord {
pub fn aad(&self) -> Vec<u8> {
secret_record_aad(&self.id, &self.name, self.updated_at, self.version)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SecretKind {
#[default]
Static,
Dynamic {
#[serde(default, skip_serializing_if = "Option::is_none")]
provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
config: Option<Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
bootstrap: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DynamicSecretMetadata {
pub provider: String,
pub config: Value,
#[serde(default)]
pub bootstrap: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_sha256: Option<String>,
}
pub fn secret_record_aad(id: &str, name: &str, updated_at: i64, version: u64) -> Vec<u8> {
const DOMAIN: &[u8] = b"dotlock/secret-record-aad/v1";
let mut aad = Vec::with_capacity(DOMAIN.len() + id.len() + name.len() + 40);
for part in [DOMAIN, id.as_bytes(), name.as_bytes()] {
aad.extend_from_slice(&(part.len() as u64).to_le_bytes());
aad.extend_from_slice(part);
}
aad.extend_from_slice(&updated_at.to_le_bytes());
aad.extend_from_slice(&version.to_le_bytes());
aad
}
#[cfg(test)]
mod tests {
use super::{DEFAULT_SECRET_ALG, SecretKind, SecretRecord};
#[test]
fn legacy_secret_records_default_to_static_kind() {
let record = toml::from_str::<SecretRecord>(
r#"
id = "secret-id"
name = "FOO"
alg = "xchacha20-poly1305"
data = "ciphertext"
updated_at = 1
"#,
)
.expect("record");
assert!(matches!(record.kind, SecretKind::Static));
assert_eq!(record.alg.as_deref(), Some(DEFAULT_SECRET_ALG));
}
}