コード例 #1
0
    async def load_block(self, access: BlockAccess) -> None:
        """
        Raises:
            FSError
            FSRemoteBlockNotFound
            FSBackendOfflineError
            FSWorkspaceInMaintenance
            FSWorkspaceNoAccess
        """
        # Download
        rep = await self._backend_cmds("block_read", access.id)
        if rep["status"] == "not_found":
            raise FSRemoteBlockNotFound(access)
        elif rep["status"] == "not_allowed":
            # Seems we lost the access to the realm
            raise FSWorkspaceNoReadAccess("Cannot load block: no read access")
        elif rep["status"] == "in_maintenance":
            raise FSWorkspaceInMaintenance(
                f"Cannot download block while the workspace in maintenance")
        elif rep["status"] != "ok":
            raise FSError(f"Cannot download block: `{rep['status']}`")

        # Decryption
        try:
            block = access.key.decrypt(rep["block"])

        # Decryption error
        except CryptoError as exc:
            raise FSError(f"Cannot decrypt block: {exc}") from exc

        # TODO: let encryption manager do the digest check ?
        assert HashDigest.from_data(block) == access.digest, access
        await self.local_storage.set_clean_block(access.id, block)
コード例 #2
0
    async def _do_wait_peer(self) -> Tuple[SASCode, SASCode, SecretKey]:
        claimer_private_key = PrivateKey.generate()
        rep = await self._cmds.invite_1_claimer_wait_peer(
            claimer_public_key=claimer_private_key.public_key)
        if rep["status"] != "ok":
            raise InviteError(f"Backend error during step 1: {rep}")

        shared_secret_key = generate_shared_secret_key(
            our_private_key=claimer_private_key,
            peer_public_key=rep["greeter_public_key"])
        claimer_nonce = generate_nonce()

        rep = await self._cmds.invite_2a_claimer_send_hashed_nonce(
            claimer_hashed_nonce=HashDigest.from_data(claimer_nonce))
        if rep["status"] == "invalid_state":
            raise InvitePeerResetError()
        elif rep["status"] != "ok":
            raise InviteError(f"Backend error during step 2a: {rep}")

        claimer_sas, greeter_sas = generate_sas_codes(
            claimer_nonce=claimer_nonce,
            greeter_nonce=rep["greeter_nonce"],
            shared_secret_key=shared_secret_key,
        )

        rep = await self._cmds.invite_2b_claimer_send_nonce(
            claimer_nonce=claimer_nonce)
        if rep["status"] == "invalid_state":
            raise InvitePeerResetError()
        elif rep["status"] != "ok":
            raise InviteError(f"Backend error during step 2b: {rep}")

        return claimer_sas, greeter_sas, shared_secret_key
コード例 #3
0
ファイル: claimer.py プロジェクト: stjordanis/parsec-cloud
    async def _do_wait_peer(self) -> Tuple[SASCode, SASCode, SecretKey]:
        claimer_private_key = PrivateKey.generate()
        rep = await self._cmds.invite_1_claimer_wait_peer(
            claimer_public_key=claimer_private_key.public_key)
        _check_rep(rep, step_name="step 1")

        shared_secret_key = generate_shared_secret_key(
            our_private_key=claimer_private_key,
            peer_public_key=rep["greeter_public_key"])
        claimer_nonce = generate_nonce()

        rep = await self._cmds.invite_2a_claimer_send_hashed_nonce(
            claimer_hashed_nonce=HashDigest.from_data(claimer_nonce))
        _check_rep(rep, step_name="step 2a")

        claimer_sas, greeter_sas = generate_sas_codes(
            claimer_nonce=claimer_nonce,
            greeter_nonce=rep["greeter_nonce"],
            shared_secret_key=shared_secret_key,
        )

        rep = await self._cmds.invite_2b_claimer_send_nonce(
            claimer_nonce=claimer_nonce)
        _check_rep(rep, step_name="step 2b")

        return claimer_sas, greeter_sas, shared_secret_key
コード例 #4
0
def test_hash_digest(bytes_cls):
    from parsec.crypto import _PyHashDigest, _RsHashDigest, HashDigest

    assert HashDigest is _RsHashDigest

    rst = HashDigest.from_data(bytes_cls(b"abc"))
    py = _PyHashDigest.from_data(bytes_cls(b"abc"))
    assert rst.digest == py.digest
    assert rst.hexdigest() == py.hexdigest()
    assert repr(rst) == repr(py)

    rst2 = HashDigest.from_data(bytes_cls(b"abc"))
    rst3 = HashDigest.from_data(bytes_cls(b"def"))
    assert rst == rst2
    assert not rst != rst2
    assert rst != rst3
    assert not rst == rst3
コード例 #5
0
ファイル: test_exchange.py プロジェクト: Scille/parsec-cloud
 async def _greeter_step_2():
     rep = await invite_2a_claimer_send_hashed_nonce(
         invited_sock, claimer_hashed_nonce=HashDigest.from_data(b"<retry_nonce>")
     )
     assert rep == {"status": "ok", "greeter_nonce": b"greeter nonce"}
     rep = await invite_2b_claimer_send_nonce(
         invited_sock, claimer_nonce=b"claimer nonce"
     )
     assert rep == {"status": "ok"}
コード例 #6
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)
コード例 #7
0
ファイル: test_exchange.py プロジェクト: Scille/parsec-cloud
 async def _claimer_step_2():
     rep = await invite_2a_greeter_get_hashed_nonce(
         alice_backend_sock, token=invitation.token
     )
     assert rep == {
         "status": "ok",
         "claimer_hashed_nonce": HashDigest.from_data(b"<retry_nonce>"),
     }
     rep = await invite_2b_greeter_send_nonce(
         alice_backend_sock, token=invitation.token, greeter_nonce=b"greeter nonce"
     )
     assert rep == {"status": "ok", "claimer_nonce": b"claimer nonce"}
コード例 #8
0
    async def _do_wait_peer(self) -> Tuple[SASCode, SASCode, SecretKey]:
        greeter_private_key = PrivateKey.generate()
        rep = await self._cmds.invite_1_greeter_wait_peer(
            token=self.token,
            greeter_public_key=greeter_private_key.public_key)
        if rep["status"] in ("not_found", "already_deleted"):
            raise InviteNotAvailableError()
        elif rep["status"] != "ok":
            raise InviteError(f"Backend error during step 1: {rep}")

        shared_secret_key = generate_shared_secret_key(
            our_private_key=greeter_private_key,
            peer_public_key=rep["claimer_public_key"])
        greeter_nonce = generate_nonce()

        rep = await self._cmds.invite_2a_greeter_get_hashed_nonce(
            token=self.token)
        if rep["status"] in ("not_found", "already_deleted"):
            raise InviteNotAvailableError()
        elif rep["status"] == "invalid_state":
            raise InvitePeerResetError()
        elif rep["status"] != "ok":
            raise InviteError(f"Backend error during step 2a: {rep}")

        claimer_hashed_nonce = rep["claimer_hashed_nonce"]

        rep = await self._cmds.invite_2b_greeter_send_nonce(
            token=self.token, greeter_nonce=greeter_nonce)
        if rep["status"] in ("not_found", "already_deleted"):
            raise InviteNotAvailableError()
        elif rep["status"] == "invalid_state":
            raise InvitePeerResetError()
        elif rep["status"] != "ok":
            raise InviteError(f"Backend error during step 2b: {rep}")

        if HashDigest.from_data(rep["claimer_nonce"]) != claimer_hashed_nonce:
            raise InviteError("Invitee nonce and hashed nonce doesn't match")

        claimer_sas, greeter_sas = generate_sas_codes(
            claimer_nonce=rep["claimer_nonce"],
            greeter_nonce=greeter_nonce,
            shared_secret_key=shared_secret_key,
        )

        return claimer_sas, greeter_sas, shared_secret_key
コード例 #9
0
ファイル: rsync.py プロジェクト: admariner/parsec-cloud
async def _update_file(workspace_fs: WorkspaceFS, entry_id: EntryID,
                       local_path: AnyPath, workspace_path: FsPath):

    remote_file_manifest = await workspace_fs.remote_loader.load_manifest(
        entry_id)
    remote_access_digests = [
        access.digest for access in remote_file_manifest.blocks
    ]
    offset = 0
    for idx, chunk in enumerate(await _chunks_from_path(local_path)):
        if HashDigest.from_data(chunk) != remote_access_digests[idx]:
            await workspace_fs.write_bytes(workspace_path, chunk, offset)
            print(f"update the block {idx} in {workspace_path}")
        offset += len(chunk)

    await workspace_fs.sync_by_id(entry_id,
                                  remote_changed=False,
                                  recursive=False)
コード例 #10
0
    async def _run_claimer(tb):
        peer_controller = tb.claimer_ctlr
        while True:
            order, step_4_payload = await peer_controller.peer_next_order()

            if order == "invite_info":
                await peer_controller.peer_do(invite_info, tb.claimer_sock)

            elif order == "1_wait_peer":
                await peer_controller.peer_do(
                    invite_1_claimer_wait_peer,
                    tb.claimer_sock,
                    claimer_public_key=tb.claimer_privkey.public_key,
                )

            elif order == "2a_send_hashed_nonce":
                await peer_controller.peer_do(
                    invite_2a_claimer_send_hashed_nonce,
                    tb.claimer_sock,
                    claimer_hashed_nonce=HashDigest.from_data(
                        b"<claimer_nonce>"),
                )

            elif order == "2b_send_nonce":
                await peer_controller.peer_do(invite_2b_claimer_send_nonce,
                                              tb.claimer_sock,
                                              claimer_nonce=b"<claimer_nonce>")

            elif order == "3a_signify_trust":
                await peer_controller.peer_do(invite_3a_claimer_signify_trust,
                                              tb.claimer_sock)

            elif order == "3b_wait_peer_trust":
                await peer_controller.peer_do(
                    invite_3b_claimer_wait_peer_trust, tb.claimer_sock)

            elif order == "4_communicate":
                assert step_4_payload is not None
                await peer_controller.peer_do(invite_4_claimer_communicate,
                                              tb.claimer_sock,
                                              payload=step_4_payload)

            else:
                assert False
コード例 #11
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)
コード例 #12
0
ファイル: test_exchange.py プロジェクト: Scille/parsec-cloud
    async def _run_claimer(peer_controller):
        while True:
            order, order_arg = await peer_controller.peer_next_order()

            if order == "1_wait_peer":
                await peer_controller.peer_do(
                    invite_1_claimer_wait_peer,
                    invited_sock,
                    claimer_public_key=claimer_privkey.public_key,
                )

            elif order == "2a_send_hashed_nonce":
                await peer_controller.peer_do(
                    invite_2a_claimer_send_hashed_nonce,
                    invited_sock,
                    claimer_hashed_nonce=HashDigest.from_data(b"<claimer_nonce>"),
                )

            elif order == "2b_send_nonce":
                await peer_controller.peer_do(
                    invite_2b_claimer_send_nonce, invited_sock, claimer_nonce=b"<claimer_nonce>"
                )

            elif order == "3a_signify_trust":
                await peer_controller.peer_do(invite_3a_claimer_signify_trust, invited_sock)

            elif order == "3b_wait_peer_trust":
                await peer_controller.peer_do(invite_3b_claimer_wait_peer_trust, invited_sock)

            elif order == "4_communicate":
                assert order_arg is not None
                await peer_controller.peer_do(
                    invite_4_claimer_communicate, invited_sock, payload=order_arg
                )

            else:
                assert False
コード例 #13
0
def test_file_manifest():
    from parsec.api.data.manifest import _RsFileManifest, FileManifest, _PyFileManifest, BlockAccess

    assert FileManifest is _RsFileManifest

    def _assert_file_manifest_eq(py, rs):
        assert isinstance(py, _PyFileManifest)
        assert isinstance(rs, _RsFileManifest)

        assert py.author == rs.author
        assert py.id == rs.id
        assert py.parent == rs.parent
        assert py.version == rs.version
        assert py.size == rs.size
        assert py.blocksize == rs.blocksize
        assert py.timestamp == rs.timestamp
        assert py.created == rs.created
        assert py.updated == rs.updated
        assert len(py.blocks) == len(rs.blocks)
        assert all(
            isinstance(b2, BlockAccess) and b1.id == b2.id
            and b1.offset == b2.offset and b1.size == b2.size
            for (b1, b2) in zip(py.blocks, rs.blocks))

    kwargs = {
        "author":
        DeviceID("user@device"),
        "id":
        EntryID.new(),
        "parent":
        EntryID.new(),
        "version":
        42,
        "size":
        1337,
        "blocksize":
        64,
        "timestamp":
        pendulum.now(),
        "created":
        pendulum.now(),
        "updated":
        pendulum.now(),
        "blocks": (BlockAccess(
            id=BlockID.new(),
            key=SecretKey.generate(),
            offset=0,
            size=1024,
            digest=HashDigest.from_data(b"a"),
        ), ),
    }

    py_fm = _PyFileManifest(**kwargs)
    rs_fm = FileManifest(**kwargs)
    _assert_file_manifest_eq(py_fm, rs_fm)

    kwargs = {
        "author":
        DeviceID("a@b"),
        "id":
        EntryID.new(),
        "parent":
        EntryID.new(),
        "version":
        1337,
        "timestamp":
        pendulum.now(),
        "created":
        pendulum.now(),
        "updated":
        pendulum.now(),
        "blocks": (BlockAccess(
            id=BlockID.new(),
            key=SecretKey.generate(),
            offset=64,
            size=2048,
            digest=HashDigest.from_data(b"b"),
        ), ),
    }

    py_fm = py_fm.evolve(**kwargs)
    rs_fm = rs_fm.evolve(**kwargs)
    _assert_file_manifest_eq(py_fm, rs_fm)
コード例 #14
0
ファイル: test_exchange.py プロジェクト: Scille/parsec-cloud
async def test_claimer_step_2_retry(
    backend, alice, backend_sock_factory, alice_backend_sock, invitation, invited_sock
):
    greeter_privkey = PrivateKey.generate()
    claimer_privkey = PrivateKey.generate()
    greeter_retry_privkey = PrivateKey.generate()
    claimer_retry_privkey = PrivateKey.generate()

    # Step 1
    async with real_clock_timeout():
        async with invite_1_greeter_wait_peer.async_call(
            alice_backend_sock,
            token=invitation.token,
            greeter_public_key=greeter_privkey.public_key,
        ) as greeter_async_rep:
            claimer_rep = await invite_1_claimer_wait_peer(
                invited_sock, claimer_public_key=claimer_privkey.public_key
            )
            assert claimer_rep == {"status": "ok", "greeter_public_key": greeter_privkey.public_key}
        assert greeter_async_rep.rep == {
            "status": "ok",
            "claimer_public_key": claimer_privkey.public_key,
        }

    # Greeter initiates step 2a...
    async with real_clock_timeout():
        with backend.event_bus.listen() as spy:

            async with invite_2a_greeter_get_hashed_nonce.async_call(
                alice_backend_sock, token=invitation.token
            ) as greeter_2a_async_rep:
                await spy.wait_with_timeout(
                    BackendEvent.INVITE_CONDUIT_UPDATED,
                    kwargs={"organization_id": alice.organization_id, "token": invitation.token},
                )

                # ...but changes his mind and reset from another connection !
                async with backend_sock_factory(backend, alice) as alice_backend_sock2:
                    async with invite_1_greeter_wait_peer.async_call(
                        alice_backend_sock2,
                        token=invitation.token,
                        greeter_public_key=greeter_retry_privkey.public_key,
                    ) as greeter_retry_1_async_rep:

                        # First connection should be notified of the reset
                        await greeter_2a_async_rep.do_recv()
                        assert greeter_2a_async_rep.rep == {"status": "invalid_state"}

                        # Claimer now arrives and try to do step 2a
                        rep = await invite_2a_claimer_send_hashed_nonce(
                            invited_sock,
                            claimer_hashed_nonce=HashDigest.from_data(b"<claimer_nonce>"),
                        )
                        assert rep == {"status": "invalid_state"}

                        # So claimer returns to step 1
                        rep = await invite_1_claimer_wait_peer(
                            invited_sock, claimer_public_key=claimer_retry_privkey.public_key
                        )
                        assert rep == {
                            "status": "ok",
                            "greeter_public_key": greeter_retry_privkey.public_key,
                        }

                    assert greeter_retry_1_async_rep.rep == {
                        "status": "ok",
                        "claimer_public_key": claimer_retry_privkey.public_key,
                    }

            # Finally retry and achieve step 2

            async def _claimer_step_2():
                rep = await invite_2a_greeter_get_hashed_nonce(
                    alice_backend_sock, token=invitation.token
                )
                assert rep == {
                    "status": "ok",
                    "claimer_hashed_nonce": HashDigest.from_data(b"<retry_nonce>"),
                }
                rep = await invite_2b_greeter_send_nonce(
                    alice_backend_sock, token=invitation.token, greeter_nonce=b"greeter nonce"
                )
                assert rep == {"status": "ok", "claimer_nonce": b"claimer nonce"}

            async def _greeter_step_2():
                rep = await invite_2a_claimer_send_hashed_nonce(
                    invited_sock, claimer_hashed_nonce=HashDigest.from_data(b"<retry_nonce>")
                )
                assert rep == {"status": "ok", "greeter_nonce": b"greeter nonce"}
                rep = await invite_2b_claimer_send_nonce(
                    invited_sock, claimer_nonce=b"claimer nonce"
                )
                assert rep == {"status": "ok"}

            async with real_clock_timeout():
                async with trio.open_nursery() as nursery:
                    nursery.start_soon(_claimer_step_2)
                    nursery.start_soon(_greeter_step_2)
コード例 #15
0
ファイル: test_exchange.py プロジェクト: Scille/parsec-cloud
async def test_conduit_exchange_good(exchange_testbed, leader):
    greeter_privkey, claimer_privkey, greeter_ctlr, claimer_ctlr = exchange_testbed

    # Step 1
    if leader == "greeter":
        await greeter_ctlr.send_order("1_wait_peer")
        await claimer_ctlr.send_order("1_wait_peer")
    else:
        await claimer_ctlr.send_order("1_wait_peer")
        await greeter_ctlr.send_order("1_wait_peer")
    greeter_rep = await greeter_ctlr.get_result()
    claimer_rep = await claimer_ctlr.get_result()
    assert greeter_rep == {"status": "ok", "claimer_public_key": claimer_privkey.public_key}
    assert claimer_rep == {"status": "ok", "greeter_public_key": greeter_privkey.public_key}

    # Step 2
    if leader == "greeter":
        await greeter_ctlr.send_order("2a_get_hashed_nonce")
        await claimer_ctlr.send_order("2a_send_hashed_nonce")
    else:
        await claimer_ctlr.send_order("2a_send_hashed_nonce")
        await greeter_ctlr.send_order("2a_get_hashed_nonce")

    greeter_rep = await greeter_ctlr.get_result()
    assert greeter_rep == {
        "status": "ok",
        "claimer_hashed_nonce": HashDigest.from_data(b"<claimer_nonce>"),
    }
    await greeter_ctlr.send_order("2b_send_nonce")

    claimer_rep = await claimer_ctlr.get_result()
    assert claimer_rep == {"status": "ok", "greeter_nonce": b"<greeter_nonce>"}
    await claimer_ctlr.send_order("2b_send_nonce")

    greeter_rep = await greeter_ctlr.get_result()
    assert greeter_rep == {"status": "ok", "claimer_nonce": b"<claimer_nonce>"}
    claimer_rep = await claimer_ctlr.get_result()
    assert claimer_rep == {"status": "ok"}

    # Step 3a
    if leader == "greeter":
        await greeter_ctlr.send_order("3a_wait_peer_trust")
        await claimer_ctlr.send_order("3a_signify_trust")
    else:
        await claimer_ctlr.send_order("3a_signify_trust")
        await greeter_ctlr.send_order("3a_wait_peer_trust")
    greeter_rep = await greeter_ctlr.get_result()
    assert greeter_rep == {"status": "ok"}
    claimer_rep = await claimer_ctlr.get_result()
    assert claimer_rep == {"status": "ok"}

    # Step 3b
    if leader == "greeter":
        await greeter_ctlr.send_order("3b_signify_trust")
        await claimer_ctlr.send_order("3b_wait_peer_trust")
    else:
        await claimer_ctlr.send_order("3b_wait_peer_trust")
        await greeter_ctlr.send_order("3b_signify_trust")
    greeter_rep = await greeter_ctlr.get_result()
    assert greeter_rep == {"status": "ok"}
    claimer_rep = await claimer_ctlr.get_result()
    assert claimer_rep == {"status": "ok"}

    # Step 4
    if leader == "greeter":
        await greeter_ctlr.send_order("4_communicate", b"hello from greeter")
        await claimer_ctlr.send_order("4_communicate", b"hello from claimer")
    else:
        await claimer_ctlr.send_order("4_communicate", b"hello from claimer")
        await greeter_ctlr.send_order("4_communicate", b"hello from greeter")
    greeter_rep = await greeter_ctlr.get_result()
    assert greeter_rep == {"status": "ok", "payload": b"hello from claimer"}
    claimer_rep = await claimer_ctlr.get_result()
    assert claimer_rep == {"status": "ok", "payload": b"hello from greeter"}

    if leader == "greeter":
        await greeter_ctlr.send_order("4_communicate", b"")
        await claimer_ctlr.send_order("4_communicate", b"")
    else:
        await claimer_ctlr.send_order("4_communicate", b"")
        await greeter_ctlr.send_order("4_communicate", b"")
    greeter_rep = await greeter_ctlr.get_result()
    assert greeter_rep == {"status": "ok", "payload": b""}
    claimer_rep = await claimer_ctlr.get_result()
    assert claimer_rep == {"status": "ok", "payload": b""}
コード例 #16
0
def test_local_file_manifest():
    from parsec.api.data.manifest import BlockAccess
    from parsec.core.types.manifest import (
        _RsLocalFileManifest,
        LocalFileManifest,
        _PyLocalFileManifest,
        Chunk,
    )

    assert LocalFileManifest is _RsLocalFileManifest

    def _assert_local_file_manifest_eq(py,
                                       rs,
                                       exclude_base=False,
                                       exclude_id=False):
        assert isinstance(py, _PyLocalFileManifest)
        assert isinstance(rs, _RsLocalFileManifest)
        if not exclude_base:
            assert py.base == rs.base
        assert py.need_sync == rs.need_sync
        assert py.updated == rs.updated
        assert py.size == rs.size
        assert py.blocksize == rs.blocksize
        assert len(py.blocks) == len(rs.blocks)
        assert isinstance(rs.blocks, type(py.blocks))
        if len(py.blocks):
            assert isinstance(rs.blocks[0], type(py.blocks[0]))
        if not exclude_id:
            assert py.id == rs.id
        assert py.created == rs.created
        assert py.base_version == rs.base_version
        assert py.is_placeholder == rs.is_placeholder
        assert py.is_reshaped() == rs.is_reshaped()
        for (b1, b2) in zip(sorted(py.blocks), sorted(rs.blocks)):
            assert len(b1) == len(b2)
            assert all(
                isinstance(c2, Chunk) and c1.id == c2.id and c1.start == c2.
                start and c1.stop == c2.stop and c1.raw_offset == c2.raw_offset
                and c1.raw_size == c2.raw_size and c1.access == c2.access
                for (c1, c2) in zip(b1, b2))

    def _assert_file_manifest_eq(py, rs):
        assert py.author == rs.author
        assert py.parent == rs.parent
        assert py.version == rs.version
        assert py.size == rs.size
        assert py.blocksize == rs.blocksize
        assert py.timestamp == rs.timestamp
        assert py.created == rs.created
        assert py.updated == rs.updated
        assert len(py.blocks) == len(rs.blocks)
        assert isinstance(rs.blocks, type(py.blocks))
        assert all(
            isinstance(b2, BlockAccess) and b1.id == b2.id
            and b1.offset == b2.offset and b1.size == b2.size
            for (b1, b2) in zip(sorted(py.blocks), sorted(rs.blocks)))

    kwargs = {
        "base":
        FileManifest(
            author=DeviceID("user@device"),
            id=EntryID.new(),
            parent=EntryID.new(),
            version=42,
            size=1337,
            blocksize=85,
            timestamp=pendulum.now(),
            created=pendulum.now(),
            updated=pendulum.now(),
            blocks=(BlockAccess(
                id=BlockID.new(),
                key=SecretKey.generate(),
                offset=0,
                size=1024,
                digest=HashDigest.from_data(b"a"),
            ), ),
        ),
        "need_sync":
        True,
        "updated":
        pendulum.now(),
        "size":
        42,
        "blocksize":
        64,
        "blocks": ((
            Chunk(
                id=ChunkID.new(),
                start=0,
                stop=250,
                raw_offset=0,
                raw_size=512,
                access=BlockAccess(
                    id=BlockID.new(),
                    key=SecretKey.generate(),
                    offset=0,
                    size=512,
                    digest=HashDigest.from_data(b"aa"),
                ),
            ),
            Chunk(id=ChunkID.new(),
                  start=0,
                  stop=250,
                  raw_offset=250,
                  raw_size=250,
                  access=None),
        ), ),
    }

    py_lfm = _PyLocalFileManifest(**kwargs)
    rs_lfm = LocalFileManifest(**kwargs)
    _assert_local_file_manifest_eq(py_lfm, rs_lfm)

    kwargs = {
        "base":
        kwargs["base"].evolve(
            **{
                "author":
                DeviceID("a@b"),
                "id":
                EntryID.new(),
                "parent":
                EntryID.new(),
                "version":
                1337,
                "size":
                4096,
                "blocksize":
                512,
                "timestamp":
                pendulum.now(),
                "created":
                pendulum.now(),
                "updated":
                pendulum.now(),
                "blocks": (BlockAccess(
                    id=BlockID.new(),
                    key=SecretKey.generate(),
                    offset=64,
                    size=2048,
                    digest=HashDigest.from_data(b"b"),
                ), ),
            }),
        "need_sync":
        False,
        "updated":
        pendulum.now(),
        "size":
        2048,
        "blocksize":
        1024,
        "blocks": ((
            Chunk(
                id=ChunkID.new(),
                start=0,
                stop=1024,
                raw_offset=0,
                raw_size=1024,
                access=BlockAccess(
                    id=BlockID.new(),
                    key=SecretKey.generate(),
                    offset=0,
                    size=1024,
                    digest=HashDigest.from_data(b"bb"),
                ),
            ),
            Chunk(
                id=ChunkID.new(),
                start=1024,
                stop=2048,
                raw_offset=1024,
                raw_size=1024,
                access=None,
            ),
        ), ),
    }

    py_lfm = py_lfm.evolve(**kwargs)
    rs_lfm = rs_lfm.evolve(**kwargs)
    _assert_local_file_manifest_eq(py_lfm, rs_lfm)

    sk = SecretKey.generate()

    py_enc = py_lfm.dump_and_encrypt(sk)
    rs_enc = rs_lfm.dump_and_encrypt(sk)

    # Decrypt rust encrypted with Python and vice versa
    lfm1 = _PyLocalFileManifest.decrypt_and_load(rs_enc, sk)
    lfm2 = LocalFileManifest.decrypt_and_load(py_enc, sk)
    assert isinstance(lfm1, LocalFileManifest)
    assert isinstance(lfm2, LocalFileManifest)
    assert lfm1 == lfm2

    py_lfm = py_lfm.evolve(**{"size": 1337})
    rs_lfm = rs_lfm.evolve(**{"size": 1337})
    _assert_local_file_manifest_eq(py_lfm, rs_lfm)

    with pytest.raises(AssertionError):
        py_lfm.assert_integrity()
    with pytest.raises(AssertionError):
        rs_lfm.assert_integrity()

    assert py_lfm.to_stats() == rs_lfm.to_stats()
    assert py_lfm.parent == rs_lfm.parent
    assert py_lfm.get_chunks(0) == rs_lfm.get_chunks(0)
    assert py_lfm.get_chunks(1000) == rs_lfm.get_chunks(1000)
    assert py_lfm.asdict() == rs_lfm.asdict()

    di = DeviceID("a@b")
    ts = pendulum.now()

    kwargs = {
        "size":
        1024,
        "blocksize":
        1024,
        "blocks": ((Chunk(
            id=ChunkID.new(),
            start=0,
            stop=1024,
            raw_offset=0,
            raw_size=1024,
            access=BlockAccess(
                id=BlockID.new(),
                key=SecretKey.generate(),
                offset=0,
                size=1024,
                digest=HashDigest.from_data(b"bb"),
            ),
        ), ), ),
    }

    py_lfm = py_lfm.evolve(**kwargs)
    rs_lfm = rs_lfm.evolve(**kwargs)
    _assert_local_file_manifest_eq(py_lfm, rs_lfm)

    py_rfm = py_lfm.to_remote(author=di, timestamp=ts)
    rs_rfm = rs_lfm.to_remote(author=di, timestamp=ts)
    _assert_file_manifest_eq(py_rfm, rs_rfm)

    py_lfm2 = _PyLocalFileManifest.from_remote(py_rfm)
    rs_lfm2 = LocalFileManifest.from_remote(rs_rfm)
    _assert_local_file_manifest_eq(py_lfm2,
                                   rs_lfm2,
                                   exclude_base=True,
                                   exclude_id=True)

    py_lfm2 = _PyLocalFileManifest.from_remote_with_local_context(
        remote=py_rfm,
        prevent_sync_pattern=r".+",
        local_manifest=py_lfm2,
        timestamp=ts)
    rs_lfm2 = LocalFileManifest.from_remote_with_local_context(
        remote=rs_rfm,
        prevent_sync_pattern=r".+",
        local_manifest=rs_lfm2,
        timestamp=ts)

    assert py_lfm.match_remote(py_rfm) == rs_lfm.match_remote(rs_rfm)

    py_lfm = py_lfm.evolve_and_mark_updated(timestamp=ts, size=4096)
    rs_lfm = rs_lfm.evolve_and_mark_updated(timestamp=ts, size=4096)

    _assert_local_file_manifest_eq(py_lfm,
                                   rs_lfm,
                                   exclude_base=True,
                                   exclude_id=True)

    with pytest.raises(TypeError) as excinfo:
        py_lfm.evolve_and_mark_updated(timestamp=ts, need_sync=True)
    assert str(excinfo.value) == "Unexpected keyword argument `need_sync`"
    with pytest.raises(TypeError) as excinfo:
        rs_lfm.evolve_and_mark_updated(timestamp=ts, need_sync=True)
    assert str(excinfo.value) == "Unexpected keyword argument `need_sync`"

    ei = EntryID.new()

    # Without blocksize
    py_lfm = _PyLocalFileManifest.new_placeholder(author=di,
                                                  parent=ei,
                                                  timestamp=ts)
    rs_lfm = LocalFileManifest.new_placeholder(author=di,
                                               parent=ei,
                                               timestamp=ts)
    _assert_local_file_manifest_eq(py_lfm,
                                   rs_lfm,
                                   exclude_base=True,
                                   exclude_id=True)
    # With blocksize
    py_lfm = _PyLocalFileManifest.new_placeholder(author=di,
                                                  parent=ei,
                                                  timestamp=ts,
                                                  blocksize=1024)
    rs_lfm = LocalFileManifest.new_placeholder(author=di,
                                               parent=ei,
                                               timestamp=ts,
                                               blocksize=1024)
    _assert_local_file_manifest_eq(py_lfm,
                                   rs_lfm,
                                   exclude_base=True,
                                   exclude_id=True)