async def _bootstrap_organization( debug, device_id, organization_bootstrap_addr, config_dir, force, password ): root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = organization_bootstrap_addr.generate_organization_addr(root_verify_key) device_display = click.style(device_id, fg="yellow") device = generate_new_device(device_id, organization_addr) with operation(f"Creating locally {device_display}"): save_device_with_password(config_dir, device, password, force=force) now = pendulum.now() certified_user = certify_user(None, root_signing_key, device.user_id, device.public_key, now) certified_device = certify_device(None, root_signing_key, device_id, device.verify_key, now) async with spinner(f"Sending {device_display} to server"): async with backend_anonymous_cmds_factory(organization_bootstrap_addr) as cmds: await cmds.organization_bootstrap( organization_bootstrap_addr.organization_id, organization_bootstrap_addr.bootstrap_token, root_verify_key, certified_user, certified_device, ) organization_addr_display = click.style(organization_addr, fg="yellow") click.echo(f"Organization url: {organization_addr_display}")
async def test_anonymous_cmds_has_right_methods(running_backend, coolorg): async with backend_anonymous_cmds_factory(coolorg.addr) as cmds: for method_name in ANONYMOUS_CMDS: assert hasattr(cmds, method_name) for method_name in (ADMINISTRATION_CMDS | AUTHENTICATED_CMDS) - ANONYMOUS_CMDS: assert not hasattr(cmds, method_name)
async def _trio_claim_user( queue, qt_on_done, qt_on_error, config, addr, device_id, token, use_pkcs11, password=None, pkcs11_token=None, pkcs11_key=None, ): portal = trio.BlockingTrioPortal() queue.put(portal) with trio.open_cancel_scope() as cancel_scope: queue.put(cancel_scope) try: async with backend_anonymous_cmds_factory(addr) as cmds: device = await core_claim_user(cmds, device_id, token) if use_pkcs11: devices_manager.save_device_with_pkcs11( config.config_dir, device, pkcs11_token, pkcs11_key) else: devices_manager.save_device_with_password( config.config_dir, device, password) qt_on_done.emit() except BackendCmdsBadResponse as e: qt_on_error.emit(e.status)
async def _alice_nd_claim(): async with backend_anonymous_cmds_factory(alice.organization_addr) as cmds: ret = await cmds.device_get_invitation_creator(nd_id) assert ret["status"] == "ok" assert ret["trustchain"] == {"devices": [], "revoked_users": [], "users": []} creator = UserCertificateContent.unsecure_load(ret["user_certificate"]) creator_device = DeviceCertificateContent.unsecure_load(ret["device_certificate"]) assert creator_device.device_id.user_id == creator.user_id answer_private_key = PrivateKey.generate() encrypted_claim = DeviceClaimContent( token=token, device_id=nd_id, verify_key=nd_signing_key.verify_key, answer_public_key=answer_private_key.public_key, ).dump_and_encrypt_for(recipient_pubkey=creator.public_key) with trio.fail_after(1): ret = await cmds.device_claim(nd_id, encrypted_claim) assert ret["status"] == "ok" assert ret["device_certificate"] == device_certificate answer = DeviceClaimAnswerContent.decrypt_and_load_for( ret["encrypted_answer"], recipient_privkey=answer_private_key ) assert answer == DeviceClaimAnswerContent( private_key=alice.private_key, user_manifest_id=alice.user_manifest_id, user_manifest_key=alice.user_manifest_key, )
async def test_handshake_unknown_organization(running_backend, coolorg): unknown_org_addr = BackendOrganizationAddr.build( backend_addr=coolorg.addr, organization_id="dummy", root_verify_key=coolorg.root_verify_key) with pytest.raises(BackendConnectionRefused) as exc: async with backend_anonymous_cmds_factory(unknown_org_addr) as cmds: await cmds.ping() assert str(exc.value) == "Unknown Organization or Device"
async def test_handshake_rvk_mismatch(running_backend, coolorg, otherorg): bad_rvk_org_addr = BackendOrganizationAddr.build( backend_addr=coolorg.addr, organization_id=coolorg.organization_id, root_verify_key=otherorg.root_verify_key, ) with pytest.raises(BackendConnectionRefused) as exc: async with backend_anonymous_cmds_factory(bad_rvk_org_addr) as cmds: await cmds.ping() assert str( exc.value ) == "Root verify key for organization differs between client and server"
async def _claim_user(config, backend_addr, token, new_device_id, password, pkcs11): async with backend_anonymous_cmds_factory(backend_addr) as cmds: async with spinner("Waiting for referee to reply"): device = await actual_claim_user(cmds, new_device_id, token) device_display = click.style(new_device_id, fg="yellow") with operation(f"Saving locally {device_display}"): if pkcs11: token_id = click.prompt("PCKS11 token id", type=int) key_id = click.prompt("PCKS11 key id", type=int) save_device_with_pkcs11(config.config_dir, device, token_id, key_id) else: save_device_with_password(config.config_dir, device, password)
async def _mallory_claim(): async with backend_anonymous_cmds_factory(mallory.organization_addr) as cmds: rep = await cmds.user_get_invitation_creator(mallory.user_id) assert rep["trustchain"] == {"devices": [], "revoked_users": [], "users": []} creator = UserCertificateContent.unsecure_load(rep["user_certificate"]) creator_device = DeviceCertificateContent.unsecure_load(rep["device_certificate"]) assert creator_device.device_id.user_id == creator.user_id encrypted_claim = UserClaimContent( device_id=mallory.device_id, token=token, public_key=mallory.public_key, verify_key=mallory.verify_key, ).dump_and_encrypt_for(recipient_pubkey=creator.public_key) with trio.fail_after(1): rep = await cmds.user_claim(mallory.user_id, encrypted_claim) assert rep["status"] == "ok"
async def _bootstrap_organization( debug, device_id, organization_bootstrap_addr, config_dir, force, password ): root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = organization_bootstrap_addr.generate_organization_addr(root_verify_key) device_display = click.style(device_id, fg="yellow") device = generate_new_device(device_id, organization_addr, True) with operation(f"Creating locally {device_display}"): save_device_with_password(config_dir, device, password, force=force) now = pendulum.now() user_certificate = UserCertificateContent( author=None, timestamp=now, user_id=device.user_id, public_key=device.public_key, is_admin=device.is_admin, ).dump_and_sign(root_signing_key) device_certificate = DeviceCertificateContent( author=None, timestamp=now, device_id=device_id, verify_key=device.verify_key ).dump_and_sign(root_signing_key) async with spinner(f"Sending {device_display} to server"): async with backend_anonymous_cmds_factory(organization_bootstrap_addr) as cmds: await cmds.organization_bootstrap( organization_bootstrap_addr.organization_id, organization_bootstrap_addr.token, root_verify_key, user_certificate, device_certificate, ) organization_addr_display = click.style(organization_addr.to_url(), fg="yellow") click.echo(f"Organization url: {organization_addr_display}")
async def test_backend_closed_cmds(running_backend, coolorg): async with backend_anonymous_cmds_factory(coolorg.addr) as cmds: pass with pytest.raises(trio.ClosedResourceError): await cmds.ping()
async def claim_user( organization_addr: BackendOrganizationAddr, new_device_id: DeviceID, token: str, keepalive: Optional[int] = None, ) -> LocalDevice: """ Raises: InviteClaimError InviteClaimBackendOfflineError InviteClaimValidationError InviteClaimPackingError InviteClaimCryptoError """ new_device = generate_new_device(new_device_id, organization_addr) 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_user_invitation_creator( cmds, new_device.root_verify_key, new_device.user_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 = UserClaimContent( device_id=new_device_id, token=token, public_key=new_device.public_key, verify_key=new_device.verify_key, ).dump_and_encrypt_for( recipient_pubkey=invitation_creator_user.public_key) except DataError as exc: raise InviteClaimError( f"Cannot generate user claim message: {exc}") from exc # 3) Send claim rep = await cmds.user_claim(new_device_id.user_id, encrypted_claim) if rep["status"] != "ok": raise InviteClaimError(f"Cannot claim user: {rep}") # 4) Verify user&device certificates and check admin status try: user = UserCertificateContent.verify_and_load( rep["user_certificate"], author_verify_key=invitation_creator_device.verify_key, expected_author=invitation_creator_device.device_id, expected_user=new_device_id.user_id, ) 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, ) new_device = new_device.evolve(is_admin=user.is_admin) except DataError as exc: raise InviteClaimCryptoError(str(exc)) from exc except BackendNotAvailable as exc: raise InviteClaimBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise InviteClaimError(f"Cannot claim user: {exc}") from exc return new_device
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(), )
async def anonymous_backend_cmds(running_backend, coolorg): async with backend_anonymous_cmds_factory(coolorg.addr) as cmds: yield cmds
async def claim_task(): async with backend_anonymous_cmds_factory(alice_device.organization_addr) as cmds: bob_device = await retry(claim_user, cmds, bob_device_id, token) save_device_with_password(config_dir, bob_device, password, force=force)
async def amain( backend_address="ws://*****:*****@laptop", bob_device_id="bob@laptop", other_device_name="pc", alice_workspace="alicews", bob_workspace="bobws", password="******", administrator_token=DEFAULT_ADMINISTRATOR_TOKEN, force=False, ): configure_logging("WARNING") config_dir = get_default_config_dir(os.environ) organization_id = OrganizationID(organization_id) backend_address = BackendAddr(backend_address) alice_device_id = DeviceID(alice_device_id) bob_device_id = DeviceID(bob_device_id) alice_slugid = f"{organization_id}:{alice_device_id}" bob_slugid = f"{organization_id}:{bob_device_id}" # Create organization async with backend_administrator_cmds_factory(backend_address, administrator_token) as cmds: bootstrap_token = await cmds.organization_create(organization_id) organization_bootstrap_addr = BackendOrganizationBootstrapAddr.build( backend_address, organization_id, bootstrap_token ) # Bootstrap organization and Alice user async with backend_anonymous_cmds_factory(organization_bootstrap_addr) as cmds: root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = organization_bootstrap_addr.generate_organization_addr(root_verify_key) alice_device = generate_new_device(alice_device_id, organization_addr) save_device_with_password(config_dir, alice_device, password, force=force) now = pendulum.now() certified_user = certify_user( None, root_signing_key, alice_device.user_id, alice_device.public_key, now ) certified_device = certify_device( None, root_signing_key, alice_device_id, alice_device.verify_key, now ) await cmds.organization_bootstrap( organization_bootstrap_addr.organization_id, organization_bootstrap_addr.bootstrap_token, root_verify_key, certified_user, certified_device, ) # Create a workspace for Alice config = load_config(config_dir, debug="DEBUG" in os.environ) async with logged_core_factory(config, alice_device) as core: await core.fs.workspace_create(f"/{alice_workspace}") # Register a new device for Alice token = generate_invitation_token() other_alice_device_id = DeviceID("@".join((alice_device.user_id, other_device_name))) other_alice_slugid = f"{organization_id}:{other_alice_device_id}" async def invite_task(): async with backend_cmds_factory( alice_device.organization_addr, alice_device.device_id, alice_device.signing_key ) as cmds: await invite_and_create_device(alice_device, cmds, other_device_name, token) async def claim_task(): async with backend_anonymous_cmds_factory(alice_device.organization_addr) as cmds: other_alice_device = await retry(claim_device, cmds, other_alice_device_id, token) save_device_with_password(config_dir, other_alice_device, password, force=force) async with trio.open_nursery() as nursery: nursery.start_soon(invite_task) nursery.start_soon(claim_task) # Invite Bob in token = generate_invitation_token() async def invite_task(): async with backend_cmds_factory( alice_device.organization_addr, alice_device.device_id, alice_device.signing_key ) as cmds: await invite_and_create_user( alice_device, cmds, bob_device_id.user_id, token, is_admin=True ) async def claim_task(): async with backend_anonymous_cmds_factory(alice_device.organization_addr) as cmds: bob_device = await retry(claim_user, cmds, bob_device_id, token) save_device_with_password(config_dir, bob_device, password, force=force) async with trio.open_nursery() as nursery: nursery.start_soon(invite_task) nursery.start_soon(claim_task) # Create bob workspace and share with Alice bob_device = load_device_with_password( config.config_dir, organization_id, bob_device_id, password ) async with logged_core_factory(config, bob_device) as core: await core.fs.workspace_create(f"/{bob_workspace}") await core.fs.share(f"/{bob_workspace}", alice_device_id.user_id) # Share Alice workspace with bob async with logged_core_factory(config, alice_device) as core: await core.fs.share(f"/{alice_workspace}", bob_device_id.user_id) # Print out click.echo( f""" Mount alice and bob drives using: $ parsec core run -P {password} -D {alice_slugid} $ parsec core run -P {password} -D {other_alice_slugid} $ parsec core run -P {password} -D {bob_slugid} """ )
async def _do_bootstrap_organization( config_dir, password: str, password_check: str, user_id: str, device_name: str, bootstrap_addr: BackendOrganizationBootstrapAddr, ): if password != password_check: raise JobResultError("password-mismatch") if len(password) < 8: raise JobResultError("password-size") try: device_id = DeviceID(f"{user_id}@{device_name}") except ValueError as exc: raise JobResultError("bad-device_name") from exc root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = bootstrap_addr.generate_organization_addr( root_verify_key) try: device = generate_new_device(device_id, organization_addr, is_admin=True) save_device_with_password(config_dir, device, password) except LocalDeviceAlreadyExistsError as exc: raise JobResultError("user-exists") from exc now = pendulum.now() user_certificate = UserCertificateContent( author=None, timestamp=now, user_id=device.user_id, public_key=device.public_key, is_admin=device.is_admin, ).dump_and_sign(root_signing_key) device_certificate = DeviceCertificateContent( author=None, timestamp=now, device_id=device_id, verify_key=device.verify_key).dump_and_sign(root_signing_key) try: async with backend_anonymous_cmds_factory(bootstrap_addr) as cmds: rep = await cmds.organization_bootstrap( bootstrap_addr.organization_id, bootstrap_addr.token, root_verify_key, user_certificate, device_certificate, ) if rep["status"] == "already_bootstrapped": raise JobResultError("already-bootstrapped", info=str(rep)) elif rep["status"] == "not_found": raise JobResultError("invalid-url", info=str(rep)) elif rep["status"] != "ok": raise JobResultError("refused-by-backend", info=str(rep)) return device, password except BackendConnectionRefused as exc: raise JobResultError("invalid-url", info=str(exc)) from exc except BackendNotAvailable as exc: raise JobResultError("backend-offline", info=str(exc)) from exc except BackendConnectionError as exc: raise JobResultError("refused-by-backend", info=str(exc)) from exc
async def test_handshake_organization_expired(running_backend, expiredorg): with pytest.raises(BackendConnectionRefused) as exc: async with backend_anonymous_cmds_factory(expiredorg.addr) as cmds: await cmds.ping() assert str(exc.value) == "Trial organization has expired"
async def initialize_test_organization( backend_address, organization_id, alice_device_id, bob_device_id, other_device_name, alice_workspace, bob_workspace, password, administration_token, force, ): configure_logging("WARNING") config_dir = get_default_config_dir(os.environ) alice_slugid = f"{organization_id}:{alice_device_id}" bob_slugid = f"{organization_id}:{bob_device_id}" # Create organization async with backend_administration_cmds_factory( backend_address, administration_token) as cmds: rep = await cmds.organization_create(organization_id) assert rep["status"] == "ok" bootstrap_token = rep["bootstrap_token"] organization_bootstrap_addr = BackendOrganizationBootstrapAddr.build( backend_address, organization_id, bootstrap_token) # Bootstrap organization and Alice user async with backend_anonymous_cmds_factory( organization_bootstrap_addr) as cmds: root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = organization_bootstrap_addr.generate_organization_addr( root_verify_key) alice_device = generate_new_device(alice_device_id, organization_addr, True) save_device_with_password(config_dir, alice_device, password, force=force) now = pendulum.now() user_certificate = UserCertificateContent( author=None, timestamp=now, user_id=alice_device.user_id, public_key=alice_device.public_key, is_admin=True, ).dump_and_sign(author_signkey=root_signing_key) device_certificate = DeviceCertificateContent( author=None, timestamp=now, device_id=alice_device.device_id, verify_key=alice_device.verify_key, ).dump_and_sign(author_signkey=root_signing_key) rep = await cmds.organization_bootstrap( organization_bootstrap_addr.organization_id, organization_bootstrap_addr.token, root_verify_key, user_certificate, device_certificate, ) assert rep["status"] == "ok" # Create a workspace for Alice config = load_config(config_dir, debug="DEBUG" in os.environ) async with logged_core_factory(config, alice_device) as core: alice_ws_id = await core.user_fs.workspace_create(f"{alice_workspace}") await core.user_fs.sync() # Register a new device for Alice token = generate_invitation_token() other_alice_device_id = DeviceID( f"{alice_device.user_id}@{other_device_name}") other_alice_slugid = f"{organization_id}:{other_alice_device_id}" async def invite_task(): await invite_and_create_device(alice_device, other_device_name, token) other_alice_device = None async def claim_task(): nonlocal other_alice_device other_alice_device = await retry_claim(claim_device, alice_device.organization_addr, other_alice_device_id, token) save_device_with_password(config_dir, other_alice_device, password, force=force) async with trio.open_service_nursery() as nursery: nursery.start_soon(invite_task) nursery.start_soon(claim_task) # Invite Bob in token = generate_invitation_token() bob_device = None async def invite_task(): await invite_and_create_user(alice_device, bob_device_id.user_id, token, is_admin=False) async def claim_task(): nonlocal bob_device bob_device = await retry_claim(claim_user, alice_device.organization_addr, bob_device_id, token) save_device_with_password(config_dir, bob_device, password, force=force) async with trio.open_service_nursery() as nursery: nursery.start_soon(invite_task) nursery.start_soon(claim_task) # Create bob workspace and share with Alice async with logged_core_factory(config, bob_device) as core: bob_ws_id = await core.user_fs.workspace_create(f"{bob_workspace}") await core.user_fs.workspace_share(bob_ws_id, alice_device_id.user_id, WorkspaceRole.MANAGER) # Share Alice workspace with bob async with logged_core_factory(config, alice_device) as core: await core.user_fs.workspace_share(alice_ws_id, bob_device_id.user_id, WorkspaceRole.MANAGER) # Synchronize every device for device in (alice_device, other_alice_device, bob_device): async with logged_core_factory(config, device) as core: await core.user_fs.process_last_messages() await core.user_fs.sync() return alice_slugid, other_alice_slugid, bob_slugid
async def test_backend_switch_offline(running_backend, coolorg): async with backend_anonymous_cmds_factory(coolorg.addr) as cmds: await cmds.ping() with running_backend.offline(): with pytest.raises(BackendNotAvailable): await cmds.ping()
async def test_ping(running_backend, coolorg): async with backend_anonymous_cmds_factory(coolorg.addr) as cmds: rep = await cmds.ping("Hello World !") assert rep == {"status": "ok", "pong": "Hello World !"}
def _cmds_factory(keepalive): return backend_anonymous_cmds_factory(coolorg.addr, keepalive=keepalive)