From 5b5084fb632c993567c90306968a724699f6a817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lasse=20M=C3=B8ldrup?= Date: Wed, 22 Nov 2023 17:12:13 +0100 Subject: [PATCH] Implement recovery from generated secrets --- recover-id-object/Cargo.lock | 1 + recover-id-object/Cargo.toml | 7 +- recover-id-object/README.md | 24 ++- recover-id-object/src/main.rs | 338 +++++++++++++++++++++++++++------- 4 files changed, 300 insertions(+), 70 deletions(-) diff --git a/recover-id-object/Cargo.lock b/recover-id-object/Cargo.lock index 7fdd2b14..d6dd4d69 100644 --- a/recover-id-object/Cargo.lock +++ b/recover-id-object/Cargo.lock @@ -2063,6 +2063,7 @@ dependencies = [ "chrono", "clap", "concordium-rust-sdk", + "concordium_base", "http", "key_derivation", "reqwest", diff --git a/recover-id-object/Cargo.toml b/recover-id-object/Cargo.toml index a9dc4eab..2e86cb20 100644 --- a/recover-id-object/Cargo.toml +++ b/recover-id-object/Cargo.toml @@ -8,11 +8,12 @@ edition = "2021" [dependencies] clap = { version = "4", features = ["env", "derive"] } anyhow = "1" -tokio = {version = "1.20", features = ["rt-multi-thread", "macros"]} -tonic = {version = "0.8", features = ["tls", "tls-roots", "tls-webpki-roots"]} +tokio = { version = "1.20", features = ["rt-multi-thread", "macros"] } +tonic = { version = "0.8", features = ["tls", "tls-roots", "tls-webpki-roots"] } thiserror = "1" concordium-rust-sdk = { path = "../deps/concordium-rust-sdk", version = "*" } +concordium_base = { path = "../deps/concordium-rust-sdk/concordium-base/rust-src/concordium_base", version = "*" } key_derivation = { path = "../deps/concordium-rust-sdk/concordium-base/rust-src/key_derivation/", version = "*" } serde_json = "1.0.96" serde = "1.0.162" @@ -20,5 +21,3 @@ reqwest = { version = "0.11.17", features = ["json"] } url = { version = "2.3.1", features = ["serde"] } http = "0.2.9" chrono = "0.4.24" - - diff --git a/recover-id-object/README.md b/recover-id-object/README.md index 43011ca6..d08eef13 100644 --- a/recover-id-object/README.md +++ b/recover-id-object/README.md @@ -9,5 +9,25 @@ 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. +It outputs the secrets to standard output. + +### Recover identity + +`recover-identity` recovers the identity objects based on either a seed phrase or the secrets generated by `generate-secrets`. + +To recover with a seed phrase, run: + +```bash +recover-id-object --ip-index ... (* other args *) recover-identity --concordium-wallet +``` + +To recover with the secrets generated by `generate-secrets`, run: + +```bash +recover-id-object --ip-index ... (* other args *) recover-identity --prf-key --id-cred-sec --id-index +``` diff --git a/recover-id-object/src/main.rs b/recover-id-object/src/main.rs index 2c7b3b71..70f4bd5a 100644 --- a/recover-id-object/src/main.rs +++ b/recover-id-object/src/main.rs @@ -1,18 +1,24 @@ -use anyhow::Context; -use clap::Parser; +use anyhow::{bail, Context}; +use clap::{Args, Parser, Subcommand}; use concordium::{ common::{Versioned, VERSION_0}, id::{ account_holder::generate_id_recovery_request, constants::{ArCurve, AttributeKind, IpPairing}, + pedersen_commitment::Value as PedersenValue, types::{ account_address_from_registration_id, IdRecoveryRequest, IdentityObjectV1, IpInfo, }, }, + v2, v2::BlockIdentifier, }; +use concordium_base::{ + common::{base16_decode_string, base16_encode_string}, + id::types::GlobalContext, +}; use concordium_rust_sdk as concordium; -use key_derivation::ConcordiumHdWallet; +use key_derivation::{ConcordiumHdWallet, PrfKey}; use tonic::transport::ClientTlsConfig; #[derive(Parser, Debug)] @@ -32,25 +38,71 @@ struct Api { default_value = "10" )] concordium_request_timeout: u64, + #[clap(long = "ip-index", help = "Identity of the identity provider.")] + ip_index: u32, + #[clap(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + GenerateSecrets(GenerateSecretsArgs), + 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, +} + +#[derive(Debug, Args)] +// #[clap(group = ArgGroup::new("recovery-secrets").multiple(true))] +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, - #[clap(long = "ip-index", help = "Identity of the identity provider.")] - ip_index: u32, + 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, + 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 +127,104 @@ 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, app.ip_index, args).await + } + Command::RecoverIdentity(args) => { + recover_identity(concordium_client, app.ip_index, 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, + ip_index: u32, + 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 mut id_index = 0; + let mut id_fail_count = 0; + 'id_loop: loop { + let mut acc_fail_count = 0; + for acc_index in 0u8..=255 { + let address = { + // 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.")?; + account_address_from_registration_id(®_id) + }; + match concordium_client + .get_account_info(&address.into(), v2::BlockIdentifier::LastFinal) + .await + { + Ok(_) => break 'id_loop, + Err(e) if e.is_not_found() => { + acc_fail_count += 1; + if acc_fail_count > MAX_ACCOUNT_FAILURES { + id_fail_count += 1; + if id_fail_count > MAX_IDENTITY_FAILURES { + bail!("Failed to find an identity."); + } + break; + } + } + Err(e) => bail!("Cannot query the node: {e}"), + } + } + id_index += 1; + } + println!("Found account at identity index {id_index}."); + 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, + ip_index: u32, + 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 == ip_index.into()) else { + anyhow::bail!("Identity provider not found."); }; println!("Using identity provider {}", id.ip_info.ip_description.name); @@ -97,71 +232,146 @@ 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")?; + let success = recover_from_secrets( + &mut concordium_client, + &client, + &id, + &crypto_params, + prf_key, + id_cred_sec, + id_index, + ) + .await?; + if success { + Ok(()) + } else { + Err(anyhow::anyhow!("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); + let mut failure_count = 0; - for idx in 0.. { - let request = generate_id_recovery_request( - &id.ip_info, + 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 success = 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 success { failure_count = 0; } else { failure_count += 1; } - if failure_count > 5 { + if failure_count > MAX_IDENTITY_FAILURES { break; } } 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() { + return Ok(false); + } + + let id_object = 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}."); + + 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 = 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 > MAX_ACCOUNT_FAILURES { + break; + } + } + Err(e) => anyhow::bail!("Cannot query the node: {e}"), + } + } + + Ok(true) +} + #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] struct RecoveryRequestData {