use serde::Serialize;
use serde::de::DeserializeOwned;
use sha2::{Digest, Sha256};
use uuid::Uuid;
use super::Transport;
use super::VtaClient;
use crate::error::VtaError;
use crate::protocols::backup_management::descriptors::{
AbortBundleBody, AbortBundleResultBody, CompleteExportBody, CompleteExportResultBody,
FinalizeImportBody, FinalizeImportResultBody, InitiateExportBody, InitiateExportResultBody,
InitiateImportBody, InitiateImportResultBody,
};
const TOKEN_HEADER: &str = "X-Backup-Token";
impl VtaClient {
pub async fn backup_export_via_descriptor(
&self,
password: &str,
include_audit: bool,
) -> Result<Vec<u8>, VtaError> {
let req = InitiateExportBody {
password: password.to_string(),
include_audit,
algorithm: "stream".into(),
};
let result: InitiateExportResultBody = self
.post_trust_task(crate::trust_tasks::TASK_BACKUP_INITIATE_EXPORT_1_0, req)
.await?;
let bytes = self
.download_blob(
&result.descriptor.transport_url,
&result.descriptor.transport_token,
)
.await?;
let actual = sha256_hex(&bytes);
if actual != result.descriptor.expected_sha256 {
return Err(VtaError::Protocol(format!(
"downloaded backup hash mismatch: expected {} got {}",
result.descriptor.expected_sha256, actual
)));
}
if bytes.len() as u64 != result.descriptor.expected_size_bytes {
return Err(VtaError::Protocol(format!(
"downloaded backup size mismatch: expected {} got {}",
result.descriptor.expected_size_bytes,
bytes.len()
)));
}
let ack_req = CompleteExportBody {
bundle_id: result.descriptor.bundle_id.clone(),
};
let _: Result<CompleteExportResultBody, VtaError> = self
.post_trust_task(crate::trust_tasks::TASK_BACKUP_COMPLETE_EXPORT_1_0, ack_req)
.await;
Ok(bytes)
}
pub async fn backup_import_via_descriptor(
&self,
bytes: &[u8],
password: &str,
confirm: bool,
) -> Result<FinalizeImportResultBody, VtaError> {
let expected_sha256 = sha256_hex(bytes);
let init_req = InitiateImportBody {
expected_sha256,
expected_size_bytes: bytes.len() as u64,
algorithm: "stream".into(),
};
let result: InitiateImportResultBody = self
.post_trust_task(
crate::trust_tasks::TASK_BACKUP_INITIATE_IMPORT_1_0,
init_req,
)
.await?;
self.upload_blob(
&result.descriptor.transport_url,
&result.descriptor.transport_token,
bytes,
)
.await?;
let finalize_req = FinalizeImportBody {
bundle_id: result.descriptor.bundle_id.clone(),
password: password.to_string(),
confirm,
};
self.post_trust_task(
crate::trust_tasks::TASK_BACKUP_FINALIZE_IMPORT_1_0,
finalize_req,
)
.await
}
pub async fn backup_abort_bundle(
&self,
bundle_id: &str,
) -> Result<AbortBundleResultBody, VtaError> {
let req = AbortBundleBody {
bundle_id: bundle_id.to_string(),
};
self.post_trust_task(crate::trust_tasks::TASK_BACKUP_ABORT_1_0, req)
.await
}
pub async fn post_trust_task<B, R>(
&self,
type_uri: &'static str,
payload: B,
) -> Result<R, VtaError>
where
B: Serialize,
R: DeserializeOwned,
{
let (client, base_url, auth) = match &self.transport {
Transport::Rest {
client,
base_url,
auth,
} => (client, base_url, auth),
#[cfg(feature = "session")]
Transport::DIDComm { .. } => {
return Err(VtaError::Validation(
"backup descriptor pattern is REST-only; \
this client is on DIDComm transport"
.into(),
));
}
};
Self::ensure_token_valid(client, base_url, auth).await?;
let token = auth.lock().await.token.clone();
let payload_value = serde_json::to_value(&payload)?;
let doc = serde_json::json!({
"id": format!("urn:uuid:{}", Uuid::new_v4()),
"type": type_uri,
"payload": payload_value,
});
let url = format!("{}/api/trust-tasks", base_url);
let req = client.post(url).json(&doc);
let resp = Self::with_auth_token(req, &token).send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(VtaError::from_http(status, body));
}
let response_doc: serde_json::Value = resp.json().await?;
let payload = response_doc
.get("payload")
.ok_or_else(|| {
VtaError::Protocol("trust-task response missing `payload` field".into())
})?
.clone();
Ok(serde_json::from_value(payload)?)
}
pub async fn download_blob(
&self,
transport_url: &str,
transport_token: &str,
) -> Result<Vec<u8>, VtaError> {
let client = match &self.transport {
Transport::Rest { client, .. } => client,
#[cfg(feature = "session")]
Transport::DIDComm { rest_client, .. } => rest_client.as_ref().ok_or_else(|| {
VtaError::Validation(
"DIDComm transport has no REST client for blob download".into(),
)
})?,
};
let resp = client
.get(transport_url)
.header(TOKEN_HEADER, transport_token)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(VtaError::from_http(status, body));
}
Ok(resp.bytes().await?.to_vec())
}
pub async fn upload_blob(
&self,
transport_url: &str,
transport_token: &str,
bytes: &[u8],
) -> Result<(), VtaError> {
let client = match &self.transport {
Transport::Rest { client, .. } => client,
#[cfg(feature = "session")]
Transport::DIDComm { rest_client, .. } => rest_client.as_ref().ok_or_else(|| {
VtaError::Validation("DIDComm transport has no REST client for blob upload".into())
})?,
};
let resp = client
.post(transport_url)
.header(TOKEN_HEADER, transport_token)
.body(bytes.to_vec())
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(VtaError::from_http(status, body));
}
Ok(())
}
}
fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
let digest = hasher.finalize();
let mut s = String::with_capacity(digest.len() * 2);
for b in digest {
s.push_str(&format!("{b:02x}"));
}
s
}