Skip to content

Commit

Permalink
Add invite_shamir_recovery_reveal command
Browse files Browse the repository at this point in the history
  • Loading branch information
vxgmichel committed Nov 28, 2024
1 parent da99dd4 commit 1a1118b
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[
{
"major_versions": [
4
],
"req": {
"cmd": "invite_shamir_recovery_reveal",
"fields": [
{
"name": "reveal_token",
"type": "InvitationToken"
}
]
},
"reps": [
{
"status": "ok",
"fields": [
{
"name": "ciphered_data",
"type": "Bytes"
}
]
},
{
"status": "not_found"
}
]
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
#![allow(clippy::unwrap_used)]

use super::invited_cmds;
use libparsec_tests_lite::{hex, p_assert_eq};
use libparsec_types::InvitationToken;

pub fn rep_not_found() {
// Generated from Parsec 3.2.1-a.0+dev
// Content:
// status: 'not_found'
let raw: &[u8] = hex!("81a6737461747573a96e6f745f666f756e64").as_ref();

let expected = invited_cmds::invite_shamir_recovery_reveal::Rep::NotFound;
println!("***expected: {:?}", expected.dump().unwrap());
let data = invited_cmds::invite_shamir_recovery_reveal::Rep::load(raw).unwrap();
p_assert_eq!(data, expected);
}

pub fn rep_ok() {
// Generated from Parsec 3.2.1-a.0+dev
// Content:
// status: 'ok'
// ciphered_data: 0x6369706865726564
let raw: &[u8] =
hex!("82a6737461747573a26f6bad63697068657265645f64617461c4086369706865726564").as_ref();

let expected = invited_cmds::invite_shamir_recovery_reveal::InviteShamirRecoveryRevealRep::Ok {
ciphered_data: "ciphered".into(),
};
println!("***expected: {:?}", expected.dump().unwrap());
let data = invited_cmds::invite_shamir_recovery_reveal::Rep::load(raw).unwrap();
p_assert_eq!(data, expected);
}
pub fn req() {
// Generated from Parsec 3.2.1-a.0+dev
// Content:
// cmd: 'invite_shamir_recovery_reveal'
// reveal_token: 0xd864b93ded264aae9ae583fd3d40c45a
let raw: &[u8] = hex!(
"82a3636d64bd696e766974655f7368616d69725f7265636f766572795f72657665616c"
"ac72657665616c5f746f6b656ec410d864b93ded264aae9ae583fd3d40c45a"
)
.as_ref();

let req = invited_cmds::invite_shamir_recovery_reveal::InviteShamirRecoveryRevealReq {
reveal_token: InvitationToken::from_hex("d864b93ded264aae9ae583fd3d40c45a").unwrap(),
};
println!("***expected: {:?}", req.dump().unwrap());

let expected = invited_cmds::AnyCmdReq::InviteShamirRecoveryReveal(req);

let data = invited_cmds::AnyCmdReq::load(raw).unwrap();
p_assert_eq!(data, expected);
}
1 change: 1 addition & 0 deletions misc/gen_protocol_typings.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def cook_field_type(
("Size", "int"),
("Index", "int"),
("NonZeroInteger", "int"),
("NonZeroU8", "int"),
("IntegerBetween1And100", "int"),
]:
if raw_type == candidate:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ from . import (
invite_claimer_start_greeting_attempt,
invite_claimer_step,
invite_info,
invite_shamir_recovery_reveal,
ping,
)

Expand All @@ -21,6 +22,7 @@ class AnyCmdReq:
| invite_claimer_start_greeting_attempt.Req
| invite_claimer_step.Req
| invite_info.Req
| invite_shamir_recovery_reveal.Req
| ping.Req
): ...

Expand All @@ -30,5 +32,6 @@ __all__ = [
"invite_claimer_start_greeting_attempt",
"invite_claimer_step",
"invite_info",
"invite_shamir_recovery_reveal",
"ping",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

# /!\ Autogenerated by misc/gen_protocol_typings.py, any modification will be lost !

from __future__ import annotations

from parsec._parsec import InvitationToken

class Req:
def __init__(self, reveal_token: InvitationToken) -> None: ...
def dump(self) -> bytes: ...
@property
def reveal_token(self) -> InvitationToken: ...

class Rep:
@staticmethod
def load(raw: bytes) -> Rep: ...
def dump(self) -> bytes: ...

class RepUnknownStatus(Rep):
def __init__(self, status: str, reason: str | None) -> None: ...
@property
def status(self) -> str: ...
@property
def reason(self) -> str | None: ...

class RepOk(Rep):
def __init__(self, ciphered_data: bytes) -> None: ...
@property
def ciphered_data(self) -> bytes: ...

class RepNotFound(Rep):
def __init__(
self,
) -> None: ...
43 changes: 43 additions & 0 deletions server/parsec/components/invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,14 @@ class InviteAsInvitedInfoBadOutcome(BadOutcomeEnum):
INVITATION_DELETED = auto()


class InviteShamirRecoveryRevealBadOutcome(BadOutcomeEnum):
ORGANIZATION_NOT_FOUND = auto()
ORGANIZATION_EXPIRED = auto()
INVITATION_NOT_FOUND = auto()
INVITATION_DELETED = auto()
DATA_NOT_FOUND = auto()


# New transport definitions
# TODO: Remove the old ones once fully migrated

Expand Down Expand Up @@ -1354,3 +1362,38 @@ async def complete(
token: InvitationToken,
) -> None | InviteCompleteBadOutcome:
raise NotImplementedError

@api
async def api_invite_shamir_recovery_reveal(
self,
client_ctx: InvitedClientContext,
req: invited_cmds.latest.invite_shamir_recovery_reveal.Req,
) -> invited_cmds.latest.invite_shamir_recovery_reveal.Rep:
outcome = await self.shamir_recovery_reveal(
organization_id=client_ctx.organization_id,
token=client_ctx.token,
reveal_token=req.reveal_token,
)
match outcome:
case Buffer() as ciphered_data:
return invited_cmds.latest.invite_shamir_recovery_reveal.RepOk(
ciphered_data=bytes(ciphered_data)
)
case InviteShamirRecoveryRevealBadOutcome.DATA_NOT_FOUND:
return invited_cmds.latest.invite_shamir_recovery_reveal.RepNotFound()
case InviteShamirRecoveryRevealBadOutcome.ORGANIZATION_NOT_FOUND:
client_ctx.organization_not_found_abort()
case InviteShamirRecoveryRevealBadOutcome.ORGANIZATION_EXPIRED:
client_ctx.organization_expired_abort()
case InviteShamirRecoveryRevealBadOutcome.INVITATION_NOT_FOUND:
return client_ctx.invitation_not_found_abort()
case InviteShamirRecoveryRevealBadOutcome.INVITATION_DELETED:
return client_ctx.invitation_already_used_or_deleted_abort()

async def shamir_recovery_reveal(
self,
organization_id: OrganizationID,
token: InvitationToken,
reveal_token: InvitationToken,
) -> bytes | InviteShamirRecoveryRevealBadOutcome:
raise NotImplementedError
37 changes: 36 additions & 1 deletion server/parsec/components/memory/invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
InviteNewForDeviceBadOutcome,
InviteNewForShamirBadOutcome,
InviteNewForUserBadOutcome,
InviteShamirRecoveryRevealBadOutcome,
NotReady,
SendEmailBadOutcome,
ShamirRecoveryInvitation,
Expand Down Expand Up @@ -566,6 +567,40 @@ async def info_as_invited(
case unknown:
assert False, unknown

@override
async def shamir_recovery_reveal(
self,
organization_id: OrganizationID,
token: InvitationToken,
reveal_token: InvitationToken,
) -> bytes | InviteShamirRecoveryRevealBadOutcome:
try:
org = self._data.organizations[organization_id]
except KeyError:
return InviteShamirRecoveryRevealBadOutcome.ORGANIZATION_NOT_FOUND
if org.is_expired:
return InviteShamirRecoveryRevealBadOutcome.ORGANIZATION_EXPIRED

try:
invitation = org.invitations[token]
except KeyError:
return InviteShamirRecoveryRevealBadOutcome.INVITATION_NOT_FOUND
if invitation.is_deleted:
return InviteShamirRecoveryRevealBadOutcome.INVITATION_DELETED

if invitation.claimer_user_id is None:
return InviteShamirRecoveryRevealBadOutcome.DATA_NOT_FOUND

shamir_recoveries = org.shamir_recoveries.get(invitation.claimer_user_id, [])
if not shamir_recoveries:
return InviteShamirRecoveryRevealBadOutcome.DATA_NOT_FOUND

*_, shamir_recovery = shamir_recoveries
if shamir_recovery.reveal_token != reveal_token:
return InviteShamirRecoveryRevealBadOutcome.DATA_NOT_FOUND

return shamir_recovery.ciphered_data

@override
async def test_dump_all_invitations(
self, organization_id: OrganizationID
Expand Down Expand Up @@ -629,7 +664,7 @@ def is_greeter_allowed(
shamir_recoveries = org.shamir_recoveries.get(invitation.claimer_user_id)
if not shamir_recoveries:
return False
shamir_recovery, *_ = shamir_recoveries
*_, shamir_recovery = shamir_recoveries
return shamir_recovery.shares.get(greeter.cooked.user_id) is not None
else:
assert False, invitation.type
Expand Down
1 change: 1 addition & 0 deletions server/tests/api_v4/invited/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
from .test_invite_claimer_start_greeting_attempt import * # noqa
from .test_invite_claimer_cancel_greeting_attempt import * # noqa
from .test_invite_claimer_step import * # noqa
from .test_invite_shamir_recovery_reveal import * # noqa
31 changes: 31 additions & 0 deletions server/tests/api_v4/invited/test_invite_shamir_recovery_reveal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS


from parsec._parsec import InvitationToken, invited_cmds
from tests.common import CoolorgRpcClients, HttpCommonErrorsTester, ShamirOrgRpcClients


async def test_invited_invite_shamir_recovery_reveal_ok(shamirorg: ShamirOrgRpcClients) -> None:
token = shamirorg.alice_shamir_reveal_token
ciphered_data = shamirorg.alice_shamir_ciphered_data
rep = await shamirorg.shamir_invited_alice.invite_shamir_recovery_reveal(reveal_token=token)
assert rep == invited_cmds.v4.invite_shamir_recovery_reveal.RepOk(ciphered_data=ciphered_data)


async def test_invited_invite_shamir_recovery_reveal_not_found(
shamirorg: ShamirOrgRpcClients,
) -> None:
token = InvitationToken.new()
rep = await shamirorg.shamir_invited_alice.invite_shamir_recovery_reveal(token)
assert rep == invited_cmds.v4.invite_shamir_recovery_reveal.RepNotFound()


async def test_invited_invite_shamir_recovery_reveal_http_common_errors(
coolorg: CoolorgRpcClients, invited_http_common_errors_tester: HttpCommonErrorsTester
) -> None:
token = InvitationToken.new()

async def do():
await coolorg.invited_alice_dev3.invite_shamir_recovery_reveal(reveal_token=token)

await invited_http_common_errors_tester(do)
46 changes: 46 additions & 0 deletions server/tests/common/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,38 @@ def mallory_shamir_topic_timestamp(self) -> DateTime:
def mike_shamir_topic_timestamp(self) -> DateTime:
return self._last_shamir_topic_timestamp_for("mike")

@property
def alice_shamir_reveal_token(self) -> InvitationToken:
return self._shamir_reveal_token_for("alice")

@property
def bob_shamir_reveal_token(self) -> InvitationToken:
return self._shamir_reveal_token_for("bob")

@property
def mallory_shamir_reveal_token(self) -> InvitationToken:
return self._shamir_reveal_token_for("mallory")

@property
def mike_shamir_reveal_token(self) -> InvitationToken:
return self._shamir_reveal_token_for("mike")

@property
def alice_shamir_ciphered_data(self) -> bytes:
return self._shamir_ciphered_data_for("alice")

@property
def bob_shamir_ciphered_data(self) -> bytes:
return self._shamir_ciphered_data_for("bob")

@property
def mallory_shamir_ciphered_data(self) -> bytes:
return self._shamir_ciphered_data_for("mallory")

@property
def mike_shamir_ciphered_data(self) -> bytes:
return self._shamir_ciphered_data_for("mike")

def _last_shamir_topic_timestamp_for(self, user: str) -> DateTime:
user_id = UserID.test_from_nickname(user)
for event in reversed(self.testbed_template.events):
Expand Down Expand Up @@ -631,6 +663,20 @@ def _shares_certificates_for(self, user: str) -> list[ShamirRecoveryShareCertifi
return event.shares_certificates
raise RuntimeError(f"New shamir recovery event not found for user `{user}` !")

def _shamir_reveal_token_for(self, user: str) -> InvitationToken:
user_id = UserID.test_from_nickname(user)
for event in self.testbed_template.events:
if isinstance(event, tb.TestbedEventNewShamirRecovery) and event.user_id == user_id:
return event.reveal_token
raise RuntimeError(f"New shamir recovery event not found for user `{user}` !")

def _shamir_ciphered_data_for(self, user: str) -> bytes:
user_id = UserID.test_from_nickname(user)
for event in self.testbed_template.events:
if isinstance(event, tb.TestbedEventNewShamirRecovery) and event.user_id == user_id:
return event.ciphered_data
raise RuntimeError(f"New shamir recovery event not found for user `{user}` !")

@property
def root_verify_key(self) -> VerifyKey:
return self.root_signing_key.verify_key
Expand Down
7 changes: 7 additions & 0 deletions server/tests/common/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,13 @@ async def invite_info(
raw_rep = await self._do_request(req.dump(), "invited")
return invited_cmds.latest.invite_info.Rep.load(raw_rep)

async def invite_shamir_recovery_reveal(
self, reveal_token: InvitationToken
) -> invited_cmds.latest.invite_shamir_recovery_reveal.Rep:
req = invited_cmds.latest.invite_shamir_recovery_reveal.Req(reveal_token=reveal_token)
raw_rep = await self._do_request(req.dump(), "invited")
return invited_cmds.latest.invite_shamir_recovery_reveal.Rep.load(raw_rep)

async def ping(self, ping: str) -> invited_cmds.latest.ping.Rep:
req = invited_cmds.latest.ping.Req(ping=ping)
raw_rep = await self._do_request(req.dump(), "invited")
Expand Down

0 comments on commit 1a1118b

Please sign in to comment.