use std::sync::Mutex;
use std::time::Duration;
use crate::client::{Api, ApiError, Auth, ClientBuilder};
use crate::keys::{KeyError, NameContext, NameKey, TokenKeys};
use crate::proto::api::{Scope, TokenSelf, VaultStatus};
use crate::proto::audit::{AuditRow, RowStop, verify_rows};
use crate::proto::children::is_reserved_name;
use crate::proto::descriptor::Descriptor;
use crate::proto::ids::{Hash32, NameHmac, TokenId};
use crate::proto::path::EnvPath;
use crate::proto::record::{RecordContext, RecordKind};
use anyhow::bail;
use crate::mcp::config::Entry;
const TIMEOUT: Duration = Duration::from_secs(60);
fn failure(e: ApiError) -> String {
match e {
ApiError::Transport(t) => format!("could not reach the server: {}", t.message()),
ApiError::Refused { ref message, .. } => format!("{}: {message}", e.stable_code()),
ApiError::Malformed { detail } => {
format!("could not reach the server: unexpected response: {detail}")
}
other => format!("could not reach the server: {other}"),
}
}
#[derive(Debug, Clone)]
pub struct NameInfo {
pub name: String,
pub hmac: NameHmac,
pub version: u64,
pub updated_at: i64,
pub size: u32,
}
struct View {
descriptor: Descriptor,
name_key: NameKey,
}
pub struct Env {
pub path: EnvPath,
pub token_id: TokenId,
api: Api,
token: TokenKeys,
view: Mutex<View>,
}
fn verify_self(path: &EnvPath, token: &TokenKeys, me: &TokenSelf) -> anyhow::Result<View> {
let id = token.id();
if me.token_id != id || me.vault_id != token.vault_id() {
bail!("{path}: token {id}: the server answered for another token or vault");
}
if me.scope != Scope::Meta {
bail!(
"{path}: token {id} has scope {}; gv-mcp accepts only meta tokens, which cannot decrypt values",
me.scope
);
}
let descriptor = me
.descriptor
.verify_for(&token.vault_id(), &me.owner_sign_pub)
.map_err(|e| anyhow::anyhow!("{path}: token {id}: integrity check failed: {e}"))?;
let bundle = token
.open_bundle(
&me.bundle,
&me.owner_sign_pub,
Scope::Meta,
descriptor.generation,
)
.map_err(|e| match e {
KeyError::Kind { .. } => anyhow::anyhow!(
"{path}: token {id}: its bundle holds more than the name key; refusing to start"
),
e => anyhow::anyhow!("{path}: token {id}: its bundle does not verify: {e}"),
})?;
let names = bundle.into_names_only().map_err(|_| {
anyhow::anyhow!(
"{path}: token {id}: its bundle holds more than the name key; refusing to start"
)
})?;
if names.generation != descriptor.generation {
bail!("{path}: token {id}: its bundle is for another generation");
}
Ok(View {
descriptor,
name_key: names.name_key,
})
}
impl Env {
pub fn open(entry: Entry) -> anyhow::Result<Env> {
let Entry {
path,
server,
token,
} = entry;
let id = token.id();
let api = ClientBuilder::new(&server)
.timeout(TIMEOUT)
.build()
.map_err(|e| anyhow::anyhow!("{path}: {e}"))?;
let me: TokenSelf = api
.token_self(&token)
.map_err(|e| anyhow::anyhow!("{path}: token {id}: {}", failure(e)))?
.value;
let view = verify_self(&path, &token, &me)?;
Ok(Env {
path,
token_id: id,
api,
token,
view: Mutex::new(view),
})
}
fn fail(&self, e: ApiError) -> String {
format!("{}: token {}: {}", self.path, self.token_id, failure(e))
}
fn reload(&self) -> Result<(), String> {
let me: TokenSelf = self
.api
.token_self(&self.token)
.map_err(|e| self.fail(e))?
.value;
let view = verify_self(&self.path, &self.token, &me).map_err(|e| format!("{e:#}"))?;
let mut current = self.view.lock().expect("the view lock is never poisoned");
if view.descriptor.generation < current.descriptor.generation {
return Err(format!(
"{}: integrity check failed: generation rollback (saw {}, the server now shows {})",
self.path, current.descriptor.generation, view.descriptor.generation
));
}
*current = view;
Ok(())
}
pub fn names(&self) -> Result<Vec<NameInfo>, String> {
for attempt in 0..2 {
match self.try_names() {
Err(Stale) if attempt == 0 => self.reload()?,
Err(Stale) => {
return Err(format!(
"{}: the vault's generation keeps changing; try again",
self.path
));
}
Ok(result) => return result,
}
}
unreachable!("the loop returns")
}
fn try_names(&self) -> Result<Result<Vec<NameInfo>, String>, Stale> {
let view = self.view.lock().expect("the view lock is never poisoned");
let d = &view.descriptor;
let mut out = Vec::new();
let mut after: Option<NameHmac> = None;
loop {
let page =
match self
.api
.list(Auth::Token(&self.token), RecordKind::Secret, after.as_ref())
{
Ok(p) => p.value,
Err(e) => return Ok(Err(self.fail(e))),
};
for item in page.items {
if item.generation != d.generation {
return Err(Stale);
}
let ctx = RecordContext {
vault_id: d.vault_id,
generation: item.generation,
kind: RecordKind::Secret,
name_index: item.name_hmac,
version: item.version,
written_at: item.written_at,
tombstone: item.tombstone,
value_ct_hash: item.value_ct_hash,
name_ct_hash: Hash32::sha256(&item.name_ct.0),
};
if let Err(e) = ctx.verify(&d.secret_writer_pub, &item.sig) {
return Ok(Err(format!(
"{}: integrity check failed on a listed record: {e}",
self.path
)));
}
if item.tombstone {
continue;
}
let name_ctx = NameContext {
vault_id: d.vault_id,
generation: d.generation,
kind: RecordKind::Secret,
};
let name =
match view
.name_key
.open_name(&name_ctx, &item.name_hmac, &item.name_ct.0)
{
Ok(n) => n,
Err(e) => {
return Ok(Err(format!(
"{}: a secret name does not decrypt: {e}",
self.path
)));
}
};
if is_reserved_name(&name) {
continue;
}
out.push(NameInfo {
name,
hmac: item.name_hmac,
version: item.version,
updated_at: item.written_at,
size: item.size,
});
}
match page.next_cursor {
Some(c) => after = Some(c),
None => break,
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(Ok(out))
}
pub fn audit(&self) -> Result<(Vec<AuditRow>, AuditVerdict), String> {
let mut rows = Vec::new();
let mut after = 0;
let (head, stop) = loop {
let page = self
.api
.audit(Auth::Token(&self.token), after)
.map_err(|e| self.fail(e))?
.value;
let n = page.rows.len();
if let Some(last) = page.rows.last() {
after = last.seq;
}
rows.extend(page.rows);
if page.stop.is_some() || n < crate::client::AUDIT_PAGE {
break (page.head, page.stop);
}
};
let verdict = match verify_rows(None, &rows, head.as_ref(), stop.as_ref()) {
Ok(verified) => Ok(verified.unverifiable.map(|stop| match stop {
RowStop::NewerFormat { seq, v } => format!(
"the rows from seq {} on are in audit row format {v}, newer than this gv-mcp \
reads: they are unverified, not tampered. Upgrade gv-mcp to verify them.",
seq.map_or_else(|| "?".to_owned(), |s| s.to_string())
),
other => format!("verification stopped early: {other:?}"),
})),
Err(e) => Err(e.to_string()),
};
Ok((rows, verdict))
}
pub fn status(&self) -> Result<(VaultStatus, TokenSelf), String> {
let status: VaultStatus = self
.api
.status(Auth::Token(&self.token))
.map_err(|e| self.fail(e))?
.value;
status
.descriptor
.verify_for(&self.token.vault_id(), &status.owner_sign_pub)
.map_err(|e| format!("{}: integrity check failed: {e}", self.path))?;
let me = self
.api
.token_self(&self.token)
.map_err(|e| self.fail(e))?
.value;
Ok((status, me))
}
}
pub type AuditVerdict = Result<Option<String>, String>;
struct Stale;