Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CLI] shared recovery invitation #9232

Merged
merged 5 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 163 additions & 3 deletions cli/src/commands/invite/claim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use std::{path::PathBuf, sync::Arc};

use anyhow::anyhow;
use libparsec::{
internal::{
claimer_retrieve_info, AnyClaimRetrievedInfoCtx, DeviceClaimFinalizeCtx,
Expand All @@ -11,8 +12,16 @@ use libparsec::{
},
ClientConfig, DeviceAccessStrategy, ParsecInvitationAddr,
};
use libparsec_client::{
ShamirRecoveryClaimInProgress1Ctx, ShamirRecoveryClaimInProgress2Ctx,
ShamirRecoveryClaimInProgress3Ctx, ShamirRecoveryClaimInitialCtx,
ShamirRecoveryClaimMaybeFinalizeCtx, ShamirRecoveryClaimMaybeRecoverDeviceCtx,
ShamirRecoveryClaimPickRecipientCtx, ShamirRecoveryClaimRecoverDeviceCtx,
ShamirRecoveryClaimShare,
};

use crate::utils::*;
use dialoguer::{Input, Select};

crate::clap_parser_with_shared_opts_builder!(
#[with = config_dir, data_dir, password_stdin]
Expand Down Expand Up @@ -69,9 +78,61 @@ pub async fn main(args: Args) -> anyhow::Result<()> {
let ctx = step4_device(ctx).await?;
save_device(ctx, save_mode).await
}
AnyClaimRetrievedInfoCtx::ShamirRecovery(_) => Err(anyhow::anyhow!(
"Shamir recovery invitation is not supported yet"
)),
AnyClaimRetrievedInfoCtx::ShamirRecovery(ctx) => {
let mut pick_ctx = ctx;

let mut device_ctx = loop {
FirelightFlagboy marked this conversation as resolved.
Show resolved Hide resolved
let ctx = shamir_pick_recipient(&pick_ctx)?;
let ctx = step1_shamir(ctx).await?;
let ctx = step2_shamir(ctx).await?;
let ctx = step3_shamir(ctx).await?;
let share_ctx = step4_shamir(ctx).await?;
let maybe = pick_ctx.add_share(share_ctx)?;
FirelightFlagboy marked this conversation as resolved.
Show resolved Hide resolved
match maybe {
ShamirRecoveryClaimMaybeRecoverDeviceCtx::RecoverDevice(
shamir_recovery_claim_recover_device_ctx,
) => {
break shamir_recovery_claim_recover_device_ctx;
}
// need more shares
ShamirRecoveryClaimMaybeRecoverDeviceCtx::PickRecipient(
shamir_recovery_claim_pick_recipient_ctx,
) => pick_ctx = shamir_recovery_claim_pick_recipient_ctx,
}
};

let final_ctx = loop {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the server is offline and the user keep retrying it could go forever :/

let ctx = step5_shamir(device_ctx).await?;
match ctx {
ShamirRecoveryClaimMaybeFinalizeCtx::Offline(ctx) => {
let retry = Select::new()
.default(0)
.with_prompt("Unable to join server, do you want to retry ?")
.items(&["yes", "no"])
.interact()?;

if retry == 0 {
// yes
device_ctx = ctx;
FirelightFlagboy marked this conversation as resolved.
Show resolved Hide resolved
continue;
} else {
// no
return Err(anyhow!("Server offline, try again later."));
}
}
ShamirRecoveryClaimMaybeFinalizeCtx::Finalize(
shamir_recovery_claim_finalize_ctx,
) => {
break shamir_recovery_claim_finalize_ctx;
}
}
};
let key_file = final_ctx.get_default_key_file();
let access_strategy = get_access_strategy(key_file, save_mode)?;
final_ctx.save_local_device(&access_strategy).await?;

Ok(())
}
}
}

Expand All @@ -89,6 +150,20 @@ async fn step0(
Ok(ctx)
}

/// Step 0.5: choose recipient
fn shamir_pick_recipient(
ctx: &ShamirRecoveryClaimPickRecipientCtx,
) -> anyhow::Result<ShamirRecoveryClaimInitialCtx> {
let recipients = ctx.recipients_without_a_share();
let human_recipients: Vec<&_> = recipients.iter().map(|r| &r.human_handle).collect();
let selection = Select::new()
.default(0)
.with_prompt("Choose a person to contact now")
.items(&human_recipients)
.interact()?;
Comment on lines +159 to +163
Copy link
Contributor

@vxgmichel vxgmichel Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we ask the user to pick the next recipient but we do not display the current progress (like 2 out of 3 shares already retrieved). Also, we don't show how the number of shares owned by each recipient.

For reference, see the v2 interaction:

async def _choose_recipient(
prelude_ctx: ShamirRecoveryClaimPreludeCtx,
) -> ShamirRecoveryRecipient:
styled_current_shares = click.style(len(prelude_ctx.shares), fg="yellow")
styled_threshold = click.style(prelude_ctx.threshold, fg="yellow")
if len(prelude_ctx.shares) == 0:
click.echo(f"A total of {styled_threshold} shares are necessary to recover the device.")
elif len(prelude_ctx.shares) == 1:
click.echo(
f"Out of the {styled_threshold} necessary shares, {styled_current_shares} share has been retrieved so far."
)
else:
click.echo(
f"Out of the {styled_threshold} necessary shares, {styled_current_shares} shares have been retrieved so far."
)
for i, recipient in enumerate(prelude_ctx.remaining_recipients):
assert recipient.human_handle is not None
styled_human_handle = click.style(recipient.human_handle.str, fg="yellow")
styled_share_number = click.style(recipient.shares, fg="yellow")
styled_share_number = (
f"{styled_share_number} share"
if recipient.shares == 1
else f"{styled_share_number} shares"
)
click.echo(f" {i} - {styled_human_handle} ({styled_share_number})")
choices = [str(x) for x in range(len(prelude_ctx.recipients))]
choice_index = await async_prompt("Next user to contact", type=click.Choice(choices))
return prelude_ctx.recipients[int(choice_index)]

Ok(ctx.pick_recipient(recipients[selection].user_id)?)
}

/// Step 1: wait peer
async fn step1_user(ctx: UserClaimInitialCtx) -> anyhow::Result<UserClaimInProgress1Ctx> {
println!(
Expand Down Expand Up @@ -121,6 +196,27 @@ async fn step1_device(ctx: DeviceClaimInitialCtx) -> anyhow::Result<DeviceClaimI
Ok(ctx)
}

/// Step 1: wait peer
async fn step1_shamir(
ctx: ShamirRecoveryClaimInitialCtx,
) -> anyhow::Result<ShamirRecoveryClaimInProgress1Ctx> {
println!(
"Invitation greeter: {YELLOW}{}{RESET}",
ctx.greeter_human_handle()
);

let mut handle = start_spinner(format!(
"Waiting the greeter {} to start the invitation procedure",
ctx.greeter_human_handle()
));

let ctx = ctx.do_wait_peer().await?;

handle.stop_with_symbol(GREEN_CHECKMARK);

Ok(ctx)
}

/// Step 2: signify trust
async fn step2_user(ctx: UserClaimInProgress1Ctx) -> anyhow::Result<UserClaimInProgress2Ctx> {
let mut input = String::new();
Expand Down Expand Up @@ -153,6 +249,24 @@ async fn step2_device(ctx: DeviceClaimInProgress1Ctx) -> anyhow::Result<DeviceCl
Ok(ctx.do_signify_trust().await?)
}

/// Step 2: signify trust
async fn step2_shamir(
ctx: ShamirRecoveryClaimInProgress1Ctx,
) -> anyhow::Result<ShamirRecoveryClaimInProgress2Ctx> {
let sas_codes = ctx.generate_greeter_sas_choices(3);

let selected_sas = Select::new()
.default(0)
.items(&sas_codes)
.with_prompt("Select code provided by greeter")
.interact()?;
if &sas_codes[selected_sas] != ctx.greeter_sas() {
Err(anyhow!("Invalid SAS code"))
} else {
Ok(ctx.do_signify_trust().await?)
}
}

/// Step 3: wait peer trust
async fn step3_user(ctx: UserClaimInProgress2Ctx) -> anyhow::Result<UserClaimInProgress3Ctx> {
println!(
Expand All @@ -169,6 +283,24 @@ async fn step3_user(ctx: UserClaimInProgress2Ctx) -> anyhow::Result<UserClaimInP
Ok(ctx)
}

/// Step 3: wait peer trust
async fn step3_shamir(
ctx: ShamirRecoveryClaimInProgress2Ctx,
) -> anyhow::Result<ShamirRecoveryClaimInProgress3Ctx> {
println!(
"Code to provide to greeter: {YELLOW}{}{RESET}",
ctx.claimer_sas()
);

let mut handle = start_spinner("Waiting for greeter".into());

let ctx = ctx.do_wait_peer_trust().await?;

handle.stop_with_symbol(GREEN_CHECKMARK);

Ok(ctx)
}

/// Step 3: wait peer trust
async fn step3_device(ctx: DeviceClaimInProgress2Ctx) -> anyhow::Result<DeviceClaimInProgress3Ctx> {
println!(
Expand Down Expand Up @@ -214,6 +346,34 @@ async fn step4_device(ctx: DeviceClaimInProgress3Ctx) -> anyhow::Result<DeviceCl
Ok(ctx)
}

/// Step 4: retrieve device
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retrieve share

async fn step4_shamir(
ctx: ShamirRecoveryClaimInProgress3Ctx,
) -> anyhow::Result<ShamirRecoveryClaimShare> {
let mut handle = start_spinner("Waiting for greeter".into());

let ctx = ctx.do_recover_share().await?;

handle.stop_with_symbol(GREEN_CHECKMARK);

Ok(ctx)
}

/// Step 5: recover device
async fn step5_shamir(
ctx: ShamirRecoveryClaimRecoverDeviceCtx,
) -> anyhow::Result<ShamirRecoveryClaimMaybeFinalizeCtx> {
let device_label = Input::new().with_prompt("Enter device label").interact()?;

let mut handle = start_spinner("Recovering device".into());

let ctx = ctx.recover_device(device_label).await?;

handle.stop_with_symbol(GREEN_CHECKMARK);

Ok(ctx)
}

fn get_access_strategy(
key_file: PathBuf,
save_mode: SaveMode,
Expand Down
79 changes: 74 additions & 5 deletions cli/src/commands/invite/greet.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

use anyhow::Context;
use dialoguer::Select;
use libparsec::{
authenticated_cmds::latest::invite_list::InviteListItem,
internal::{
Expand All @@ -11,7 +12,10 @@ use libparsec::{
},
InvitationToken,
};
use libparsec_client::Client;
use libparsec_client::{
Client, ShamirRecoveryGreetInProgress1Ctx, ShamirRecoveryGreetInProgress2Ctx,
ShamirRecoveryGreetInProgress3Ctx, ShamirRecoveryGreetInitialCtx,
};

use crate::utils::*;

Expand All @@ -30,6 +34,12 @@ pub async fn device_greet(args: Args, client: &StartedClient) -> anyhow::Result<
let Args { token, .. } = args;
log::trace!("Greeting invitation");

{
let mut spinner = start_spinner("Poll server for new certificates".into());
client.poll_server_for_new_certificates().await?;
spinner.stop_with_symbol(GREEN_CHECKMARK);
}

let invitation = step0(client, token).await?;

match invitation {
Expand All @@ -45,15 +55,20 @@ pub async fn device_greet(args: Args, client: &StartedClient) -> anyhow::Result<
InviteListItem::Device { token, .. } => {
let ctx = client.start_device_invitation_greet(token);

let ctx = step1_device(ctx).await?;
let ctx: DeviceGreetInProgress1Ctx = step1_device(ctx).await?;
let ctx = step2_device(ctx).await?;
let ctx = step3_device(ctx).await?;
let ctx = step4_device(ctx).await?;
step5_device(ctx).await
}
InviteListItem::ShamirRecovery { .. } => Err(anyhow::anyhow!(
"Shamir recovery invitation is not supported yet"
)),
InviteListItem::ShamirRecovery { token, .. } => {
let ctx = client.start_shamir_recovery_invitation_greet(token).await?;

let ctx = step1_shamir(ctx).await?;
let ctx = step2_shamir(ctx).await?;
let ctx = step3_shamir(ctx).await?;
step4_shamir(ctx).await
}
}
}

Expand All @@ -69,6 +84,7 @@ async fn step0(
let invitation = match invitations.into_iter().find(|invitation| match invitation {
InviteListItem::User { token, .. } if *token == invitation_token => true,
InviteListItem::Device { token, .. } if *token == invitation_token => true,
InviteListItem::ShamirRecovery { token, .. } if *token == invitation_token => true,
_ => false,
}) {
Some(invitation) => invitation,
Expand Down Expand Up @@ -102,6 +118,19 @@ async fn step1_device(ctx: DeviceGreetInitialCtx) -> anyhow::Result<DeviceGreetI
Ok(ctx)
}

/// Step 1: wait peer
async fn step1_shamir(
ctx: ShamirRecoveryGreetInitialCtx,
) -> anyhow::Result<ShamirRecoveryGreetInProgress1Ctx> {
let mut handle = start_spinner("Waiting for claimer".into());

let ctx = ctx.do_wait_peer().await?;

handle.stop_with_symbol(GREEN_CHECKMARK);

Ok(ctx)
}

/// Step 2: wait peer trust
async fn step2_user(ctx: UserGreetInProgress1Ctx) -> anyhow::Result<UserGreetInProgress2Ctx> {
println!(
Expand Down Expand Up @@ -134,6 +163,24 @@ async fn step2_device(ctx: DeviceGreetInProgress1Ctx) -> anyhow::Result<DeviceGr
Ok(ctx)
}

/// Step 2: wait peer trust
async fn step2_shamir(
ctx: ShamirRecoveryGreetInProgress1Ctx,
) -> anyhow::Result<ShamirRecoveryGreetInProgress2Ctx> {
println!(
"Code to provide to claimer: {YELLOW}{}{RESET}",
ctx.greeter_sas()
);

let mut handle = start_spinner("Waiting for claimer".into());

let ctx = ctx.do_wait_peer_trust().await?;

handle.stop_with_symbol(GREEN_CHECKMARK);

Ok(ctx)
}

/// Step 3: signify trust
async fn step3_user(ctx: UserGreetInProgress2Ctx) -> anyhow::Result<UserGreetInProgress3Ctx> {
let mut input = String::new();
Expand Down Expand Up @@ -164,6 +211,23 @@ async fn step3_device(ctx: DeviceGreetInProgress2Ctx) -> anyhow::Result<DeviceGr
Ok(ctx.do_signify_trust().await?)
}

/// Step 3: signify trust
async fn step3_shamir(
ctx: ShamirRecoveryGreetInProgress2Ctx,
) -> anyhow::Result<ShamirRecoveryGreetInProgress3Ctx> {
let sas_codes = ctx.generate_claimer_sas_choices(3);
let selected_sas = Select::new()
.default(0)
.items(&sas_codes)
.with_prompt("Select code provided by claimer")
.interact()?;
if &sas_codes[selected_sas] != ctx.claimer_sas() {
Err(anyhow::anyhow!("Invalid SAS code"))
} else {
Ok(ctx.do_signify_trust().await?)
}
}

/// Step 4: get claim requests
async fn step4_user(ctx: UserGreetInProgress3Ctx) -> anyhow::Result<UserGreetInProgress4Ctx> {
Ok(ctx.do_get_claim_requests().await?)
Expand All @@ -174,6 +238,11 @@ async fn step4_device(ctx: DeviceGreetInProgress3Ctx) -> anyhow::Result<DeviceGr
Ok(ctx.do_get_claim_requests().await?)
}

/// Step 4: send shares
async fn step4_shamir(ctx: ShamirRecoveryGreetInProgress3Ctx) -> anyhow::Result<()> {
Ok(ctx.do_send_share().await?)
}

/// Step 5: create new user
async fn step5_user(ctx: UserGreetInProgress4Ctx) -> anyhow::Result<()> {
let mut input = String::new();
Expand Down
7 changes: 6 additions & 1 deletion cli/src/commands/invite/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ crate::build_main_with_client!(main, list_invite);

pub async fn list_invite(_args: Args, client: &StartedClient) -> anyhow::Result<()> {
log::trace!("Listing invitations");
{
let mut spinner = start_spinner("Poll server for new certificates".into());
client.poll_server_for_new_certificates().await?;
spinner.stop_with_symbol(GREEN_CHECKMARK);
}
FirelightFlagboy marked this conversation as resolved.
Show resolved Hide resolved

let mut handle = start_spinner("Listing invitations".into());

Expand All @@ -31,7 +36,7 @@ pub async fn list_invite(_args: Args, client: &StartedClient) -> anyhow::Result<
status,
token,
..
} => (token, status, format!("user (email={claimer_email}")),
} => (token, status, format!("user (email={claimer_email})")),
FirelightFlagboy marked this conversation as resolved.
Show resolved Hide resolved
InviteListItem::Device { status, token, .. } => (token, status, "device".into()),
InviteListItem::ShamirRecovery {
status,
Expand Down
Loading
Loading