use anyhow::Context;
use clap::Parser;
use holochain_client::{
AdminWebsocket, AppWebsocket, CellId, ClientAgentSigner, DynAgentSigner, SigningCredentials,
ZomeCallTarget,
};
use holochain_conductor_api::{CellInfo, IssueAppAuthenticationTokenPayload};
use holochain_types::prelude::{
AgentPubKey, CapAccess, CapSecret, DnaHashB64, ExternIO, FunctionName,
GrantZomeCallCapabilityPayload, GrantedFunctions, InstalledAppId, ZomeCallCapGrant, ZomeName,
CAP_SECRET_BYTES,
};
use holochain_types::websocket::AllowedOrigins;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
#[derive(Debug, Parser)]
pub struct ConnectArgs {
#[arg(short, long)]
pub port: u16,
}
#[derive(Debug, Parser)]
pub struct ZomeCallAuth {
#[command(flatten)]
pub connect_args: ConnectArgs,
#[arg(long)]
pub piped: bool,
pub app_id: String,
}
#[derive(Debug, Parser)]
pub struct ZomeCall {
#[command(flatten)]
pub connect_args: ConnectArgs,
#[arg(long)]
pub piped: bool,
pub app_id: String,
pub dna_hash: DnaHashB64,
pub zome_name: String,
pub function: String,
pub payload: String,
}
pub async fn zome_call_auth(zome_call_auth: ZomeCallAuth) -> anyhow::Result<()> {
let admin_port = zome_call_auth.connect_args.port;
let admin_client = AdminWebsocket::connect(format!("localhost:{admin_port}"), None).await?;
let app_client = get_app_client(&admin_client, zome_call_auth.app_id.clone(), None).await?;
let app_info = app_client.app_info().await?;
let info = match app_info {
Some(info) => info,
_ => anyhow::bail!("No app info found for app id {}", zome_call_auth.app_id),
};
let cell_ids = info
.cell_info
.values()
.flatten()
.filter_map(|info| match info {
CellInfo::Provisioned(info) => Some(info.cell_id.clone()),
_ => None,
})
.collect::<HashSet<_>>();
let admin_client = AdminWebsocket::connect(format!("localhost:{admin_port}"), None).await?;
holochain_util::pw::pw_set_piped(zome_call_auth.piped);
if !zome_call_auth.piped {
crate::msg!("Enter new passphrase to authorize zome calls: ");
}
let passphrase = holochain_util::pw::pw_get().context("Failed to get passphrase")?;
let (auth, key) = generate_signing_credentials(passphrase).await?;
let signing_agent_key = AgentPubKey::from_raw_32(key.verifying_key().as_bytes().to_vec());
for cell_id in cell_ids {
admin_client
.grant_zome_call_capability(GrantZomeCallCapabilityPayload {
cell_id: cell_id.clone(),
cap_grant: ZomeCallCapGrant::new(
"sandbox".to_string(),
CapAccess::Assigned {
secret: auth.cap_secret,
assignees: vec![signing_agent_key.clone()].into_iter().collect(),
},
GrantedFunctions::All,
),
})
.await?;
crate::msg!("Authorized zome calls for cell: {:?}", cell_id);
}
Ok(())
}
pub async fn zome_call(zome_call: ZomeCall) -> anyhow::Result<()> {
let admin_port = zome_call.connect_args.port;
let admin_client = AdminWebsocket::connect(format!("localhost:{admin_port}"), None).await?;
let app_client = get_app_client(&admin_client, zome_call.app_id.clone(), None).await?;
let app_info = app_client.app_info().await?;
let info = match app_info {
Some(info) => info,
_ => anyhow::bail!("No app info found for app id {}", zome_call.app_id.clone()),
};
let cell_ids = info
.cell_info
.values()
.flatten()
.filter_map(|info| match info {
CellInfo::Provisioned(info)
if info.cell_id.dna_hash() == zome_call.dna_hash.as_ref() =>
{
Some(info.cell_id.clone())
}
_ => None,
})
.collect::<Vec<_>>();
if cell_ids.is_empty() {
anyhow::bail!(
"No cell found for DNA hash [{:?}] in app {:?}",
zome_call.dna_hash,
info
);
}
holochain_util::pw::pw_set_piped(zome_call.piped);
if !zome_call.piped {
crate::msg!("Enter passphrase to authorize zome calls: ");
}
let passphrase = holochain_util::pw::pw_get().context("Failed to get passphrase")?;
let (auth, key) = generate_signing_credentials(passphrase).await?;
let credentials: Vec<(CellId, SigningCredentials)> = cell_ids
.clone()
.into_iter()
.map(|cell_id| {
(
cell_id,
SigningCredentials {
signing_agent_key: AgentPubKey::from_raw_32(
key.verifying_key().as_bytes().to_vec(),
),
keypair: key.clone(),
cap_secret: auth.cap_secret,
},
)
})
.collect();
let app_client =
get_app_client(&admin_client, zome_call.app_id.clone(), Some(credentials)).await?;
let response = app_client
.call_zome(
ZomeCallTarget::CellId(cell_ids.first().unwrap().clone()),
ZomeName::from(zome_call.zome_name),
FunctionName(zome_call.function),
ExternIO::encode(serde_json::from_slice::<serde_json::Value>(
zome_call.payload.as_bytes(),
)?)?,
)
.await?;
serde_json::to_writer(
std::io::stdout(),
&response.decode::<hc_serde_json::Value>()?,
)?;
println!();
Ok(())
}
async fn generate_signing_credentials(
passphrase: Arc<Mutex<sodoken::LockedArray>>,
) -> anyhow::Result<(Auth, ed25519_dalek::SigningKey)> {
let auth = load_or_create_auth().await?;
let mut salt = [0; sodoken::argon2::ARGON2_ID_SALTBYTES];
salt.copy_from_slice(&auth.salt);
let hash = tokio::task::spawn_blocking(move || -> std::io::Result<[u8; 32]> {
let mut hash = [0; 32];
sodoken::argon2::blocking_argon2id(
&mut hash,
&passphrase.lock().unwrap().lock(),
&salt,
sodoken::argon2::ARGON2_ID_OPSLIMIT_INTERACTIVE,
sodoken::argon2::ARGON2_ID_MEMLIMIT_INTERACTIVE,
)?;
Ok(hash)
})
.await??;
Ok((auth, ed25519_dalek::SigningKey::from_bytes(&hash)))
}
async fn get_app_client(
admin_client: &AdminWebsocket,
installed_app_id: InstalledAppId,
credentials: Option<Vec<(CellId, SigningCredentials)>>,
) -> anyhow::Result<AppWebsocket> {
let app_interfaces = admin_client.list_app_interfaces().await?;
let existing_port = app_interfaces
.iter()
.filter_map(|app_interface| {
if app_interface.installed_app_id.is_some()
&& app_interface.installed_app_id.as_ref().unwrap() != &installed_app_id
{
return None;
}
if app_interface.allowed_origins.is_allowed("sandbox") {
Some(app_interface.port)
} else {
None
}
})
.next();
let port = match existing_port {
Some(port) => port,
None => {
admin_client
.attach_app_interface(
0,
None,
AllowedOrigins::Origins(vec!["sandbox".to_string()].into_iter().collect()),
None,
)
.await?
}
};
let token = admin_client
.issue_app_auth_token(IssueAppAuthenticationTokenPayload::for_installed_app_id(
installed_app_id.clone(),
))
.await?;
let signer = ClientAgentSigner::new();
match credentials {
None => (),
Some(c) => {
for (cell_id, creds) in c {
signer.add_credentials(cell_id, creds);
}
}
}
Ok(AppWebsocket::connect(
format!("localhost:{port}"),
token.token,
DynAgentSigner::from(signer),
Some(String::from("sandbox")),
)
.await?)
}
#[derive(Debug, Serialize, Deserialize)]
struct Auth {
salt: Vec<u8>,
cap_secret: CapSecret,
}
async fn create_auth() -> anyhow::Result<Auth> {
let mut salt = [0; sodoken::argon2::ARGON2_ID_SALTBYTES];
sodoken::random::randombytes_buf(&mut salt)?;
let mut cap_secret = [0; CAP_SECRET_BYTES];
sodoken::random::randombytes_buf(&mut cap_secret)?;
let auth = Auth {
salt: salt.to_vec(),
cap_secret: CapSecret::from(cap_secret),
};
std::fs::write(".hc_auth", serde_json::to_vec(&auth)?)
.context("Failed to write .hc_auth file")?;
Ok(auth)
}
async fn load_or_create_auth() -> anyhow::Result<Auth> {
if let Ok(content) = std::fs::read(".hc_auth") {
Ok(serde_json::from_slice(&content)?)
} else {
create_auth().await
}
}