use base64::engine::general_purpose::STANDARD as B64;
use base64::Engine;
use dynamic_waas_sdk_core::{
api::{RecoverBackupReq, StoreBackupReq},
BackupLocation, BackupLocationInfo, Error, KeyShareBackupInfo, Result, ServerKeyShare,
};
use crate::client::DynamicWalletClient;
use crate::crypto::{self, EncryptedData};
#[derive(serde::Serialize, serde::Deserialize)]
struct KeyShareWrapper<'a> {
#[serde(rename = "keyShareId")]
key_share_id: &'a str,
#[serde(rename = "secretShare")]
secret_share: &'a str,
}
#[derive(Debug, serde::Deserialize)]
struct RecoveredKeyShareWrapper {
#[serde(rename = "keyShareId")]
key_share_id: Option<String>,
#[serde(rename = "secretShare")]
secret_share: String,
}
fn encrypt_share(share: &ServerKeyShare, password: &str) -> Result<String> {
let wrapper = KeyShareWrapper {
key_share_id: &share.key_share_id,
secret_share: &share.secret_share,
};
let plaintext = serde_json::to_string(&wrapper)?;
let enc = crypto::encrypt(&plaintext, password)?;
let enc_json = serde_json::to_string(&enc)?;
Ok(B64.encode(enc_json.as_bytes()))
}
fn decode_recovered_blob(blob: &str, password: &str) -> Result<RecoveredKeyShareWrapper> {
let enc_json_bytes = B64
.decode(blob)
.map_err(|e| Error::Encryption(format!("backup blob b64 decode: {e}")))?;
let enc_json = std::str::from_utf8(&enc_json_bytes)
.map_err(|e| Error::Encryption(format!("backup blob not UTF-8: {e}")))?;
let enc: EncryptedData = serde_json::from_str(enc_json)?;
let plaintext = crypto::decrypt(&enc, password)?;
serde_json::from_str::<RecoveredKeyShareWrapper>(&plaintext).map_err(Error::from)
}
pub async fn run_backup_dynamic(
client: &DynamicWalletClient,
wallet_id: &str,
shares: &[ServerKeyShare],
keygen_id: &str,
password: &str,
) -> Result<KeyShareBackupInfo> {
if shares.is_empty() {
return Err(Error::InvalidArgument(
"shares is empty — nothing to back up".into(),
));
}
let encrypted_blobs: Result<Vec<String>> =
shares.iter().map(|s| encrypt_share(s, password)).collect();
let encrypted_account_credentials = encrypted_blobs?;
let store_resp = client
.api()
.store_encrypted_backup(
wallet_id,
&StoreBackupReq {
encrypted_account_credentials,
password_encrypted: true,
encryption_version: Some(crypto::V2.to_string()),
},
)
.await?;
let server_key_share_ids = store_resp.key_share_ids;
let locations: Vec<serde_json::Value> = server_key_share_ids
.iter()
.map(|id| {
serde_json::json!({
"location": BackupLocation::Dynamic.as_wire_str(),
"passwordEncrypted": true,
"keygenId": keygen_id,
"externalKeyShareId": id,
})
})
.collect();
let mark_body = serde_json::json!({ "locations": locations });
client
.api()
.mark_key_shares_as_backed_up(wallet_id, &mark_body)
.await?;
let backups = std::iter::once((
BackupLocation::Dynamic.as_wire_str().to_string(),
server_key_share_ids
.into_iter()
.map(|id| BackupLocationInfo {
location: BackupLocation::Dynamic,
key_share_id: id,
})
.collect::<Vec<_>>(),
))
.collect();
Ok(KeyShareBackupInfo {
password_encrypted: true,
backups,
})
}
pub async fn run_mark_external_no_backup(
client: &DynamicWalletClient,
wallet_id: &str,
shares: &[ServerKeyShare],
keygen_id: &str,
) -> Result<()> {
if shares.is_empty() {
return Err(Error::InvalidArgument(
"shares is empty — nothing to mark".into(),
));
}
let locations: Vec<serde_json::Value> = shares
.iter()
.map(|_| {
serde_json::json!({
"location": BackupLocation::External.as_wire_str(),
"passwordEncrypted": false,
"keygenId": keygen_id,
})
})
.collect();
let mark_body = serde_json::json!({ "locations": locations });
client
.api()
.mark_key_shares_as_backed_up(wallet_id, &mark_body)
.await?;
Ok(())
}
pub async fn run_recover_key_shares(
client: &DynamicWalletClient,
wallet_id: &str,
key_share_ids: Vec<String>,
password: &str,
) -> Result<Vec<ServerKeyShare>> {
let resp = client
.api()
.recover_encrypted_backup(wallet_id, &RecoverBackupReq { key_share_ids })
.await?;
let mut recovered = Vec::with_capacity(resp.key_shares.len());
for entry in &resp.key_shares {
let Some(blob) = entry.encrypted_blob() else {
continue;
};
let wrapper = decode_recovered_blob(blob, password)?;
let key_share_id = wrapper
.key_share_id
.or_else(|| entry.id.clone())
.unwrap_or_default();
recovered.push(ServerKeyShare::new(key_share_id, wrapper.secret_share));
}
if recovered.is_empty() {
return Err(Error::InvalidArgument(
"Key share recovery returned no shares. Check that the password is \
correct and the backup exists."
.into(),
));
}
Ok(recovered)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encrypt_share_round_trips_through_decode() {
let share = ServerKeyShare::new("ks-1", "secret-data");
let blob = encrypt_share(&share, "pw").unwrap();
let wrapper = decode_recovered_blob(&blob, "pw").unwrap();
assert_eq!(wrapper.key_share_id.as_deref(), Some("ks-1"));
assert_eq!(wrapper.secret_share, "secret-data");
}
#[test]
fn decode_rejects_wrong_password() {
let share = ServerKeyShare::new("ks-1", "secret-data");
let blob = encrypt_share(&share, "right-pw").unwrap();
let err = decode_recovered_blob(&blob, "wrong-pw").unwrap_err();
assert!(matches!(err, Error::Encryption(_)));
}
}