diff --git a/recover-id-object/README.md b/recover-id-object/README.md index 43011ca6..064db4f9 100644 --- a/recover-id-object/README.md +++ b/recover-id-object/README.md @@ -9,5 +9,32 @@ To build run `cargo build --release`. This produces the binary `target/release/r ## Run -See `--help` for the list of all the options. The tool requires access to the -seed phrase to be used for recovery. +See `--help` for the list of all the options. The tool has two modes: `generate-secrets` and `recover-identity`. + +### Generate secrets + +`generate-secrets` generates secrets for the recovery requests based on a seed phrase, the identity provider index, and the identity index. +It outputs the secrets to standard output. + +E.g. + +```bash +recover-id-object generate-secrets --concordium-wallet --ip-index ... --id-index (* other args *) +``` + +### Recover identity + +`recover-identity` recovers the identity objects based on either a seed phrase or the secrets generated by `generate-secrets` and writes the recovered identity object to a JSON file. + +To recover with a seed phrase, run: + +```bash +recover-id-object recover-identity --concordium-wallet --ip-index ... (* other args *) +``` + +To recover with the secrets generated by `generate-secrets`, run: + +```bash +recover-id-object recover-identity --prf-key --id-cred-sec --id-index --ip-index ... (* other args *) + +``` diff --git a/recover-id-object/src/main.rs b/recover-id-object/src/main.rs index 2c7b3b71..b0140ade 100644 --- a/recover-id-object/src/main.rs +++ b/recover-id-object/src/main.rs @@ -1,18 +1,18 @@ -use anyhow::Context; -use clap::Parser; +use anyhow::{bail, Context}; +use clap::{Args, Parser, Subcommand}; use concordium::{ - common::{Versioned, VERSION_0}, + common::{base16_decode_string, base16_encode_string, Versioned, VERSION_0}, id::{ account_holder::generate_id_recovery_request, constants::{ArCurve, AttributeKind, IpPairing}, - types::{ - account_address_from_registration_id, IdRecoveryRequest, IdentityObjectV1, IpInfo, - }, + pedersen_commitment::Value as PedersenValue, + types::{GlobalContext, IdRecoveryRequest, IdentityObjectV1, IpInfo}, }, - v2::BlockIdentifier, + types::CredentialRegistrationID, + v2::{self, AccountIdentifier, BlockIdentifier}, }; use concordium_rust_sdk as concordium; -use key_derivation::ConcordiumHdWallet; +use key_derivation::{ConcordiumHdWallet, PrfKey}; use tonic::transport::ClientTlsConfig; #[derive(Parser, Debug)] @@ -22,35 +22,91 @@ struct Api { long = "concordium-api", name = "concordium-api", help = "GRPC V2 interface of the Concordium node.", - default_value = "http://localhost:20000" + default_value = "http://localhost:20000", + global = true )] api: concordium::v2::Endpoint, /// Request timeout for Concordium node requests. #[clap( long, help = "Timeout for requests to the Concordium node.", - default_value = "10" + default_value = "10", + global = true )] concordium_request_timeout: u64, + #[clap(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + #[clap(about = "Generate secrets for a given seed phrase.")] + GenerateSecrets(GenerateSecretsArgs), + #[clap(about = "Recover an identity from a seed phrase or generated secrets.")] + RecoverIdentity(RecoverIdentityArgs), +} + +#[derive(Debug, Args)] +struct GenerateSecretsArgs { /// Location of the seed phrase. #[clap(long, help = "Path to the seed phrase file.")] concordium_wallet: std::path::PathBuf, + #[clap(long = "ip-index", help = "Identity of the identity provider.")] + ip_index: u32, + #[clap( + long = "id-index", + help = "Index of the identity to generate secrets for." + )] + id_index: u32, +} + +#[derive(Debug, Args)] +struct RecoverIdentityArgs { /// Recovery URL start. #[clap( long = "ip-info-url", help = "Identity recovery URL", default_value = "http://wallet-proxy.testnet.concordium.com/v1/ip_info" )] - wp_url: url::Url, + wp_url: url::Url, + /// Location of the seed phrase. + #[clap( + long, + help = "Path to the seed phrase file. Specify either this or --id-cred-sec, --prf-key, and --id-index.", + conflicts_with_all = ["prf_key", "id_cred_sec", "id_index"], + required_unless_present_all = ["prf_key", "id_cred_sec", "id_index"] + )] + concordium_wallet: Option, #[clap(long = "ip-index", help = "Identity of the identity provider.")] - ip_index: u32, + ip_index: u32, + #[clap( + long, + help = "Hex encoded id credential secret. Specify either this or --concordium-wallet.", + required_unless_present = "concordium_wallet", + requires_all = ["prf_key", "id_index"] + )] + id_cred_sec: Option, + #[clap( + long, + help = "Hex encoded PRF key. Specify either this or --concordium-wallet.", + required_unless_present = "concordium_wallet", + requires_all = ["id_cred_sec", "id_index"] + )] + prf_key: Option, + #[clap( + long, + help = "Identity index of account to recover. Specify either this or --concordium-wallet.", + required_unless_present = "concordium_wallet", + requires_all = ["id_cred_sec", "prf_key"] + )] + id_index: Option, } #[tokio::main] async fn main() -> anyhow::Result<()> { let app: Api = Api::parse(); - let mut concordium_client = { + let concordium_client = { // Use TLS if the URI scheme is HTTPS. // This uses whatever system certificates have been installed as trusted roots. let endpoint = if app @@ -75,21 +131,103 @@ async fn main() -> anyhow::Result<()> { .context("Unable to connect Concordium node.")? }; - let seed_phrase = std::fs::read_to_string(app.concordium_wallet)?; + match app.command { + Command::GenerateSecrets(args) => generate_secrets(concordium_client, args).await, + Command::RecoverIdentity(args) => recover_identity(concordium_client, args).await, + } +} + +/// When finding all accounts for an identity, we stop after this many failures, +/// i.e. if we have not found any accounts for this many consecutive indices. +const MAX_ACCOUNT_FAILURES: u8 = 20; +/// When finding all identities for a seedphrase, we stop after this many +/// failures, i.e. if we have failed to find accounts for a candidate identity +/// this many times. +const MAX_IDENTITY_FAILURES: u8 = 20; + +async fn generate_secrets( + mut concordium_client: v2::Client, + generate_args: GenerateSecretsArgs, +) -> anyhow::Result<()> { + let seed_phrase = std::fs::read_to_string(generate_args.concordium_wallet)?; let words = seed_phrase.split_ascii_whitespace().collect::>(); let wallet = ConcordiumHdWallet::from_words(&words, key_derivation::Net::Testnet); - let client = reqwest::Client::new(); + let crypto_params = concordium_client + .get_cryptographic_parameters(BlockIdentifier::LastFinal) + .await? + .response; + + let ip_index = generate_args.ip_index; + + // Loop through all identities until we find one with an account. + let id_index = generate_args.id_index; + let mut acc_fail_count = 0; + let mut accs_found = 0; + for acc_index in 0u8..=255 { + let reg_id = { + // This needs to be in a separate scope to avoid keeping prf_key across an await + // boundary + let prf_key = wallet + .get_prf_key(ip_index, id_index) + .context("Failed to get PRF key.")?; + let reg_id = prf_key + .prf(crypto_params.elgamal_generator(), acc_index) + .context("Failed to compute PRF.")?; + AccountIdentifier::CredId(CredentialRegistrationID::new(reg_id)) + }; + match concordium_client + .get_account_info(®_id, v2::BlockIdentifier::LastFinal) + .await + { + Ok(info) => { + println!( + "Account with address {} found.", + info.response.account_address + ); + accs_found += 1; + } + Err(e) if e.is_not_found() => { + acc_fail_count += 1; + if acc_fail_count > MAX_ACCOUNT_FAILURES { + break; + } + } + Err(e) => bail!("Cannot query the node: {e}"), + } + } + if accs_found == 0 { + println!("No accounts were found for the supplied indices."); + } + + let prf_key = wallet + .get_prf_key(ip_index, id_index) + .context("Failed to get PRF key.")?; + let id_cred_scalar = wallet + .get_id_cred_sec(ip_index, id_index) + .context("Could not get idCredSec")?; + let id_cred_sec: PedersenValue = PedersenValue::new(id_cred_scalar); + println!("prf-key: {}", base16_encode_string(&prf_key)); + println!("id-cred-sec: {}", base16_encode_string(&id_cred_sec)); + + Ok(()) +} + +async fn recover_identity( + mut concordium_client: v2::Client, + recovery_args: RecoverIdentityArgs, +) -> anyhow::Result<()> { + let client = reqwest::Client::new(); let ids = client - .get(app.wp_url) + .get(recovery_args.wp_url) .send() .await? .json::>() .await?; - let Some(id) = ids.into_iter().find(|x| x.ip_info.ip_identity == app.ip_index.into()) else { - anyhow::bail!("Identity provider not found.") + let Some(id) = ids.into_iter().find(|x| x.ip_info.ip_identity == recovery_args.ip_index.into()) else { + anyhow::bail!("Identity provider not found."); }; println!("Using identity provider {}", id.ip_info.ip_description.name); @@ -97,68 +235,151 @@ async fn main() -> anyhow::Result<()> { .get_cryptographic_parameters(BlockIdentifier::LastFinal) .await? .response; + + if let Some(concordium_wallet) = recovery_args.concordium_wallet { + recover_from_wallet( + concordium_client, + client, + id, + crypto_params, + concordium_wallet, + ) + .await + } else { + let prf_key: PrfKey = + base16_decode_string(&recovery_args.prf_key.context("Missing prf_key")?)?; + let id_cred_sec: PedersenValue = + base16_decode_string(&recovery_args.id_cred_sec.context("Missing prf_key")?)?; + let id_index = recovery_args.id_index.context("Missing id_index")?; + + recover_from_secrets( + &mut concordium_client, + &client, + &id, + &crypto_params, + prf_key, + id_cred_sec, + id_index, + ) + .await + .context("Could not recover identity") + } +} + +async fn recover_from_wallet( + mut concordium_client: v2::Client, + client: reqwest::Client, + id: WpIpInfos, + crypto_params: GlobalContext, + concordium_wallet: std::path::PathBuf, +) -> anyhow::Result<()> { + let seed_phrase = std::fs::read_to_string(concordium_wallet)?; + let words = seed_phrase.split_ascii_whitespace().collect::>(); + let wallet = ConcordiumHdWallet::from_words(&words, key_derivation::Net::Testnet); + + // Try to find all identities for this wallet. let mut failure_count = 0; - for idx in 0.. { - let request = generate_id_recovery_request( - &id.ip_info, + let mut success_count = 0; + for id_index in 0.. { + let id_cred_sec = wallet.get_id_cred_sec(id.ip_info.ip_identity.0, id_index)?; + let id_cred_sec = PedersenValue::new(id_cred_sec); + let prf_key = wallet.get_prf_key(id.ip_info.ip_identity.0, id_index)?; + let result = recover_from_secrets( + &mut concordium_client, + &client, + &id, &crypto_params, - &concordium::id::pedersen_commitment::Value::new( - wallet.get_id_cred_sec(app.ip_index, idx)?, - ), - chrono::Utc::now().timestamp() as u64, + prf_key, + id_cred_sec, + id_index, ) - .context("Unable to construct recovery request")?; - let response = client - .get(id.metadata.recovery_start.clone()) - .query(&[( - "state", - serde_json::to_string(&RecoveryRequestData { - id_recovery_request: Versioned::new(VERSION_0, request), - }) - .unwrap(), - )]) - .send() - .await?; - if response.status().is_success() { - let id_object = response - .json::>>() - .await?; - std::fs::write( - format!("{}-{idx}.json", app.ip_index), - serde_json::to_string_pretty(&serde_json::json!({ - "identityIndex": idx, - "ipInfo": &id.ip_info, - "idObject": id_object.value - }))?, - )?; - println!("Got identity object for index {idx}."); - let mut acc_fail_count = 0; - for acc_idx in 0u8..=255 { - let prf_key = wallet.get_prf_key(app.ip_index, idx)?; - let reg_id = prf_key.prf(crypto_params.elgamal_generator(), acc_idx)?; - let address = account_address_from_registration_id(®_id); - match concordium_client - .get_account_info(&address.into(), BlockIdentifier::LastFinal) - .await - { - Ok(_) => println!("Account with address {address} found."), - Err(e) if e.is_not_found() => { - acc_fail_count += 1; - if acc_fail_count > 5 { - break; - } - } - Err(e) => anyhow::bail!("Cannot query the node: {e}"), - } - } + .await; + + if result.is_ok() { failure_count = 0; + success_count += 1; } else { failure_count += 1; } - if failure_count > 5 { + if failure_count > MAX_IDENTITY_FAILURES { break; } } + + if success_count == 0 { + Err(anyhow::anyhow!("Failed to find an identity for wallet.")) + } else { + Ok(()) + } +} + +async fn recover_from_secrets( + concordium_client: &mut v2::Client, + client: &reqwest::Client, + id: &WpIpInfos, + crypto_params: &GlobalContext, + prf_key: PrfKey, + id_cred_sec: PedersenValue, + id_index: u32, +) -> anyhow::Result<()> { + let request = generate_id_recovery_request( + &id.ip_info, + crypto_params, + &id_cred_sec, + chrono::Utc::now().timestamp() as u64, + ) + .context("Unable to construct recovery request")?; + let response = client + .get(id.metadata.recovery_start.clone()) + .query(&[( + "state", + serde_json::to_string(&RecoveryRequestData { + id_recovery_request: Versioned::new(VERSION_0, request), + }) + .unwrap(), + )]) + .send() + .await?; + + if !response.status().is_success() { + bail!("Recovery request failed: {}", response.text().await?); + } + + let id_object: Versioned> = + response.json().await?; + std::fs::write( + format!("{}-{id_index}.json", id.ip_info.ip_identity.0), + serde_json::to_string_pretty(&serde_json::json!({ + "identityIndex": id_index, + "ipInfo": &id.ip_info, + "idObject": id_object.value + }))?, + )?; + println!("Got identity object for index {id_index}."); + + // Print all accounts for this identity. + let mut acc_fail_count = 0; + for acc_idx in 0u8..=id_object.value.alist.max_accounts { + let reg_id = prf_key.prf(crypto_params.elgamal_generator(), acc_idx)?; + let address = AccountIdentifier::CredId(CredentialRegistrationID::new(reg_id)); + match concordium_client + .get_account_info(&address, BlockIdentifier::LastFinal) + .await + { + Ok(info) => println!( + "Account with address {} found.", + info.response.account_address + ), + Err(e) if e.is_not_found() => { + acc_fail_count += 1; + if acc_fail_count > MAX_ACCOUNT_FAILURES { + break; + } + } + Err(e) => anyhow::bail!("Cannot query the node: {e}"), + } + } + Ok(()) }