def generate_ALICE_local_device(): return LocalDevice( organization_addr=BackendOrganizationAddr.from_url( "parsec://alice_dev1.example.com:9999/CoolOrg?no_ssl=true&rvk=XYUXM4ZM5SGKSTXNZ4FK7VATZUKZGY7A7LOJ42CXFR32DYL5TO6Qssss" ), device_id=DeviceID("alice@dev1"), device_label=DeviceLabel("My dev1 machine"), human_handle=HumanHandle("*****@*****.**", "Alicey McAliceFace"), signing_key=SigningKey( unhexlify( "d544f66ece9c85d5b80275db9124b5f04bb038081622bed139c1e789c5217400" )), private_key=PrivateKey( unhexlify( "74e860967fd90d063ebd64fb1ba6824c4c010099dd37508b7f2875a5db2ef8c9" )), profile=UserProfile.ADMIN, user_manifest_id=EntryID.from_hex("a4031e8bcdd84df8ae12bd3d05e6e20f"), user_manifest_key=SecretKey( unhexlify( "26bf35a98c1e54e90215e154af92a1af2d1142cdd0dba25b990426b0b30b0f9a" )), local_symkey=SecretKey( unhexlify( "125a78618995e2e0f9a19bc8617083c809c03deb5457d5b82df5bcaec9966cd4" )), )
def generate_BOB_local_device(): return LocalDevice( organization_addr=BackendOrganizationAddr.from_url( "parsec://bob_dev1.example.com:9999/CoolOrg?no_ssl=true&rvk=XYUXM4ZM5SGKSTXNZ4FK7VATZUKZGY7A7LOJ42CXFR32DYL5TO6Qssss" ), device_id=DeviceID("bob@dev1"), device_label=DeviceLabel("My dev1 machine"), human_handle=HumanHandle("*****@*****.**", "Boby McBobFace"), signing_key=SigningKey( unhexlify( "85f47472a2c0f30f01b769617db248f3ec8d96a490602a9262f95e9e43432b30" )), private_key=PrivateKey( unhexlify( "16767ec446f2611f971c36f19c2dc11614d853475ac395d6c1d70ba46d07dd49" )), profile=UserProfile.STANDARD, user_manifest_id=EntryID.from_hex("71568d41afcb4e2380b3d164ace4fb85"), user_manifest_key=SecretKey( unhexlify( "65de53d2c6cd965aa53a1ba5cc7e54b331419e6103466121996fa99a97197a48" )), local_symkey=SecretKey( unhexlify( "93f25b18491016f20b10dcf4eb7986716d914653d6ab4e778701c13435e6bdf0" )), )
def test_local_device(): from parsec.core.types.local_device import _RsLocalDevice, LocalDevice, _PyLocalDevice assert LocalDevice is _RsLocalDevice def _assert_local_device_eq(py, rs): assert isinstance(py, _PyLocalDevice) assert isinstance(rs, _RsLocalDevice) assert py.organization_addr == rs.organization_addr assert py.device_id == rs.device_id assert py.device_label == rs.device_label assert py.human_handle == rs.human_handle assert py.signing_key == rs.signing_key assert py.private_key == rs.private_key assert py.profile == rs.profile assert py.user_manifest_id == rs.user_manifest_id assert py.user_manifest_key == rs.user_manifest_key assert py.local_symkey == rs.local_symkey assert py.is_admin == rs.is_admin assert py.is_outsider == rs.is_outsider assert py.slug == rs.slug assert py.slughash == rs.slughash assert py.root_verify_key == rs.root_verify_key assert py.organization_id == rs.organization_id assert py.device_name == rs.device_name assert py.user_id == rs.user_id assert py.verify_key == rs.verify_key assert py.public_key == rs.public_key assert py.user_display == rs.user_display assert py.short_user_display == rs.short_user_display assert py.device_display == rs.device_display signing_key = SigningKey.generate() kwargs = { "organization_addr": BackendOrganizationAddr.build( BackendAddr.from_url("parsec://foo"), organization_id=OrganizationID("org"), root_verify_key=signing_key.verify_key, ), "device_id": DeviceID.new(), "device_label": None, "human_handle": None, "signing_key": signing_key, "private_key": PrivateKey.generate(), "profile": UserProfile.ADMIN, "user_manifest_id": EntryID.new(), "user_manifest_key": SecretKey.generate(), "local_symkey": SecretKey.generate(), } py_ba = _PyLocalDevice(**kwargs) rs_ba = LocalDevice(**kwargs) _assert_local_device_eq(py_ba, rs_ba)
def generate_new_device(device_id: DeviceID, organization_addr: BackendOrganizationAddr, is_admin: bool = False) -> LocalDevice: return LocalDevice( organization_addr=organization_addr, device_id=device_id, signing_key=SigningKey.generate(), private_key=PrivateKey.generate(), is_admin=is_admin, user_manifest_id=EntryID(uuid4().hex), user_manifest_key=SecretKey.generate(), local_symkey=SecretKey.generate(), )
def decrypt_verify_and_load( cls: Type[BaseSignedDataTypeVar], encrypted: bytes, key: SecretKey, author_verify_key: VerifyKey, expected_author: DeviceID, expected_timestamp: DateTime, **kwargs, ) -> BaseSignedDataTypeVar: """ Raises: DataError """ try: signed = key.decrypt(encrypted) except CryptoError as exc: raise DataError(str(exc)) from exc return cls.verify_and_load( signed, author_verify_key=author_verify_key, expected_author=expected_author, expected_timestamp=expected_timestamp, **kwargs, )
def test_workspace_entry(): from parsec.api.data.manifest import _RsWorkspaceEntry, WorkspaceEntry, _PyWorkspaceEntry from parsec.api.data import EntryName assert WorkspaceEntry is _RsWorkspaceEntry def _assert_workspace_entry_eq(py, rs): assert isinstance(py, _PyWorkspaceEntry) assert isinstance(rs, _RsWorkspaceEntry) assert py.is_revoked() == rs.is_revoked() assert py.name == rs.name assert py.id == rs.id assert py.key == rs.key assert py.encryption_revision == rs.encryption_revision assert py.encrypted_on == rs.encrypted_on assert py.role_cached_on == rs.role_cached_on assert py.role == rs.role kwargs = { "name": EntryName("name"), "id": EntryID.new(), "key": SecretKey.generate(), "encryption_revision": 1, "encrypted_on": pendulum.now(), "role_cached_on": pendulum.now(), "role": RealmRole.OWNER, } py_we = _PyWorkspaceEntry(**kwargs) rs_we = WorkspaceEntry(**kwargs) _assert_workspace_entry_eq(py_we, rs_we) kwargs = { "name": EntryName("new_name"), "id": EntryID.new(), "key": SecretKey.generate(), "encryption_revision": 42, "encrypted_on": pendulum.now(), "role_cached_on": pendulum.now(), "role": None, } py_we = py_we.evolve(**kwargs) rs_we = rs_we.evolve(**kwargs) _assert_workspace_entry_eq(py_we, rs_we)
async def do_claim_device( self, requested_device_label: Optional[DeviceLabel]) -> LocalDevice: # Device key is generated here and kept in memory until the end of # the enrollment process. This mean we can lost it if something goes wrong. # This has no impact until step 4 (somewhere between data exchange and # confirmation exchange steps) where greeter upload our certificate in # the server. # This is considered acceptable given 1) the error window is small and # 2) if this occurs the inviter can revoke the device and retry the # enrollment process to fix this signing_key = SigningKey.generate() try: payload = InviteDeviceData( requested_device_label=requested_device_label, verify_key=signing_key.verify_key).dump_and_encrypt( key=self._shared_secret_key) except DataError as exc: raise InviteError( "Cannot generate InviteDeviceData payload") from exc rep = await self._cmds.invite_4_claimer_communicate(payload=payload) _check_rep(rep, step_name="step 4 (data exchange)") rep = await self._cmds.invite_4_claimer_communicate(payload=b"") _check_rep(rep, step_name="step 4 (confirmation exchange)") try: confirmation = InviteDeviceConfirmation.decrypt_and_load( rep["payload"], key=self._shared_secret_key) except DataError as exc: raise InviteError( "Invalid InviteDeviceConfirmation payload provided by peer" ) from exc organization_addr = BackendOrganizationAddr.build( backend_addr=self._cmds.addr.get_backend_addr(), organization_id=self._cmds.addr.organization_id, root_verify_key=confirmation.root_verify_key, ) return LocalDevice( organization_addr=organization_addr, device_id=confirmation.device_id, device_label=confirmation.device_label, human_handle=confirmation.human_handle, profile=confirmation.profile, private_key=confirmation.private_key, signing_key=signing_key, user_manifest_id=confirmation.user_manifest_id, user_manifest_key=confirmation.user_manifest_key, local_symkey=SecretKey.generate(), )
def dump_and_encrypt(self, key: SecretKey) -> bytes: """ Raises: DataError """ try: raw = self.dump() return key.encrypt(raw) except CryptoError as exc: raise DataError(str(exc)) from exc
def new(cls: Type[WorkspaceEntryTypeVar], name: str) -> "WorkspaceEntry": now = pendulum_now() return WorkspaceEntry( name=name, id=EntryID(), key=SecretKey.generate(), encryption_revision=1, encrypted_on=now, role_cached_on=now, role=RealmRole.OWNER, )
def dump_sign_and_encrypt(self, author_signkey: SigningKey, key: SecretKey) -> bytes: """ Raises: DataError """ try: signed = author_signkey.sign(self._serialize()) return key.encrypt(signed) except CryptoError as exc: raise DataError(str(exc)) from exc
def test_block_access(): from parsec.api.data.manifest import _RsBlockAccess, BlockAccess, _PyBlockAccess assert BlockAccess is _RsBlockAccess def _assert_block_access_eq(py, rs): assert isinstance(py, _PyBlockAccess) assert isinstance(rs, _RsBlockAccess) assert py.id == rs.id assert py.key == rs.key assert py.offset == rs.offset assert py.size == rs.size assert py.digest == rs.digest kwargs = { "id": BlockID.new(), "key": SecretKey.generate(), "offset": 0, "size": 1024, "digest": HashDigest.from_data(b"a"), } py_ba = _PyBlockAccess(**kwargs) rs_ba = BlockAccess(**kwargs) _assert_block_access_eq(py_ba, rs_ba) kwargs = { "id": BlockID.new(), "key": SecretKey.generate(), "offset": 64, "size": 2048, "digest": HashDigest.from_data(b"b"), } py_ba = py_ba.evolve(**kwargs) rs_ba = rs_ba.evolve(**kwargs) _assert_block_access_eq(py_ba, rs_ba) kwargs["size"] = 0 with pytest.raises(ValueError): BlockAccess(**kwargs)
def generate_new_device( organization_addr: BackendOrganizationAddr, device_id: Optional[DeviceID] = None, profile: UserProfile = UserProfile.STANDARD, human_handle: Optional[HumanHandle] = None, device_label: Optional[DeviceLabel] = None, signing_key: Optional[SigningKey] = None, private_key: Optional[PrivateKey] = None, ) -> LocalDevice: return LocalDevice( organization_addr=organization_addr, device_id=device_id or DeviceID.new(), device_label=device_label, human_handle=human_handle, signing_key=signing_key or SigningKey.generate(), private_key=private_key or PrivateKey.generate(), profile=profile, user_manifest_id=EntryID.new(), user_manifest_key=SecretKey.generate(), local_symkey=SecretKey.generate(), )
async def do_claim_device( self, requested_device_label: Optional[str]) -> LocalDevice: signing_key = SigningKey.generate() try: payload = InviteDeviceData( requested_device_label=requested_device_label, verify_key=signing_key.verify_key).dump_and_encrypt( key=self._shared_secret_key) except DataError as exc: raise InviteError( "Cannot generate InviteDeviceData payload") from exc rep = await self._cmds.invite_4_claimer_communicate(payload=payload) if rep["status"] == "invalid_state": raise InvitePeerResetError() elif rep["status"] != "ok": raise InviteError( f"Backend error during step 4 (data exchange): {rep}") rep = await self._cmds.invite_4_claimer_communicate(payload=b"") if rep["status"] == "invalid_state": raise InvitePeerResetError() elif rep["status"] != "ok": raise InviteError( f"Backend error during step 4 (confirmation exchange): {rep}") try: confirmation = InviteDeviceConfirmation.decrypt_and_load( rep["payload"], key=self._shared_secret_key) except DataError as exc: raise InviteError( "Invalid InviteDeviceConfirmation payload provided by peer" ) from exc organization_addr = BackendOrganizationAddr.build( backend_addr=self._cmds.addr, organization_id=self._cmds.addr.organization_id, root_verify_key=confirmation.root_verify_key, ) return LocalDevice( organization_addr=organization_addr, device_id=confirmation.device_id, device_label=confirmation.device_label, human_handle=confirmation.human_handle, profile=confirmation.profile, private_key=confirmation.private_key, signing_key=signing_key, user_manifest_id=confirmation.user_manifest_id, user_manifest_key=confirmation.user_manifest_key, local_symkey=SecretKey.generate(), )
def new(cls: Type[WorkspaceEntryTypeVar], name: EntryName, timestamp: DateTime) -> "WorkspaceEntry": assert isinstance(name, EntryName) return _PyWorkspaceEntry( name=name, id=EntryID.new(), key=SecretKey.generate(), encryption_revision=1, encrypted_on=timestamp, role_cached_on=timestamp, role=RealmRole.OWNER, )
def decrypt_and_load(cls, encrypted: bytes, key: SecretKey, **kwargs) -> "BaseData": """ Raises: DataError """ try: raw = key.decrypt(encrypted) except CryptoError as exc: raise DataError(str(exc)) from exc return cls.load(raw, **kwargs)
def decrypt_and_load(cls: Type[BaseDataTypeVar], encrypted: bytes, key: SecretKey, **kwargs: object) -> BaseDataTypeVar: """ Raises: DataError """ try: raw = key.decrypt(encrypted) except CryptoError as exc: raise DataError(str(exc)) from exc return cls.load(raw, **kwargs)
async def workspace_start_reencryption( self, workspace_id: EntryID) -> ReencryptionJob: """ Raises: FSError FSBackendOfflineError FSWorkspaceNoAccess FSWorkspaceNotFoundError """ user_manifest = self.get_user_manifest() workspace_entry = user_manifest.get_workspace_entry(workspace_id) if not workspace_entry: raise FSWorkspaceNotFoundError( f"Unknown workspace `{workspace_id}`") now = pendulum_now() new_workspace_entry = workspace_entry.evolve( encryption_revision=workspace_entry.encryption_revision + 1, encrypted_on=now, key=SecretKey.generate(), ) while True: # In order to provide the new key to each participant, we must # encrypt a message for each of them participants = await self._retrieve_participants(workspace_entry.id ) reencryption_msgs = self._generate_reencryption_messages( new_workspace_entry, participants, now) # Actually ask the backend to start the reencryption ok = await self._send_start_reencryption_cmd( workspace_entry.id, new_workspace_entry.encryption_revision, now, reencryption_msgs) if not ok: # Participant list has changed concurrently logger.info( "Realm participants list has changed during start reencryption tentative, retrying", workspace_id=workspace_id, ) continue else: break # Note we don't update the user manifest here, this will be done when # processing the `realm.updated` message from the backend return ReencryptionJob(self.backend_cmds, new_workspace_entry, workspace_entry)
async def _create_new_device_for_self( original_device: LocalDevice, new_device_label: DeviceLabel) -> LocalDevice: """ Raises: BackendConnectionError """ new_device = LocalDevice( organization_addr=original_device.organization_addr, device_id=DeviceID(f"{original_device.user_id}@{DeviceName.new()}"), device_label=new_device_label, human_handle=original_device.human_handle, profile=original_device.profile, private_key=original_device.private_key, signing_key=SigningKey.generate(), user_manifest_id=original_device.user_manifest_id, user_manifest_key=original_device.user_manifest_key, local_symkey=SecretKey.generate(), ) now = pendulum_now() device_certificate = DeviceCertificateContent( author=original_device.device_id, timestamp=now, device_id=new_device.device_id, device_label=new_device.device_label, verify_key=new_device.verify_key, ) redacted_device_certificate = device_certificate.evolve(device_label=None) device_certificate = device_certificate.dump_and_sign( original_device.signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( original_device.signing_key) async with backend_authenticated_cmds_factory( addr=original_device.organization_addr, device_id=original_device.device_id, signing_key=original_device.signing_key, ) as cmds: rep = await cmds.device_create( device_certificate=device_certificate, redacted_device_certificate=redacted_device_certificate, ) if rep["status"] != "ok": raise BackendConnectionError(f"Cannot create recovery device: {rep}") return new_device
def generate_sas_codes( claimer_nonce: bytes, greeter_nonce: bytes, shared_secret_key: SecretKey) -> Tuple[SASCode, SASCode]: # Computes combined HMAC combined_nonce = claimer_nonce + greeter_nonce # Digest size of 5 bytes so we can split it beween two 20bits SAS combined_hmac = shared_secret_key.hmac(combined_nonce, digest_size=5) hmac_as_int = int.from_bytes(combined_hmac, "big") # Big endian number extracted from bits [0, 20[ claimer_sas = hmac_as_int % 2**20 # Big endian number extracted from bits [20, 40[ greeter_sas = (hmac_as_int >> 20) % 2**20 return SASCode.from_int(claimer_sas), SASCode.from_int(greeter_sas)
def test_generate_sas_codes(): from parsec.api.data.invite import ( _Rs_generate_sas_codes, generate_sas_codes, _Py_generate_sas_codes, ) assert generate_sas_codes is _Rs_generate_sas_codes sk = SecretKey(b"a" * 32) py_claimer, py_greeter = _Py_generate_sas_codes(b"abcd", b"efgh", sk) rs_claimer, rs_greeter = generate_sas_codes(b"abcd", b"efgh", sk) assert py_claimer.str == rs_claimer.str assert py_greeter.str == rs_greeter.str
async def test_new_workspace(running_backend, alice, alice_user_fs, alice2_user_fs): with freeze_time("2000-01-02"): wid = await alice_user_fs.workspace_create(EntryName("w")) workspace = alice_user_fs.get_workspace(wid) with alice_user_fs.event_bus.listen() as spy: with freeze_time("2000-01-03"): await workspace.sync() spy.assert_events_occured([(CoreEvent.FS_ENTRY_SYNCED, { "workspace_id": wid, "id": wid }, datetime(2000, 1, 3))]) workspace2 = alice_user_fs.get_workspace(wid) await alice_user_fs.sync() await workspace2.sync() workspace_entry = workspace.get_workspace_entry() path_info = await workspace.path_info("/") assert path_info == { "type": "folder", "id": wid, "is_placeholder": False, "need_sync": False, "base_version": 1, "children": [], "created": datetime(2000, 1, 2), "updated": datetime(2000, 1, 2), "confinement_point": None, } KEY = SecretKey.generate() workspace_entry = workspace_entry.evolve(key=KEY) assert workspace_entry == WorkspaceEntry( name=EntryName("w"), id=wid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.OWNER, ) workspace_entry2 = workspace.get_workspace_entry() workspace_entry2 = workspace_entry2.evolve(key=KEY) path_info2 = await workspace.path_info("/") assert workspace_entry == workspace_entry2 assert path_info == path_info2
async def test_revoke_sharing_trigger_event(alice_core, bob_core, running_backend): KEY = SecretKey.generate() def _update_event(event): if event.event == CoreEvent.SHARING_UPDATED: event.kwargs["new_entry"] = event.kwargs["new_entry"].evolve( key=KEY, role_cached_on=datetime(2000, 1, 2) ) event.kwargs["previous_entry"] = event.kwargs["previous_entry"].evolve( key=KEY, role_cached_on=datetime(2000, 1, 2) ) return event with freeze_time("2000-01-02"): wid = await create_shared_workspace(EntryName("w"), alice_core, bob_core) with bob_core.event_bus.listen() as spy: with freeze_time("2000-01-03"): await alice_core.user_fs.workspace_share(wid, recipient=UserID("bob"), role=None) # Each workspace participant should get the message await spy.wait_with_timeout( CoreEvent.SHARING_UPDATED, { "new_entry": WorkspaceEntry( name=EntryName("w"), id=wid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=None, ), "previous_entry": WorkspaceEntry( name=EntryName("w"), id=wid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.MANAGER, ), }, update_event_func=_update_event, )
def evolve_as_block(self, data: bytes) -> "Chunk": # No-op if self.is_block: return self # Check alignement if self.raw_offset != self.start: raise TypeError("This chunk is not aligned") # Craft access access = BlockAccess( id=BlockID(self.id), key=SecretKey.generate(), offset=self.start, size=self.stop - self.start, digest=HashDigest.from_data(data), ) # Evolve return self.evolve(access=access)
async def test_new_sharing_trigger_event(alice_core, bob_core, running_backend): KEY = SecretKey.generate() # First, create a folder and sync it on backend with freeze_time("2000-01-01"): wid = await alice_core.user_fs.workspace_create(EntryName("foo")) workspace = alice_core.user_fs.get_workspace(wid) with freeze_time("2000-01-02"): await workspace.sync() # Now we can share this workspace with Bob with bob_core.event_bus.listen() as spy: with freeze_time("2000-01-03"): await alice_core.user_fs.workspace_share( wid, recipient=UserID("bob"), role=WorkspaceRole.MANAGER ) def _update_event(event): if event.event == CoreEvent.SHARING_UPDATED: event.kwargs["new_entry"] = event.kwargs["new_entry"].evolve( key=KEY, role_cached_on=datetime(2000, 1, 1) ) return event # Bob should get a notification await spy.wait_with_timeout( CoreEvent.SHARING_UPDATED, { "new_entry": WorkspaceEntry( name=EntryName("foo"), id=wid, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 1), role=WorkspaceRole.MANAGER, ), "previous_entry": None, }, update_event_func=_update_event, )
def test_invite_device_data(): from parsec.api.data.invite import _RsInviteDeviceData, InviteDeviceData, _PyInviteDeviceData assert InviteDeviceData is _RsInviteDeviceData dl = DeviceLabel("label") sk = SigningKey.generate() vk = sk.verify_key sek = SecretKey.generate() py_idd = _PyInviteDeviceData(requested_device_label=dl, verify_key=vk) rs_idd = InviteDeviceData(requested_device_label=dl, verify_key=vk) assert rs_idd.requested_device_label.str == py_idd.requested_device_label.str rs_encrypted = rs_idd.dump_and_encrypt(key=sek) py_encrypted = py_idd.dump_and_encrypt(key=sek) # Decrypt Rust-encrypted with Rust rs_idd2 = InviteDeviceData.decrypt_and_load(rs_encrypted, sek) assert rs_idd.requested_device_label.str == rs_idd2.requested_device_label.str # Decrypt Python-encrypted with Python rs_idd3 = InviteDeviceData.decrypt_and_load(py_encrypted, sek) assert rs_idd.requested_device_label.str == rs_idd3.requested_device_label.str # Decrypt Rust-encrypted with Python py_idd2 = _PyInviteDeviceData.decrypt_and_load(rs_encrypted, sek) assert rs_idd.requested_device_label.str == py_idd2.requested_device_label.str # With requested_human_handle and requested_device_label as None py_idd = _PyInviteDeviceData(requested_device_label=None, verify_key=vk) rs_idd = InviteDeviceData(requested_device_label=None, verify_key=vk) assert py_idd.requested_device_label is None assert rs_idd.requested_device_label is None
from pendulum import datetime from parsec.api.data import UserManifest, EntryID, EntryName from parsec.crypto import SecretKey from parsec.core.fs.remote_loader import MANIFEST_STAMP_AHEAD_US from parsec.core.types import ( WorkspaceEntry, WorkspaceRole, LocalUserManifest, LocalWorkspaceManifest, ) from parsec.core.fs import FSWorkspaceNotFoundError, FSBackendOfflineError from tests.common import freeze_time KEY = SecretKey.generate() def _update_user_manifest_key(um): return um.evolve( base=um.base.evolve(workspaces=tuple( w.evolve(key=KEY) for w in um.base.workspaces)), workspaces=tuple(w.evolve(key=KEY) for w in um.workspaces), ) @pytest.mark.trio async def test_get_manifest(alice_user_fs): um = alice_user_fs.get_user_manifest() assert um.base_version == 1 assert not um.need_sync
def test_invite_device_confirmation(): from parsec.api.data.invite import ( _RsInviteDeviceConfirmation, InviteDeviceConfirmation, _PyInviteDeviceConfirmation, ) assert InviteDeviceConfirmation is _RsInviteDeviceConfirmation di = DeviceID("a@b") dl = DeviceLabel("label") hh = HumanHandle("*****@*****.**", "Hubert Farnsworth") profile = UserProfile.STANDARD pk = PrivateKey.generate() umi = EntryID.new() umk = SecretKey.generate() sk = SigningKey.generate() vk = sk.verify_key sek = SecretKey.generate() py_idc = _PyInviteDeviceConfirmation( device_id=di, device_label=dl, human_handle=hh, profile=profile, private_key=pk, user_manifest_id=umi, user_manifest_key=umk, root_verify_key=vk, ) rs_idc = InviteDeviceConfirmation( device_id=di, device_label=dl, human_handle=hh, profile=profile, private_key=pk, user_manifest_id=umi, user_manifest_key=umk, root_verify_key=vk, ) assert rs_idc.device_label.str == py_idc.device_label.str assert str(rs_idc.human_handle) == str(py_idc.human_handle) assert rs_idc.device_id.str == py_idc.device_id.str assert rs_idc.profile == py_idc.profile assert rs_idc.user_manifest_id.hex == py_idc.user_manifest_id.hex rs_encrypted = rs_idc.dump_and_encrypt(key=sek) py_encrypted = py_idc.dump_and_encrypt(key=sek) # Decrypt Rust-encrypted with Rust rs_idc2 = InviteDeviceConfirmation.decrypt_and_load(rs_encrypted, sek) assert rs_idc.device_label.str == rs_idc2.device_label.str assert str(rs_idc.human_handle) == str(rs_idc2.human_handle) assert rs_idc.device_id.str == rs_idc2.device_id.str assert rs_idc.profile == rs_idc2.profile assert rs_idc.user_manifest_id.hex == rs_idc2.user_manifest_id.hex # Decrypt Python-encrypted with Python rs_idc3 = InviteDeviceConfirmation.decrypt_and_load(py_encrypted, sek) assert rs_idc.device_label.str == rs_idc3.device_label.str assert str(rs_idc.human_handle) == str(rs_idc3.human_handle) assert rs_idc.device_id.str == rs_idc3.device_id.str assert rs_idc.profile == rs_idc3.profile assert rs_idc.user_manifest_id.hex == rs_idc3.user_manifest_id.hex # Decrypt Rust-encrypted with Python py_idc2 = _PyInviteDeviceConfirmation.decrypt_and_load(rs_encrypted, sek) assert rs_idc.device_label.str == py_idc2.device_label.str assert str(rs_idc.human_handle) == str(py_idc2.human_handle) assert rs_idc.device_id.str == py_idc2.device_id.str assert rs_idc.profile == py_idc2.profile assert rs_idc.user_manifest_id.hex == rs_idc2.user_manifest_id.hex # With human_handle and device_label as None py_idc = _PyInviteDeviceConfirmation( device_id=di, device_label=None, human_handle=None, profile=profile, private_key=pk, user_manifest_id=umi, user_manifest_key=umk, root_verify_key=vk, ) rs_idc = InviteDeviceConfirmation( device_id=di, device_label=None, human_handle=None, profile=profile, private_key=pk, user_manifest_id=umi, user_manifest_key=umk, root_verify_key=vk, ) assert py_idc.device_label is None assert rs_idc.device_label is None assert py_idc.human_handle is None assert rs_idc.human_handle is None
async def test_concurrent_devices_agree_on_user_manifest( running_backend, backend_data_binder, data_base_dir, user_fs_factory, coolorg, alice, alice2, with_speculative, ): KEY = SecretKey.generate() async def _switch_running_backend_offline(task_status): should_switch_online = trio.Event() backend_online = trio.Event() async def _switch_backend_online(): should_switch_online.set() await backend_online.wait() with running_backend.offline(): task_status.started(_switch_backend_online) await should_switch_online.wait() backend_online.set() # I call this "diagonal programming"... async with trio.open_nursery() as nursery: switch_back_online = await nursery.start( _switch_running_backend_offline) with freeze_time("2000-01-01"): if with_speculative != "both": await user_storage_non_speculative_init( data_base_dir=data_base_dir, device=alice) async with user_fs_factory( alice, data_base_dir=data_base_dir) as user_fs1: wksp1_id = await user_fs1.workspace_create(EntryName("wksp1")) with freeze_time("2000-01-02"): if with_speculative not in ("both", "alice2"): await user_storage_non_speculative_init( data_base_dir=data_base_dir, device=alice2) async with user_fs_factory( alice2, data_base_dir=data_base_dir) as user_fs2: wksp2_id = await user_fs2.workspace_create( EntryName("wksp2")) with freeze_time("2000-01-03"): # Only now the backend appear offline, this is to ensure each # userfs has created a user manifest in isolation await backend_data_binder.bind_organization( coolorg, alice, initial_user_manifest="not_synced") await backend_data_binder.bind_device( alice2, certifier=alice) await switch_back_online() # Sync user_fs2 first to ensure created_on field is # kept even if further syncs have an earlier value with freeze_time("2000-01-04"): await user_fs2.sync() with freeze_time("2000-01-05"): await user_fs1.sync() with freeze_time("2000-01-06"): await user_fs2.sync() # Now, both user fs should have the same view on data expected_workspaces_entries = ( WorkspaceEntry( name=EntryName("wksp1"), id=wksp1_id, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 1), role_cached_on=datetime(2000, 1, 1), role=WorkspaceRole.OWNER, ), WorkspaceEntry( name=EntryName("wksp2"), id=wksp2_id, key=KEY, encryption_revision=1, encrypted_on=datetime(2000, 1, 2), role_cached_on=datetime(2000, 1, 2), role=WorkspaceRole.OWNER, ), ) expected_user_manifest = LocalUserManifest( base=UserManifest( id=alice.user_manifest_id, version=2, timestamp=datetime(2000, 1, 5), author=alice.device_id, created=datetime(2000, 1, 2), updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=expected_workspaces_entries, ), need_sync=False, updated=datetime(2000, 1, 2), last_processed_message=0, workspaces=expected_workspaces_entries, speculative=False, ) user_fs1_manifest = user_fs1.get_user_manifest() user_fs2_manifest = user_fs2.get_user_manifest() # We use to use ANY for the "key" argument in expected_user_manifest, # so that we could compare the two instances safely. Sadly, ANY doesn't # play nicely with the Rust bindings, so we instead update the instances # to change the key. user_fs1_manifest = user_fs1_manifest.evolve( workspaces=tuple( w.evolve(key=KEY) for w in user_fs1_manifest.workspaces), base=user_fs1_manifest.base.evolve( workspaces=tuple( w.evolve(key=KEY) for w in user_fs1_manifest.base.workspaces)), ) user_fs2_manifest = user_fs2_manifest.evolve( workspaces=tuple( w.evolve(key=KEY) for w in user_fs2_manifest.workspaces), base=user_fs2_manifest.base.evolve( workspaces=tuple( w.evolve(key=KEY) for w in user_fs2_manifest.base.workspaces)), ) assert user_fs1_manifest == expected_user_manifest assert user_fs2_manifest == expected_user_manifest
async def claim_device( organization_addr: BackendOrganizationAddr, new_device_id: DeviceID, token: str, keepalive: Optional[int] = None, ) -> LocalDevice: """ Raises: InviteClaimError InviteClaimBackendOfflineError InviteClaimValidationError InviteClaimPackingError InviteClaimCryptoError """ device_signing_key = SigningKey.generate() answer_private_key = PrivateKey.generate() try: async with backend_anonymous_cmds_factory(organization_addr, keepalive=keepalive) as cmds: # 1) Retrieve invitation creator try: invitation_creator_user, invitation_creator_device = await get_device_invitation_creator( cmds, organization_addr.root_verify_key, new_device_id) except RemoteDevicesManagerBackendOfflineError as exc: raise InviteClaimBackendOfflineError(str(exc)) from exc except RemoteDevicesManagerError as exc: raise InviteClaimError( f"Cannot retrieve invitation creator: {exc}") from exc # 2) Generate claim info for invitation creator try: encrypted_claim = DeviceClaimContent( token=token, device_id=new_device_id, verify_key=device_signing_key.verify_key, answer_public_key=answer_private_key.public_key, ).dump_and_encrypt_for( recipient_pubkey=invitation_creator_user.public_key) except DataError as exc: raise InviteClaimError( f"Cannot generate device claim message: {exc}") from exc # 3) Send claim rep = await cmds.device_claim(new_device_id, encrypted_claim) if rep["status"] != "ok": raise InviteClaimError(f"Claim request error: {rep}") # 4) Verify device certificate try: DeviceCertificateContent.verify_and_load( rep["device_certificate"], author_verify_key=invitation_creator_device.verify_key, expected_author=invitation_creator_device.device_id, expected_device=new_device_id, ) except DataError as exc: raise InviteClaimCryptoError(str(exc)) from exc try: answer = DeviceClaimAnswerContent.decrypt_and_load_for( rep["encrypted_answer"], recipient_privkey=answer_private_key) except DataError as exc: raise InviteClaimCryptoError( f"Cannot decrypt device claim answer: {exc}") from exc except BackendNotAvailable as exc: raise InviteClaimBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise InviteClaimError(f"Cannot claim device: {exc}") from exc return LocalDevice( organization_addr=organization_addr, device_id=new_device_id, signing_key=device_signing_key, private_key=answer.private_key, is_admin=invitation_creator_user.is_admin, user_manifest_id=answer.user_manifest_id, user_manifest_key=answer.user_manifest_key, local_symkey=SecretKey.generate(), )
def test_invite_user_data(): from parsec.api.data.invite import _RsInviteUserData, InviteUserData, _PyInviteUserData assert InviteUserData is _RsInviteUserData dl = DeviceLabel("label") hh = HumanHandle("*****@*****.**", "Hubert Farnsworth") pk = PrivateKey.generate() sik = SigningKey.generate() sek = SecretKey.generate() py_iud = _PyInviteUserData( requested_device_label=dl, requested_human_handle=hh, public_key=pk.public_key, verify_key=sik.verify_key, ) rs_iud = InviteUserData( requested_device_label=dl, requested_human_handle=hh, public_key=pk.public_key, verify_key=sik.verify_key, ) assert rs_iud.requested_device_label.str == py_iud.requested_device_label.str assert str(rs_iud.requested_human_handle) == str( py_iud.requested_human_handle) rs_encrypted = rs_iud.dump_and_encrypt(key=sek) py_encrypted = py_iud.dump_and_encrypt(key=sek) # Decrypt Rust-encrypted with Rust rs_iud2 = InviteUserData.decrypt_and_load(rs_encrypted, sek) assert rs_iud.requested_device_label.str == rs_iud2.requested_device_label.str assert str(rs_iud.requested_human_handle) == str( rs_iud2.requested_human_handle) # Decrypt Python-encrypted with Python rs_iud3 = InviteUserData.decrypt_and_load(py_encrypted, sek) assert rs_iud.requested_device_label.str == rs_iud3.requested_device_label.str assert str(rs_iud.requested_human_handle) == str( rs_iud3.requested_human_handle) # Decrypt Rust-encrypted with Python py_iud2 = _PyInviteUserData.decrypt_and_load(rs_encrypted, sek) assert rs_iud.requested_device_label.str == py_iud2.requested_device_label.str assert str(rs_iud.requested_human_handle) == str( py_iud2.requested_human_handle) # With requested_human_handle and requested_device_label as None py_iud = _PyInviteUserData( requested_device_label=None, requested_human_handle=None, public_key=pk.public_key, verify_key=sik.verify_key, ) rs_iud = InviteUserData( requested_device_label=None, requested_human_handle=None, public_key=pk.public_key, verify_key=sik.verify_key, ) assert py_iud.requested_device_label is None assert rs_iud.requested_device_label is None assert py_iud.requested_human_handle is None assert rs_iud.requested_human_handle is None