use std::collections::{BTreeMap, HashMap};
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, RwLock};
use crate::client::{Answer, Api, ApiError, Auth, Pre, Progress, now};
use crate::keys::{
Bundle, ConfigSecret, FullBundle, NodeKey, OwnerKeys, TokenKeys, VaultSecret, WriterKey,
seal_child_key, seal_for_scope, seal_owner_bundle,
};
use crate::proto::api::{
ChallengePurpose, CreateVaultRequest, ErrorCode, RegisterTokenRequest, RegisterTokenResponse,
ReportTokenRequest, RevokeResponse, Scope, SecretVersion, TokenSelf, VaultStatus, VersionMeta,
};
use crate::proto::audit::{AuditRow, ChainHead, RowStop, verify_chain, verify_rows};
use crate::proto::children::{ChildEntry, ChildMode, ChildrenRecord};
use crate::proto::descriptor::{Descriptor, SignedDescriptor};
use crate::proto::ids::{B64, Hash32, Key32, NameHmac, TokenId, VaultId};
use crate::proto::integrity::IntegrityError;
use crate::proto::path::Segment;
use crate::proto::pow;
use crate::proto::record::RecordKind;
use crate::seal::{
ConfigFormat, Opened, OpenedConfig, OpenedRecord, Writer, WrittenRecord, build_rotation,
open_config_record, open_name, open_secret, verify_listed, verify_meta,
};
use serde::{Deserialize, Serialize};
use crate::error::Error;
pub(crate) const DAY: i64 = 86_400;
pub const EXPIRY_WARNING_SECS: i64 = 14 * DAY;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct DescriptorPin {
pub generation: u32,
pub hash: Hash32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pins {
#[serde(default)]
pub descriptor: Option<DescriptorPin>,
#[serde(default)]
pub secrets: BTreeMap<NameHmac, u64>,
#[serde(default)]
pub configs: BTreeMap<NameHmac, u64>,
}
impl Pins {
fn versions(&mut self, kind: RecordKind) -> &mut BTreeMap<NameHmac, u64> {
match kind {
RecordKind::Secret => &mut self.secrets,
RecordKind::Config => &mut self.configs,
}
}
}
pub(crate) enum Holder {
Owner(Box<OwnerKeys>),
Token(Box<TokenKeys>),
}
#[derive(Debug, Clone)]
pub(crate) struct Item {
pub name: String,
pub hmac: NameHmac,
pub version: u64,
pub updated_at: i64,
pub size: u32,
pub tombstone: bool,
}
pub(crate) struct View {
pub descriptor: Descriptor,
pub bundle: Bundle,
pub scope: Option<Scope>,
pub allow_list: Option<Vec<NameHmac>>,
pub token_expires_at: Option<i64>,
pub status: Option<VaultStatus>,
}
pub(crate) struct RawAudit {
pub rows: Vec<AuditRow>,
pub new_rows: usize,
pub head: Option<ChainHead>,
pub names: HashMap<NameHmac, String>,
pub unverifiable: Option<RowStop>,
}
fn noun(kind: RecordKind) -> &'static str {
match kind {
RecordKind::Secret => "secret",
RecordKind::Config => "config",
}
}
fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
m.lock().unwrap_or_else(|p| p.into_inner())
}
pub(crate) fn short(h: &NameHmac) -> String {
h.to_hex()[..8].to_owned()
}
pub(crate) struct Handle {
api: Api,
holder: Holder,
pub label: String,
pub vault_id: VaultId,
pub owner_sign_pub: Key32,
view: RwLock<Arc<View>>,
warned: AtomicBool,
last_expires_at: AtomicI64,
pins: Mutex<Pins>,
}
fn owner_error(e: ApiError, label: &str) -> Error {
if e.code() == Some(ErrorCode::Unauthorized) {
Error::VaultNotFound {
label: label.to_owned(),
message: format!(
"{label}: the server does not know this vault (it expired, was deleted, or was re-rooted)"
),
}
} else {
Error::api(e, Some(label))
}
}
fn token_refused(e: ApiError) -> Error {
let message = match e.code() {
Some(ErrorCode::Unauthorized) => {
"the token was not accepted: it is unknown, revoked, or its vault has expired"
}
Some(ErrorCode::TokenExpired) => "the token has expired",
_ => return Error::api(e, None),
};
Error::Auth {
code: e.stable_code(),
message: message.to_owned(),
}
}
fn verify_descriptor(
signed: &SignedDescriptor,
vault_id: &VaultId,
owner_sign_pub: &Key32,
) -> Result<Descriptor, IntegrityError> {
#[cfg(all(feature = "adversary-plant", debug_assertions))]
if plant::skips_descriptor_signature() {
return plant::descriptor_without_signature(signed, vault_id, owner_sign_pub);
}
signed.verify_for(vault_id, owner_sign_pub)
}
#[cfg(all(feature = "adversary-plant", not(debug_assertions)))]
compile_error!(
"galata-vault's `adversary-plant` feature weakens verification for the adversarial \
suite's self-test and cannot be built without debug assertions (a release build)"
);
#[cfg(all(feature = "adversary-plant", debug_assertions))]
mod plant {
use crate::proto::descriptor::{Descriptor, SignedDescriptor};
use crate::proto::ids::{Key32, VaultId};
use crate::proto::integrity::IntegrityError;
pub(super) const VAR: &str = "GV_ADVERSARY_PLANT";
pub(super) const SKIP_DESCRIPTOR_SIGNATURE: &str = "skip-descriptor-signature";
pub(super) fn skips_descriptor_signature() -> bool {
std::env::var(VAR).is_ok_and(|v| v == SKIP_DESCRIPTOR_SIGNATURE)
}
pub(super) fn descriptor_without_signature(
signed: &SignedDescriptor,
vault_id: &VaultId,
owner_sign_pub: &Key32,
) -> Result<Descriptor, IntegrityError> {
if VaultId::from_owner_sign_pub(owner_sign_pub) != *vault_id {
return Err(IntegrityError::KeyMismatch("owner signing key"));
}
let descriptor = signed.decode_unverified()?;
if descriptor.vault_id != *vault_id {
return Err(IntegrityError::KeyMismatch("descriptor's vault id"));
}
Ok(descriptor)
}
}
fn owner_view(owner: &OwnerKeys, status: VaultStatus, label: &str) -> Result<View, Error> {
let fail = |e| Error::integrity(label, e);
if status.vault_id != owner.vault_id() {
return Err(fail(IntegrityError::KeyMismatch("vault id")));
}
if status.owner_sign_pub != owner.sign_pub() {
return Err(fail(IntegrityError::KeyMismatch("owner signing key")));
}
if status.owner_box_pub != owner.box_pub() {
return Err(fail(IntegrityError::KeyMismatch("owner box key")));
}
let descriptor = verify_descriptor(&status.descriptor, &owner.vault_id(), &owner.sign_pub())
.map_err(fail)?;
if descriptor.generation != status.generation {
return Err(fail(IntegrityError::BindingMismatch("generation")));
}
let Some(signed) = status.owner_bundle.as_ref() else {
return Err(Error::other("the server sent no owner bundle"));
};
let full = owner
.open_own_bundle(signed, descriptor.generation)
.map_err(|e| Error::key(e, format!("{label}: the owner bundle")))?;
if !full.matches(&descriptor) {
return Err(fail(IntegrityError::KeyMismatch("owner bundle")));
}
Ok(View {
descriptor,
bundle: Bundle::Full(full),
scope: None,
allow_list: None,
token_expires_at: None,
status: Some(status),
})
}
fn token_view(token: &TokenKeys, me: TokenSelf, label: &str) -> Result<(View, Key32), Error> {
if me.vault_id != token.vault_id() {
return Err(Error::integrity(
label,
IntegrityError::KeyMismatch("vault id"),
));
}
if me.token_id != token.id() {
return Err(Error::integrity(
label,
IntegrityError::KeyMismatch("token id"),
));
}
let scope = me.scope.get().ok_or_else(|| {
Error::unsupported(format!(
"{label}: this token has scope {:?}, which this client does not know",
me.scope.unknown().unwrap_or("?")
))
})?;
let descriptor = verify_descriptor(&me.descriptor, &token.vault_id(), &me.owner_sign_pub)
.map_err(|e| Error::integrity(label, e))?;
let bundle = token
.open_bundle(&me.bundle, &me.owner_sign_pub, scope, descriptor.generation)
.map_err(|e| Error::key(e, format!("{label}: the token's bundle")))?;
if !bundle.matches(&descriptor) {
return Err(Error::integrity(
label,
IntegrityError::KeyMismatch("token bundle"),
));
}
Ok((
View {
descriptor,
bundle,
scope: Some(scope),
allow_list: me.allow_list,
token_expires_at: Some(me.expires_at),
status: None,
},
me.owner_sign_pub,
))
}
fn head_before(rows: &[AuditRow]) -> Option<ChainHead> {
rows.first()
.filter(|r| r.seq > 1)
.map(|r| ChainHead::new(r.seq - 1, r.prev))
}
fn children_error(e: impl std::fmt::Display) -> Error {
Error::other(e.to_string())
}
type AuditRows = (Vec<AuditRow>, Option<ChainHead>, Option<RowStop>);
impl Handle {
fn new(api: &Api, holder: Holder, label: &str, view: View, owner_sign_pub: Key32) -> Handle {
let pins = Pins {
descriptor: Some(DescriptorPin {
generation: view.descriptor.generation,
hash: view.descriptor.hash(),
}),
..Pins::default()
};
Handle {
api: api.clone(),
holder,
label: label.to_owned(),
vault_id: view.descriptor.vault_id,
owner_sign_pub,
view: RwLock::new(Arc::new(view)),
warned: AtomicBool::new(false),
last_expires_at: AtomicI64::new(i64::MIN),
pins: Mutex::new(pins),
}
}
pub fn open_owner(api: &Api, key: &NodeKey, label: &str) -> Result<Handle, Error> {
api.require_protocol()
.map_err(|e| Error::api(e, Some(label)))?;
let owner = key.owner();
let answer = api
.status(Auth::Owner(&owner))
.map_err(|e| owner_error(e, label))?;
let view = owner_view(&owner, answer.value, label)?;
let sign_pub = owner.sign_pub();
let handle = Handle::new(api, Holder::Owner(Box::new(owner)), label, view, sign_pub);
handle.note_expiry(answer.expires_at);
Ok(handle)
}
pub fn open_token(api: &Api, token: TokenKeys, label: &str) -> Result<Handle, Error> {
api.require_protocol()
.map_err(|e| Error::api(e, Some(label)))?;
let answer = api.token_self(&token).map_err(token_refused)?;
let (view, sign_pub) = token_view(&token, answer.value, label)?;
let handle = Handle::new(api, Holder::Token(Box::new(token)), label, view, sign_pub);
handle.note_expiry(answer.expires_at);
Ok(handle)
}
pub fn create(api: &Api, key: &NodeKey, label: &str) -> Result<bool, Error> {
Handle::create_with_keys(api, key, label, &FullBundle::generate(1))
}
pub fn create_with_keys(
api: &Api,
key: &NodeKey,
label: &str,
full: &FullBundle,
) -> Result<bool, Error> {
if full.generation != 1 {
return Err(Error::other(format!(
"{label}: a new vault starts at generation 1"
)));
}
let owner = key.owner();
let wants_proof = api
.capabilities()
.map_err(|e| Error::api(e, None))?
.is_none_or(|c| c.proof_of_work.is_some());
api.require_protocol()
.map_err(|e| Error::api(e, Some(label)))?;
let (challenge, nonce) = if wants_proof {
let c = api
.challenge(ChallengePurpose::CreateVault)
.map_err(|e| Error::api(e, None))?
.value;
if c.difficulty > 16 {
api.events().progress(&Progress::ProofOfWork {
label: label.to_owned(),
difficulty: c.difficulty,
});
}
let nonce = pow::solve(&c.challenge, c.difficulty).map_err(|e| {
Error::other(format!(
"{label}: the server's proof-of-work challenge (difficulty {}): {e}",
c.difficulty
))
})?;
(Some(c.challenge), Some(nonce))
} else {
(None, None)
};
let descriptor = full.descriptor(owner.vault_id(), Hash32([0; 32]), now());
let owner_bundle = seal_owner_bundle(&owner, full)
.map_err(|e| Error::key(e, format!("{label}: the owner bundle")))?;
let request = CreateVaultRequest::new(
owner.vault_id(),
owner.sign_pub(),
owner.box_pub(),
owner.sign_descriptor(&descriptor),
owner_bundle,
)
.with_proof(challenge, nonce);
match api.create_vault(&owner, &request) {
Ok(_) => {}
Err(e) if e.code() == Some(ErrorCode::Conflict) => return Ok(false),
Err(e) => {
return Err(Error::api(e, None).context(format!("creating the vault for {label}")));
}
}
let vault = Handle::open_owner(api, key, label)?;
match vault.put_children(&ChildrenRecord::new(), Pre::Create) {
Ok(_) | Err(Error::Conflict { .. }) => Ok(true),
Err(e) => Err(e.context(format!("{label}: the children record"))),
}
}
pub fn report_token(api: &Api, token: &TokenKeys) -> Result<RevokeResponse, Error> {
let ts = now();
let request = ReportTokenRequest::new(token.id(), ts, token.sign_report(ts));
Ok(api
.report_token(&request)
.map_err(|e| Error::api(e, None))?
.value)
}
pub fn api(&self) -> &Api {
&self.api
}
pub fn view(&self) -> Arc<View> {
self.view.read().unwrap_or_else(|p| p.into_inner()).clone()
}
fn auth(&self) -> Auth<'_> {
match &self.holder {
Holder::Owner(o) => Auth::Owner(o),
Holder::Token(t) => Auth::Token(t),
}
}
pub fn owner(&self) -> Result<&OwnerKeys, Error> {
match &self.holder {
Holder::Owner(o) => Ok(o.as_ref()),
Holder::Token(_) => Err(Error::forbidden(format!(
"{}: this needs the owner key, not a token",
self.label
))),
}
}
pub fn scope(&self) -> Option<Scope> {
self.view().scope
}
pub fn status(&self) -> Option<VaultStatus> {
self.view().status.clone()
}
pub fn last_expires_at(&self) -> Option<i64> {
let at = self.last_expires_at.load(Ordering::Relaxed);
(at != i64::MIN).then_some(at)
}
pub fn token_expires_at(&self) -> Option<i64> {
self.view().token_expires_at
}
fn note_expiry(&self, expires_at: Option<i64>) {
let Some(at) = expires_at else { return };
self.last_expires_at.store(at, Ordering::Relaxed);
if at - now() < EXPIRY_WARNING_SECS && !self.warned.swap(true, Ordering::Relaxed) {
self.api.events().expiry(&self.label, at);
}
}
fn take<T>(&self, answer: Answer<T>) -> T {
self.note_expiry(answer.expires_at);
answer.value
}
fn server_err(&self, e: ApiError) -> Error {
Error::api(e, Some(&self.label))
}
fn integrity(&self, e: IntegrityError) -> Error {
Error::integrity(&self.label, e)
}
pub fn may_read(&self, v: &View, name: &NameHmac) -> bool {
v.allow_list.as_ref().is_none_or(|l| l.contains(name))
}
pub fn hmac(&self, name: &str) -> NameHmac {
self.view().bundle.name_key().hmac(name)
}
#[cfg(feature = "test-util")]
pub fn name_key(&self) -> crate::keys::NameKey {
self.view().bundle.name_key().clone()
}
pub fn full<'v>(&self, v: &'v View) -> Result<&'v FullBundle, Error> {
match &v.bundle {
Bundle::Full(f) => Ok(f),
_ => Err(Error::forbidden(format!(
"{}: this needs every key of the vault (the owner, or an admin token)",
self.label
))),
}
}
pub fn vault_secret<'v>(&self, v: &'v View) -> Result<&'v VaultSecret, Error> {
v.bundle.vault_secret().ok_or_else(|| {
Error::forbidden(format!(
"{}: this credential can list names but cannot decrypt secrets",
self.label
))
})
}
pub fn config_secret<'v>(&self, v: &'v View) -> Result<&'v ConfigSecret, Error> {
v.bundle.config_secret().ok_or_else(|| {
Error::forbidden(format!(
"{}: this credential cannot read configs",
self.label
))
})
}
fn writer<'v>(&self, v: &'v View, kind: RecordKind) -> Result<&'v WriterKey, Error> {
match kind {
RecordKind::Secret => v.bundle.secret_writer(),
RecordKind::Config => v.bundle.config_writer(),
}
.ok_or_else(|| {
Error::forbidden(format!(
"{}: this credential may not write {}s",
self.label,
noun(kind)
))
})
}
pub fn pins(&self) -> Pins {
lock(&self.pins).clone()
}
pub fn set_pins(&self, pins: Pins) -> Result<(), Error> {
if let Some(pin) = &pins.descriptor {
self.check_descriptor_pin(pin, &self.view().descriptor)?;
}
let mut mine = lock(&self.pins);
let current = mine.descriptor;
*mine = pins;
mine.descriptor = current;
Ok(())
}
fn check_descriptor_pin(&self, pin: &DescriptorPin, d: &Descriptor) -> Result<(), Error> {
let rollback = || {
self.integrity(IntegrityError::GenerationRollback {
known: pin.generation,
got: d.generation,
})
};
if d.generation < pin.generation {
return Err(rollback());
}
if d.generation == pin.generation {
return if d.hash() == pin.hash {
Ok(())
} else {
Err(rollback())
};
}
let (mut generation, mut hash) = (pin.generation, pin.hash);
for next in self.descriptors_after(pin.generation)? {
if next.generation > d.generation {
break;
}
if next.generation != generation + 1 || next.prev_hash != hash {
return Err(self.integrity(IntegrityError::BindingMismatch("descriptor chain")));
}
(generation, hash) = (next.generation, next.hash());
}
if generation != d.generation || hash != d.hash() {
return Err(self.integrity(IntegrityError::BindingMismatch("descriptor chain")));
}
Ok(())
}
fn check_version(&self, kind: RecordKind, index: &NameHmac, got: u64) -> Result<(), Error> {
let mut pins = lock(&self.pins);
let map = pins.versions(kind);
match map.get(index) {
Some(&known) if got < known => {
Err(self.integrity(IntegrityError::VersionRollback { known, got }))
}
_ => {
map.insert(*index, got);
Ok(())
}
}
}
pub fn descriptors_after(&self, after: u32) -> Result<Vec<Descriptor>, Error> {
let list = self
.api
.descriptors(self.auth(), after)
.map_err(|e| self.server_err(e))?;
self.take(list)
.descriptors
.iter()
.map(|s| {
verify_descriptor(s, &self.vault_id, &self.owner_sign_pub)
.map_err(|e| self.integrity(e))
})
.collect()
}
pub fn refresh(&self) -> Result<(), Error> {
let (view, expires_at) = match &self.holder {
Holder::Owner(owner) => {
let answer = self
.api
.status(Auth::Owner(owner))
.map_err(|e| owner_error(e, &self.label))?;
(
owner_view(owner, answer.value, &self.label)?,
answer.expires_at,
)
}
Holder::Token(token) => {
let answer = self.api.token_self(token).map_err(token_refused)?;
let (view, sign_pub) = token_view(token, answer.value, &self.label)?;
if sign_pub != self.owner_sign_pub {
return Err(self.integrity(IntegrityError::KeyMismatch("owner signing key")));
}
(view, answer.expires_at)
}
};
let pinned = lock(&self.pins).descriptor;
if let Some(pin) = pinned {
self.check_descriptor_pin(&pin, &view.descriptor)?;
}
let pin = DescriptorPin {
generation: view.descriptor.generation,
hash: view.descriptor.hash(),
};
*self.view.write().unwrap_or_else(|p| p.into_inner()) = Arc::new(view);
lock(&self.pins).descriptor = Some(pin);
self.note_expiry(expires_at);
Ok(())
}
fn list_kind(&self, v: &View, kind: RecordKind) -> Result<Vec<Item>, Error> {
let mut out = Vec::new();
let mut after: Option<NameHmac> = None;
loop {
let page = self
.api
.list(self.auth(), kind, after.as_ref())
.map_err(|e| self.server_err(e))?;
let page = self.take(page);
for it in page.items {
let what = || format!("{}: {} #{}", self.label, noun(kind), short(&it.name_hmac));
verify_listed(&v.descriptor, kind, &it).map_err(|e| Error::seal(e, what()))?;
let name = open_name(
&v.descriptor,
v.bundle.name_key(),
kind,
&it.name_hmac,
&it.name_ct.0,
)
.map_err(|e| Error::seal(e, what()))?;
self.check_version(kind, &it.name_hmac, it.version)?;
out.push(Item {
name,
hmac: it.name_hmac,
version: it.version,
updated_at: it.written_at,
size: it.size,
tombstone: it.tombstone,
});
}
match page.next_cursor {
Some(c) => after = Some(c),
None => break,
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
}
fn versions_kind(
&self,
v: &View,
kind: RecordKind,
name: &str,
) -> Result<Vec<VersionMeta>, Error> {
let index = v.bundle.name_key().index(kind, name);
let list = match self.api.versions(self.auth(), kind, &index) {
Ok(a) => self.take(a),
Err(e) if e.code() == Some(ErrorCode::NotFound) => {
return match self.moved_on(v) {
Some(moved) => Err(moved),
None => Ok(Vec::new()),
};
}
Err(e) => return Err(self.server_err(e)),
};
for m in &list.versions {
verify_meta(&v.descriptor, kind, &index, m).map_err(|e| {
Error::seal(
e,
format!(
"{}: version {} of {} {name}",
self.label,
m.version,
noun(kind)
),
)
})?;
}
Ok(list.versions)
}
fn moved_on(&self, v: &View) -> Option<Error> {
let status = self.refresh_status().ok()?;
if status.generation == v.descriptor.generation {
return None;
}
Some(Error::stale(
&self.label,
format!(
"{}: the vault moved to generation {} since this handle was opened, so its \
records are under new keys; refresh it",
self.label, status.generation
),
))
}
fn fetch(
&self,
v: &View,
kind: RecordKind,
name: &str,
version: Option<u64>,
) -> Result<SecretVersion, Error> {
let index = v.bundle.name_key().index(kind, name);
let record = match self.api.record(self.auth(), kind, &index, version) {
Ok(a) => self.take(a),
Err(e) if e.code() == Some(ErrorCode::NotFound) => {
if let Some(moved) = self.moved_on(v) {
return Err(moved);
}
return Err(Error::not_found(match version {
Some(n) => format!("{}: {} {name} has no version {n}", self.label, noun(kind)),
None => format!("{}: no {} named {name}", self.label, noun(kind)),
}));
}
Err(e) if e.code() == Some(ErrorCode::Forbidden) => {
return Err(Error::forbidden(format!(
"{}: this credential may not read {} {name}",
self.label,
noun(kind)
)));
}
Err(e) => return Err(self.server_err(e)),
};
if record.name_hmac != index {
return Err(self.integrity(IntegrityError::BindingMismatch("record index")));
}
match version {
Some(want) if record.version != want => {
return Err(self.integrity(IntegrityError::BindingMismatch("record version")));
}
Some(_) => {}
None => {
crate::seal::verify_version(&v.descriptor, kind, &record).map_err(|e| {
Error::seal(
e,
format!(
"{}: version {} of {} #{}",
self.label,
record.version,
noun(kind),
short(&record.name_hmac)
),
)
})?;
self.check_version(kind, &index, record.version)?;
}
}
Ok(record)
}
fn open_record(
&self,
v: &View,
kind: RecordKind,
record: &SecretVersion,
) -> Result<OpenedRecord, Error> {
let what = || {
format!(
"{}: version {} of {} #{}",
self.label,
record.version,
noun(kind),
short(&record.name_hmac)
)
};
match kind {
RecordKind::Secret => open_secret(
&v.descriptor,
v.bundle.name_key(),
self.vault_secret(v)?,
record,
)
.map_err(|e| Error::seal(e, what())),
RecordKind::Config => open_config_record(
&v.descriptor,
v.bundle.name_key(),
self.config_secret(v)?,
record,
)
.map_err(|e| Error::seal(e, what())),
}
}
fn send_write(&self, v: &View, name: &str, rec: WrittenRecord, pre: Pre) -> Result<u64, Error> {
let kind = rec.kind;
let deleting = rec.value_ct.is_none();
let answer = match (rec.put_request(), rec.delete_request()) {
(Some(put), _) => self.api.put(self.auth(), kind, &rec.name_hmac, &put, pre),
(None, Some(delete)) => {
self.api
.delete(self.auth(), kind, &rec.name_hmac, &delete, pre)
}
(None, None) => {
return Err(Error::other(format!(
"{}: {name} has neither a value nor a tombstone",
self.label
)));
}
};
match answer {
Ok(a) => {
let version = self.take(a).version;
if version != rec.version {
return Err(self.integrity(IntegrityError::BindingMismatch("written version")));
}
self.check_version(kind, &rec.name_hmac, rec.version)?;
Ok(rec.version)
}
Err(e)
if matches!(
e.code(),
Some(ErrorCode::PreconditionFailed | ErrorCode::VersionMismatch)
) =>
{
Err(self.conflict(v, kind, name, pre, deleting))
}
Err(e) if e.code() == Some(ErrorCode::Forbidden) => Err(Error::forbidden(format!(
"{}: this credential may not write {} {name}",
self.label,
noun(kind)
))),
Err(e) if e.code() == Some(ErrorCode::StaleGeneration) => Err(Error::stale(
&self.label,
format!(
"{}: the vault was rotated while writing {name}; open it again (or refresh the handle) and retry",
self.label
),
)),
Err(e) => Err(self.server_err(e)),
}
}
#[allow(clippy::too_many_arguments)]
fn write_value(
&self,
v: &View,
kind: RecordKind,
name: &str,
format: Option<ConfigFormat>,
body: &[u8],
pre: Pre,
written_at: i64,
) -> Result<u64, Error> {
let w = Writer {
descriptor: &v.descriptor,
name_key: v.bundle.name_key(),
writer: self.writer(v, kind)?,
};
let version = pre.next_version();
let rec = match (kind, format) {
(RecordKind::Secret, _) => w.secret(name, body, version, written_at),
(RecordKind::Config, Some(f)) => w.config(name, f, body, version, written_at),
(RecordKind::Config, None) => {
w.config(name, ConfigFormat::Text, body, version, written_at)
}
}
.map_err(|e| {
Error::seal(
e,
format!("{}: encrypting {} {name}", self.label, noun(kind)),
)
})?;
self.send_write(v, name, rec, pre)
}
fn write_tombstone(
&self,
v: &View,
kind: RecordKind,
name: &str,
pre: Pre,
written_at: i64,
) -> Result<u64, Error> {
let w = Writer {
descriptor: &v.descriptor,
name_key: v.bundle.name_key(),
writer: self.writer(v, kind)?,
};
let rec = w
.tombstone(kind, name, pre.next_version(), written_at)
.map_err(|e| {
Error::seal(e, format!("{}: deleting {} {name}", self.label, noun(kind)))
})?;
self.send_write(v, name, rec, pre)
}
fn conflict(
&self,
v: &View,
kind: RecordKind,
name: &str,
expected: Pre,
deleting: bool,
) -> Error {
match self.versions_kind(v, kind, name) {
Ok(versions) => {
Error::conflict(name, expected, versions.last().map(|m| m.version), deleting)
}
Err(e) => e,
}
}
pub fn list(&self) -> Result<Vec<Item>, Error> {
self.list_kind(&self.view(), RecordKind::Secret)
}
pub fn versions(&self, name: &str) -> Result<Vec<VersionMeta>, Error> {
self.versions_kind(&self.view(), RecordKind::Secret, name)
}
pub fn current(&self, name: &str) -> Result<Pre, Error> {
Ok(Pre::after(self.versions(name)?.last()))
}
pub fn get(&self, name: &str, version: Option<u64>) -> Result<(u64, Opened), Error> {
let v = self.view();
self.vault_secret(&v)?;
let record = self.fetch(&v, RecordKind::Secret, name, version)?;
let rec = self.open_record(&v, RecordKind::Secret, &record)?;
let Some(value) = rec.value else {
return Err(Error::not_found(format!(
"{}: {name} was deleted at version {}",
self.label, record.version
)));
};
Ok((
record.version,
Opened {
name: rec.name,
value,
written_at: rec.written_at,
},
))
}
pub fn put(&self, name: &str, value: &[u8], pre: Pre) -> Result<u64, Error> {
let v = self.view();
self.write_value(&v, RecordKind::Secret, name, None, value, pre, now())
}
pub fn delete(&self, name: &str, version: u64) -> Result<u64, Error> {
let v = self.view();
self.write_tombstone(&v, RecordKind::Secret, name, Pre::Update(version), now())
}
pub fn list_configs(&self) -> Result<Vec<Item>, Error> {
self.list_kind(&self.view(), RecordKind::Config)
}
pub fn config_versions(&self, name: &str) -> Result<Vec<VersionMeta>, Error> {
self.versions_kind(&self.view(), RecordKind::Config, name)
}
pub fn config_current(&self, name: &str) -> Result<Pre, Error> {
Ok(Pre::after(self.config_versions(name)?.last()))
}
pub fn get_config(
&self,
name: &str,
version: Option<u64>,
) -> Result<(u64, OpenedConfig), Error> {
let v = self.view();
self.config_secret(&v)?;
let record = self.fetch(&v, RecordKind::Config, name, version)?;
let rec = self.open_record(&v, RecordKind::Config, &record)?;
let (Some(body), Some(format)) = (rec.value, rec.format) else {
return Err(Error::not_found(format!(
"{}: config {name} was deleted at version {}",
self.label, record.version
)));
};
Ok((
record.version,
OpenedConfig {
name: rec.name,
format,
body,
written_at: rec.written_at,
},
))
}
pub fn put_config(
&self,
name: &str,
format: ConfigFormat,
body: &[u8],
pre: Pre,
) -> Result<u64, Error> {
let v = self.view();
self.write_value(&v, RecordKind::Config, name, Some(format), body, pre, now())
}
pub fn delete_config(&self, name: &str, version: u64) -> Result<u64, Error> {
let v = self.view();
self.write_tombstone(&v, RecordKind::Config, name, Pre::Update(version), now())
}
pub fn children(&self) -> Result<(ChildrenRecord, Pre), Error> {
let owner = self.owner()?;
match self.api.children_get(owner) {
Ok(a) => {
let blob = self.take(a);
let record = owner
.open_children(&blob)
.map_err(|e| Error::key(e, format!("{}: the children record", self.label)))?;
Ok((record, Pre::Update(blob.version)))
}
Err(e) if e.code() == Some(ErrorCode::NotFound) => {
Ok((ChildrenRecord::new(), Pre::Create))
}
Err(e) => Err(self.server_err(e)),
}
}
pub fn put_children(&self, record: &ChildrenRecord, pre: Pre) -> Result<u64, Error> {
let owner = self.owner()?;
let version = pre.next_version();
let blob = owner
.seal_children(version, record)
.map_err(|e| Error::key(e, "the children record".to_owned()))?;
match self.api.children_put(owner, &blob, pre) {
Ok(a) => {
self.take(a);
Ok(version)
}
Err(e)
if matches!(
e.code(),
Some(ErrorCode::PreconditionFailed | ErrorCode::VersionMismatch)
) =>
{
Err(Error::conflict(
&format!("{}'s children record", self.label),
pre,
None,
false,
))
}
Err(e) => Err(self.server_err(e)),
}
}
pub fn update_children(
&self,
mut change: impl FnMut(&mut ChildrenRecord) -> Result<(), Error>,
) -> Result<(), Error> {
for _ in 0..5 {
let (mut record, pre) = self.children()?;
change(&mut record)?;
match self.put_children(&record, pre) {
Ok(_) => return Ok(()),
Err(Error::Conflict { .. }) => continue,
Err(e) => return Err(e),
}
}
Err(Error::other(format!(
"{}: the children record kept changing; try again",
self.label
)))
}
pub fn seal_child(&self, seg: &Segment, key: &NodeKey) -> Result<(), Error> {
let to = self.owner()?.box_pub();
let key_ct = seal_child_key(&to, &self.vault_id, key)
.map_err(|e| Error::key(e, format!("the sealed key for {seg}")))?;
self.update_children(|r| {
let mode = ChildMode::Sealed {
key_ct: B64(key_ct.clone()),
};
if r.set_mode(seg, mode.clone()).is_err() {
r.insert(ChildEntry::new(seg.clone(), now(), mode))
.map_err(children_error)?;
}
Ok(())
})
}
pub fn refresh_status(&self) -> Result<VaultStatus, Error> {
let answer = self
.api
.status(self.auth())
.map_err(|e| self.server_err(e))?;
let status = self.take(answer);
if status.owner_sign_pub != self.owner_sign_pub || status.vault_id != self.vault_id {
return Err(self.integrity(IntegrityError::KeyMismatch("owner signing key")));
}
verify_descriptor(&status.descriptor, &self.vault_id, &self.owner_sign_pub)
.map_err(|e| self.integrity(e))?;
Ok(status)
}
fn audit_page(&self, after: u64) -> Result<crate::proto::api::AuditPage, Error> {
match self.api.audit(self.auth(), after) {
Ok(a) => Ok(self.take(a)),
Err(e) if e.code() == Some(ErrorCode::Forbidden) => Err(Error::forbidden(format!(
"{}: this credential may not read the audit log",
self.label
))),
Err(e) => Err(self.server_err(e)),
}
}
fn audit_rows(&self, mut after: u64, until: Option<u64>) -> Result<AuditRows, Error> {
let mut rows = Vec::new();
loop {
let page = self.audit_page(after)?;
let n = page.rows.len();
if let Some(last) = page.rows.last() {
after = last.seq;
}
rows.extend(page.rows);
if page.stop.is_some() {
return Ok((rows, page.head, page.stop));
}
if n < crate::client::AUDIT_PAGE || until.is_some_and(|u| after >= u) {
return Ok((rows, page.head, None));
}
}
}
pub fn verify_audit(
&self,
known: Option<ChainHead>,
earlier: usize,
) -> Result<RawAudit, Error> {
let failed = format!("{}: audit verification FAILED", self.label);
let (rows, server_head, stop) = self.audit_rows(known.map_or(0, |h| h.seq), None)?;
let verified = verify_rows(known.as_ref(), &rows, server_head.as_ref(), stop.as_ref())
.map_err(|e| Error::audit_chain(e, &failed))?;
let head = verified.head;
let mut shown = Vec::new();
if let Some(k) = known.filter(|k| earlier > 0 && k.seq > 0) {
let (mut before, _, _) =
self.audit_rows(k.seq.saturating_sub(earlier as u64), Some(k.seq))?;
before.retain(|r| r.seq <= k.seq);
if !before.is_empty() {
let reached = verify_chain(head_before(&before).as_ref(), &before)
.map_err(|e| Error::audit_chain(e, &failed))?;
if reached != Some(k) {
return Err(self
.integrity(IntegrityError::BindingMismatch("audit rows"))
.context(&failed));
}
shown = before;
}
}
let new_rows = rows.len();
shown.extend(rows);
let mut names = HashMap::new();
for item in self.list().unwrap_or_default() {
names.insert(item.hmac, item.name);
}
for item in self.list_configs().unwrap_or_default() {
names.insert(item.hmac, item.name);
}
Ok(RawAudit {
rows: shown,
new_rows,
head,
names,
unverifiable: verified.unverifiable,
})
}
fn all_versions(&self, v: &View, kind: RecordKind) -> Result<Vec<SecretVersion>, Error> {
let mut out = Vec::new();
for item in self.list_kind(v, kind)? {
for meta in self.versions_kind(v, kind, &item.name)? {
out.push(self.fetch(v, kind, &item.name, Some(meta.version))?);
}
}
Ok(out)
}
pub fn rotate(&self, revoke: &[TokenId]) -> Result<u32, Error> {
let owner = self.owner()?;
let v = self.view();
let full = self.full(&v)?;
for _ in 0..3 {
let status = self.refresh_status()?;
if status.generation != v.descriptor.generation {
return Err(Error::stale(
&self.label,
format!(
"{}: the vault moved to generation {} since it was opened; open it again",
self.label, status.generation
),
));
}
let gathered = self
.all_versions(&v, RecordKind::Secret)
.and_then(|secrets| {
self.all_versions(&v, RecordKind::Config)
.map(|configs| (secrets, configs))
});
let (secrets, configs) = match gathered {
Ok(both) => both,
Err(Error::NotFound { .. }) => continue,
Err(e) => return Err(e),
};
let rotation = build_rotation(
owner,
&status,
&v.descriptor,
full,
&secrets,
&configs,
revoke,
now(),
)
.map_err(|e| Error::seal(e, format!("{}: preparing the rotation", self.label)))?;
match self.api.rotate(owner, &rotation.request) {
Ok(a) => return Ok(self.take(a).generation),
Err(e) if e.code() == Some(ErrorCode::Conflict) => continue,
Err(e) => return Err(self.server_err(e)),
}
}
Err(Error::other(format!(
"{}: the vault kept changing during rotation; try again",
self.label
)))
}
pub fn mint(
&self,
scope: Scope,
ttl_secs: u64,
allow_list: Option<Vec<NameHmac>>,
) -> Result<(TokenKeys, RegisterTokenResponse), Error> {
let owner = self.owner()?;
let v = self.view();
let full = self.full(&v)?;
let token = TokenKeys::generate(self.vault_id);
let bundle = seal_for_scope(owner, &token.box_pub(), &token.id(), scope, full)
.map_err(|e| Error::key(e, format!("{}: minting a {scope} token", self.label)))?;
let request = RegisterTokenRequest::new(
token.id(),
token.auth_pub(),
token.box_pub(),
scope,
ttl_secs,
allow_list,
full.generation,
bundle,
);
let answer = self
.api
.register_token(owner, &request)
.map_err(|e| self.server_err(e))?;
Ok((token, self.take(answer)))
}
pub fn revoke(&self, id: &TokenId) -> Result<RevokeResponse, Error> {
let answer = self
.api
.revoke(self.auth(), id)
.map_err(|e| self.server_err(e))?;
Ok(self.take(answer))
}
pub fn delete_vault(&self) -> Result<(), Error> {
let owner = self.owner()?;
let answer = self
.api
.delete_vault(owner)
.map_err(|e| self.server_err(e))?;
self.take(answer);
Ok(())
}
pub fn copy_from(&self, old: &Handle) -> Result<usize, Error> {
let v = self.view();
let ov = old.view();
self.full(&v)?;
old.full(&ov)?;
let mut copied = 0;
for kind in [RecordKind::Secret, RecordKind::Config] {
for item in old.list_kind(&ov, kind)? {
let mut source = Vec::new();
for meta in old.versions_kind(&ov, kind, &item.name)? {
if source.is_empty() && meta.tombstone {
continue;
}
source.push(meta.version);
}
let target = self.versions_kind(&v, kind, &item.name)?;
let mut pre = Pre::after(target.last());
for &n in source.iter().skip(target.len()) {
let raw = old.fetch(&ov, kind, &item.name, Some(n))?;
let rec = old.open_record(&ov, kind, &raw)?;
let written = match &rec.value {
Some(body) => self.write_value(
&v,
kind,
&item.name,
rec.format,
body,
pre,
rec.written_at,
)?,
None => self.write_tombstone(&v, kind, &item.name, pre, rec.written_at)?,
};
pre = if rec.value.is_some() {
Pre::Update(written)
} else {
Pre::Revive(written)
};
copied += 1;
}
}
}
Ok(copied)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn meta(version: u64, tombstone: bool) -> VersionMeta {
VersionMeta::new(
version,
0,
crate::proto::audit::Actor::Owner,
0,
tombstone,
1,
Hash32([0; 32]),
Hash32([0; 32]),
crate::proto::ids::Sig64([0; 64]),
)
}
#[test]
fn preconditions_follow_the_latest_version() {
assert_eq!(Pre::after(None), Pre::Create);
assert_eq!(Pre::after(Some(&meta(3, true))), Pre::Revive(3));
assert_eq!(Pre::after(Some(&meta(3, false))), Pre::Update(3));
}
#[test]
fn pins_roundtrip_through_json_and_toml() {
let mut pins = Pins {
descriptor: Some(DescriptorPin {
generation: 2,
hash: Hash32([7; 32]),
}),
..Pins::default()
};
pins.secrets.insert(NameHmac([1; 32]), 4);
pins.configs.insert(NameHmac([2; 32]), 9);
let json = serde_json::to_string(&pins).unwrap();
assert_eq!(serde_json::from_str::<Pins>(&json).unwrap(), pins);
let text = toml::to_string(&pins).unwrap();
assert_eq!(toml::from_str::<Pins>(&text).unwrap(), pins);
}
}