""" (C) 2019 Splunk Inc. All rights reserved. APIs for device registration to Spacebridge """ import collections import sys from cloudgateway import py23 from cloudgateway.key_bundle import KeyBundle from cloudgateway.private.registration.authenticate import submit_auth_code, parse_spacebridge_response, \ verify_mdm_signature from cloudgateway.private.registration.pairing import build_encypted_credentials_bundle, pair_device_with_sb from cloudgateway.private.registration import unregister from cloudgateway.private.registration.client import make_device_authentication_request, \ parse_device_authentication_response, make_authentication_result_request, parse_authentication_result_response, \ parse_credentials_bundle from cloudgateway.private.encryption.encryption_handler import encrypt_for_send from cloudgateway.device import DeviceInfo, CredentialsBundle from cloudgateway.private.registration.util import sb_auth_header from cloudgateway.private.exceptions.rest import CloudgatewayServerError, CloudgatewayMaxRetriesError from cloudgateway.private.util.config import SplunkConfig from functools import partial from cloudgateway.private.util.tokens_util import calculate_token_info from spacebridge_protocol import http_pb2 def authenticate_code(auth_code, encryption_context, resolve_app_name, config=SplunkConfig(), key_bundle=None, mdm_signing_public_key=None): """ Part 1/2 of the registration process Submit an auth code to space bridge, and retrieve the encryption credentials for the device associated to that auth code :param auth_code: auth code shown on mobile device :param encryption_context: EncryptionContext object. Can be a regular EncryptionContext or a subclass such as SplunkEncryptionContext depending on whether you want to run in standalone mode or not. :param resolve_app_name: A function that, given an app id, will return a human friendly app name :param config: CloudgatewaySdkConfig object :return: DeviceInfo object """ raw_response = submit_auth_code(auth_code, encryption_context, config, key_bundle) sb_response_proto = parse_spacebridge_response(raw_response) encrypt_public_key = sb_response_proto.payload.publicKeyForEncryption sign_public_key = sb_response_proto.payload.publicKeyForSigning app_friendly_name = sb_response_proto.payload.appFriendlyName app_name = app_friendly_name if app_friendly_name else resolve_app_name(sb_response_proto.payload.appId) platform = sb_response_proto.payload.appPlatform if mdm_signing_public_key: verify_mdm_signature(sb_response_proto.payload, mdm_signing_public_key, encryption_context) device_encryption_info = DeviceInfo(encrypt_public_key, sign_public_key, sb_response_proto.payload.deviceId, encryption_context.generichash_hex(sign_public_key).upper()[:8], sb_response_proto.payload.appId, app_name=app_name, platform=platform) return device_encryption_info def pair_device(auth_code, user_auth_credentials, device_encryption_info, encryption_context, server_name="", server_app_id = "", config=SplunkConfig(), key_bundle=None, env_metadata=None): """ Part 2/2 of the registration process. Send splunk app's public key information and encrypted credentials for user to cloudgateway. Upon success, this will complete the credential swap as now the client has the splunk app's credentials and the splunk app has received the app's public key :param auth_code: auth code of the device :param user_auth_credentials: UserAuthCredentials object interface which captures different forms of session tokens :param device_encryption_info: DeviceInfo object which was returned in the authenticate_code api call :param encryption_context: EncryptionContext object. Can be a regular EncryptionContext or a subclass such as SplunkEncryptionContext depending on whether you want to run in standalone mode or not. :param server_name: optional parameter for name of server so that device can identify which instance it is paired with :param config: CloudgatewaySdkConfig object :return: """ user_auth_credentials.validate() sodium_client = encryption_context.sodium_client encrypt_public_key = encryption_context.encrypt_public_key() sign_public_key = encryption_context.sign_public_key() encryption_func = partial(encrypt_for_send, sodium_client, device_encryption_info.encrypt_public_key) auth_header = sb_auth_header(encryption_context) session_token = user_auth_credentials.get_credentials() if sys.version_info < (3, 0) else str.encode(user_auth_credentials.get_credentials()) token_expires_at = user_auth_credentials.get_expiration() token_type = user_auth_credentials.get_token_type() encrypted_session_token = encryption_context.secure_session_token(session_token) encrypted_credentials_bundle = build_encypted_credentials_bundle(user_auth_credentials.get_username(), encrypted_session_token, encryption_func, server_app_id, deployment_name=server_name, token_type=token_type, token_expires_at=token_expires_at, env_metadata=env_metadata) pair_device_with_sb(auth_code, auth_header, device_encryption_info.device_id, encrypt_public_key, sign_public_key, encrypted_credentials_bundle, config, key_bundle) def unregister_device(device_id, encryption_context, config=SplunkConfig(), key_bundle=None): """ Unregister ascljk a device from cloud gateway. Initiating this will cause cloud gateway to remove the routing entry and also force the client device to unregister :param device_id: device id from DeviceInfo object :param encryption_context: EncryptionContext object. Can be a regular EncryptionContext or a subclass such as SplunkEncryptionContext depending on whether you want to run in standalone mode or not. :return: DeviceUnregistrationResponse proto or throws an exception if the request can't be completed :param config: CloudgatewaySdkConfig object """ unregister_proto = unregister.build_device_unregister_req(device_id, encryption_context) raw_response = unregister.make_unregister_req(unregister_proto, sb_auth_header(encryption_context), config, key_bundle) unregister.parse_sb_response(raw_response) return raw_response def request_code(device_info, encryption_context, config=SplunkConfig(), key_bundle=None, mdm_encryption_context=None): """Submits device's public key information to Cloud Gateway and fetches 10 digit pairing code. First part of the registration process for the client. Args: device_info ([DeviceInfo]): device information of the client side encryption_context ([EncryptionContext]): Returns: [String]: 10-digit auth code """ r = make_device_authentication_request(device_info, encryption_context, config, key_bundle, mdm_encryption_context) return parse_device_authentication_response(r) def fetch_server_credentials(auth_code, encryption_context, num_retries=10, config=SplunkConfig(), key_bundle=None): """Fetch server's encryption keys as well as session token. This is the last part of the registration process for the client side and needs to happen after the server side has finished the registration process on the server side. If the server side has not completed registration yet, Cloudgateway will wait up to 30s and return a 204. This api will continue to retry when it receives a 204 until it we exceed num_retries at which point it will return a max retries exceeded exception. Args: auth_code ([string]): 10-digit auth code encryption_context ([EncryptionContext]): num_retries ([int]): Number of times to retry. Only retries if Cloud gateway has not received registration from the server side. Returns: [(DeviceInfo, CredentialsBundle)]: Tuple where first element is a DeviceInfo object containing encryption information of the server side. The second element is a CredentialsBundle object which contains a session token and username which is used for authenticating with the server side and running splunk queries. """ for i in range(num_retries): r = make_authentication_result_request(auth_code, encryption_context, config, key_bundle) try: payload = parse_authentication_result_response(r) break except CloudgatewayServerError as e: if e.status != 204: raise e if e.status == 204 and i == num_retries - 1: raise CloudgatewayMaxRetriesError("Server side device has not completed registration. Please try again", 204) signing_public_key = payload.deploymentPublicKeyForSigning encrypt_public_key = payload.deploymentPublicKeyForEncryption encrypted_credentials_bundle = payload.encryptedCredentialsBundle server_id = encryption_context.generichash_raw(signing_public_key) server_info = DeviceInfo(encrypt_public_key, signing_public_key, device_id=server_id) credentials_bundle = parse_credentials_bundle(encrypted_credentials_bundle, encryption_context) return server_info, credentials_bundle