Esempio n. 1
0
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"
            )),
    )
Esempio n. 2
0
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"
            )),
    )
Esempio n. 3
0
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)
Esempio n. 4
0
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(),
    )
Esempio n. 5
0
    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,
        )
Esempio n. 6
0
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)
Esempio n. 7
0
    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(),
        )
Esempio n. 8
0
    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
Esempio n. 9
0
 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,
     )
Esempio n. 10
0
    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
Esempio n. 11
0
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)
Esempio n. 12
0
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(),
    )
Esempio n. 13
0
    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(),
        )
Esempio n. 14
0
 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,
     )
Esempio n. 15
0
    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)
Esempio n. 16
0
    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)
Esempio n. 17
0
    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)
Esempio n. 18
0
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
Esempio n. 19
0
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)
Esempio n. 20
0
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
Esempio n. 21
0
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
Esempio n. 22
0
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,
        )
Esempio n. 23
0
    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)
Esempio n. 24
0
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,
        )
Esempio n. 25
0
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
Esempio n. 26
0
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
Esempio n. 27
0
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
Esempio n. 28
0
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
Esempio n. 29
0
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(),
    )
Esempio n. 30
0
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