def generate_keys(): """ Generates the public and private keys necessary for encryption and signing. """ sodium_client = SodiumClient() [sign_pk, sign_sk] = sodium_client.sign_generate_keypair() [box_pk, box_sk] = sodium_client.box_generate_keypair() return EncryptionKeys(sign_pk, sign_sk, box_pk, box_sk)
def get(self, request): auth_token = request['system_authtoken'] friendly_name = get_deployment_friendly_name(auth_token) encryption_context = SplunkEncryptionContext( auth_token, constants.SPACEBRIDGE_APP_NAME, SodiumClient(LOGGER)) mdm_sign_public_key = get_mdm_public_signing_key(auth_token) mdm_keypair_generation_time = get_mdm_update_timestamp( request, auth_token) return { 'payload': { constants.DEPLOYMENT_FRIENDLY_NAME: friendly_name, constants.SIGN_PUBLIC_KEY: py23.b64encode_to_str(encryption_context.sign_public_key()), constants.DEPLOYMENT_ID: encryption_context.sign_public_key( transform=encryption_context.generichash_hex), constants.ENCRYPT_PUBLIC_KEY: py23.b64encode_to_str(encryption_context.encrypt_public_key()), constants.SERVER_VERSION: str(app_version()), constants.MDM_SIGN_PUBLIC_KEY: mdm_sign_public_key, constants.MDM_KEYPAIR_GENERATION_TIME: mdm_keypair_generation_time }, 'status': 200 }
def __init__(self, encryption_keys, sodium_client=None): """ Args: encryption_keys ([EncryptionKeys]): User must generate encryption keys using the generate_keys method and provide keys in the context. It's up the user to persist the keys themselves between sessions. sodium_client ([type], optional): [description]. Defaults to None. """ self.mode = SdkMode.STANDALONE if sodium_client: self.sodium_client = sodium_client else: self.sodium_client = SodiumClient() self._key_cache = encryption_keys.__dict__
def __init__(self, session_key, app_name, sodium_client=None): """ Pass a session token and create a KV Store Handler to be able to write and fetch public keys from KV Store. The session token itself is not exposed, only the handler. """ self.mode = SdkMode.SPLUNK if sodium_client: self.sodium_client = sodium_client else: self.sodium_client = SodiumClient() self._key_cache = {} self.session_key = session_key self.app_name = app_name self.generate_keys()
def delete_device_from_spacebridge(device_id, system_authtoken, key_bundle=None): """ Deletes device from spacebridge :param device_id: :param system_authtoken: :return: response from spacebridge """ sodium_client = SodiumClient(LOGGER.getChild("sodium_client")) encryption_context = SplunkEncryptionContext( system_authtoken, constants.SPACEBRIDGE_APP_NAME, sodium_client) public_key_hash = encryption_context.sign_public_key( transform=encryption_context.generichash_hex) unregister_proto = http_pb2.DeviceUnregistrationRequest() unregister_proto.deviceId = b64decode(device_id) unregister_proto.deploymentId = encryption_context.sign_public_key( transform=encryption_context.generichash_raw) headers = { 'Authorization': public_key_hash, 'Content-Type': 'application/x-protobuf' } with requests_ssl_context(key_bundle): try: response = requests.delete( "%s/api/session" % config.get_spacebridge_domain(), headers=headers, proxies=config.get_proxies(), data=unregister_proto.SerializeToString()) except Exception: LOGGER.exception( "Exception attempting sending delete device request to Spacebridge" ) raise Errors.SpacebridgeServerError('Unable to reach Spacebridge', 503) LOGGER.info( "Received response=%s on delete device from Spacebridge request" % response.status_code) spacebridge_response = http_pb2.DeviceUnregistrationResponse() spacebridge_response.ParseFromString(response.content) LOGGER.info('Spacebridge response: %s' % str(spacebridge_response)) if spacebridge_response.HasField( 'error' ) and spacebridge_response.error.code != http_pb2.HttpError.Code.Value( 'ERROR_ROUTING_UNDELIVERABLE'): raise Errors.SpacebridgeServerError( "Spacebridge error on delete device request=%s" % spacebridge_response.error.message) return response
def process_subscriptions(request_context, async_client_factory, encryption_context=None, tv_device_ids=None, tv_subscriptions=None, ipad_subscriptions=None): """ Function to send subscription updates to the appropriate tv and all ipads registered to the user :param tv_config: tv config to send :param request_context: request context used to make kvstore request :param async_client_factory: used to create spacebridge and kvstore client """ async_kvstore_client = async_client_factory.kvstore_client() if not tv_subscriptions: tv_subscriptions = yield fetch_subscriptions( request_context.auth_header, async_kvstore_client, user_list=[request_context.current_user], subscription_type=constants.DRONE_MODE_TV, device_ids=tv_device_ids) LOGGER.debug( 'Active subscriptions%s: %s', ' for tv device_ids={}'.format(tv_device_ids) if tv_device_ids else '', tv_subscriptions) if tv_subscriptions: # create encryption context if not passed in if not encryption_context: sodium_client = SodiumClient() encryption_context = SplunkEncryptionContext( request_context.system_auth_header.session_token, constants.SPACEBRIDGE_APP_NAME, sodium_client) tv_subscription_tuples = yield build_tv_subscription_updates( tv_subscriptions, request_context, async_client_factory.kvstore_client()) yield send_updates(tv_subscription_tuples, encryption_context, async_client_factory, request_context, constants.DRONE_MODE_TV) # Fetch active iPad subscriptions if not ipad_subscriptions: ipad_subscriptions = yield fetch_subscriptions( request_context.auth_header, async_kvstore_client, user_list=[request_context.current_user], subscription_type=constants.DRONE_MODE_IPAD) if ipad_subscriptions: ipad_subscription_tuples = yield build_ipad_subscription_updates( ipad_subscriptions, request_context, async_client_factory.kvstore_client()) yield send_updates(ipad_subscription_tuples, encryption_context, async_client_factory, request_context, constants.DRONE_MODE_IPAD)
def do_run(self, input_config): """ Spins up a websocket connection Spacebridge and begins the reactor loops """ shard_id = default_shard_id() self.logger.info("Starting libsodium child process") sodium_logger = self.logger.getChild('sodium_client') sodium_logger.setLevel(logging.WARN) sodium_client = SodiumClient(sodium_logger) encryption_context = SplunkEncryptionContext( self.session_key, constants.SPACEBRIDGE_APP_NAME, sodium_client) self.logger.info( "Running Splunk Cloud Gateway modular input on search head, shard_id=%s", shard_id) # Fetch load balancer address if configured, otherwise use default URI try: uri = get_uri(self.session_key) self.logger.debug( "Successfully verified load_balancer_address={}".format(uri)) except Exception as e: self.logger.exception( "Failed to verify load_balancer_address. {}".format(e)) if not uri: return try: ensure_deployment_friendly_name(self.session_key) async_client_factory = AsyncClientFactory(uri) cloudgateway_message_handler = CloudgatewayMessageHandler( SplunkAuthHeader(self.session_key), logger=self.logger, encryption_context=encryption_context, async_client_factory=async_client_factory, shard_id=shard_id) client = CloudGatewayWsClient( encryption_context, message_handler=cloudgateway_message_handler, mode=WebsocketMode.ASYNC, logger=self.logger, config=config, shard_id=shard_id) client.connect() except Exception as e: self.logger.exception( "Exception connecting to cloud gateway={0}".format(e))
def process_mpc_broadcast_request(request_context, client_single_request, single_server_response, async_client_factory): """ This Method will process a MPC Broadcast Request :param request_context: Used to authenticate kvstore requests :param client_single_request: client request object protobuf :param single_server_response: server response object protobuf :param async_client factory: factory class used to generate kvstore and spacebridge clients """ async_kvstore_client = async_client_factory.kvstore_client() device_id = client_single_request.startMPCBroadcastRequest.deviceId subscriptions = yield fetch_subscriptions(request_context.auth_header, async_kvstore_client, user_list=[request_context.current_user], subscription_type=constants.DRONE_MODE_TV, device_ids=[device_id]) if not subscriptions: raise SpacebridgeApiRequestError('No active subscriptions for device_id={}' .format(device_id), status_code=http.BAD_REQUEST) async_spacebridge_client = async_client_factory.spacebridge_client() active_subscription = subscriptions[0] sodium_client = SodiumClient() encryption_context = SplunkEncryptionContext(request_context.system_auth_header.session_token, constants.SPACEBRIDGE_APP_NAME, sodium_client) start_broadcast = StartMPCBroadcast(device_id=device_id) subscription_update = DroneModeTVEvent(data_object=start_broadcast, event_type=TVEventType.MPC_BROADCAST) response, subscription_key = yield send_drone_mode_subscription_update(request_context.system_auth_header, active_subscription, subscription_update, encryption_context, async_spacebridge_client, async_kvstore_client) yield check_and_raise_error(response, request_context, 'MPC Broadcast Request') single_server_response.startMPCBroadcastResponse.SetInParent() LOGGER.info('Successfully sent mpc broadcast message to device_id=%s with subscription_key=%s', device_id, subscription_key)
def do_run(self, input_config): """ This will spin up a drone mode subscription manager and begins the reactor loop :param input_config: :return: """ try: sodium_client = SodiumClient(self.logger.getChild('sodium_client')) encryption_context = SplunkEncryptionContext( self.session_key, SPACEBRIDGE_APP_NAME, sodium_client) self.logger.debug( "Running Drone Mode Subscription Manager modular input") # Fetch load balancer address if configured, otherwise use default URI try: uri = get_uri(self.session_key) self.logger.debug( "Successfully verified load_balancer_address=%s", uri) except Exception: self.logger.exception( "Failed to verify load_balancer_address.") if not uri: return subscription_manager = DroneModeSubscriptionManager( input_config=input_config, encryption_context=encryption_context, session_key=self.session_key, async_splunk_client=AsyncSplunkClient(uri), parent_process_monitor=ParentProcessMonitor(), cluster_monitor=ClusterMonitor( self.logger, interval=config.get_cluster_monitor_interval())) subscription_manager.run() except Exception as e: self.logger.exception( 'An error occurred running the drone mode subscription modular input' ) raise e
class EncryptionContext(object): """Base class for encryption which provides utilities such as getters for the public and private keys for encryption and signing. Does not offer any out of the box mechanism for persisting keys. Sub classes such as SplunkEncryptionContext can provide specific implementations for persistence depending on the persistence mechanism. """ def __init__(self, encryption_keys, sodium_client=None): """ Args: encryption_keys ([EncryptionKeys]): User must generate encryption keys using the generate_keys method and provide keys in the context. It's up the user to persist the keys themselves between sessions. sodium_client ([type], optional): [description]. Defaults to None. """ self.mode = SdkMode.STANDALONE if sodium_client: self.sodium_client = sodium_client else: self.sodium_client = SodiumClient() self._key_cache = encryption_keys.__dict__ def get_encryption_keys(self): """Getter for encryption keys Returns: [EncryptionKeys]: Wrapper object. Also the same object that the constructor expects. The user should persist this object and load it into the constructor on future sessions. """ return EncryptionKeys(self._key_cache[SIGN_PUBLIC_KEY], self._key_cache[SIGN_PRIVATE_KEY], self._key_cache[ENCRYPT_PUBLIC_KEY], self._key_cache[ENCRYPT_PRIVATE_KEY]) def __noop(input): return input def generichash_hex(self, input): raw = self.generichash_raw(input) if sys.version_info < (3, 0): return raw.encode('hex') else: return raw.hex() def generichash_raw(self, input): return self.sodium_client.hash_generic(input) def sign_public_key(self, transform=__noop): """ :param transform: a function to transform the public key into another representation, default noop :return: Signing public key from KV Store. Requires key to have been generated. This is the splApp's :raises splunk.RESTException: The key cache hasn't been initialized "identity" key. """ key = self._key_cache[SIGN_PUBLIC_KEY] return transform(key) def sign_private_key(self): """ Fetch signing private key from KV Store. Requires key to have been generated """ key = self._key_cache[SIGN_PRIVATE_KEY] return key def encrypt_public_key(self): """ Fetch encryption public key from KV Store. Requires key to have been generated """ key = self._key_cache[ENCRYPT_PUBLIC_KEY] return key def encrypt_private_key(self): """ Fetch encryption public key from KV Store. Requires key to have been generated """ key = self._key_cache[ENCRYPT_PRIVATE_KEY] return key def secure_session_token(self, session_token): """ :param session_token: string representing session token :return: base64 encoded encrypted session token """ public_key = self.encrypt_public_key() private_key = self.encrypt_private_key() ciphertext = encrypt_session_token(self.sodium_client, session_token, public_key, private_key) return base64.b64encode(ciphertext)
def send_mdm_signing_key_to_spacebridge(authtoken, mdm_public_signing_key, key_bundle=None): """ Send the mdm public signing key to spacebridge Abstraction layer for the spacebridge request. This function: 1. Creates the mdm_credentials_bundle 2. Serializes the bundle to bytes 3. Signs the serialized bundle with the splapps private signing key 4. Creates a proto object with the serialized bundle + signature and sends to spacebridge 5. Parses the protobuf response, checking for error objects """ sodium_client = SodiumClient(LOGGER.getChild("sodium_client")) encryption_context = SplunkEncryptionContext( authtoken, constants.SPACEBRIDGE_APP_NAME, sodium_client) sign_func = partial(sign_detached, sodium_client, encryption_context.sign_private_key()) client_id = encryption_context.sign_public_key( transform=encryption_context.generichash_raw) request_proto = http_pb2.MdmAuthenticationGrantRequest() client_mdm_permission = request_proto.ClientMdmPermission() client_mdm_permission.clientId = client_id client_mdm_permission.mdmPublicKeyForSigning = mdm_public_signing_key serialized = client_mdm_permission.SerializeToString() signature = sign_func(serialized) request_proto.clientMdmPermission = serialized request_proto.signature = signature headers = { 'Authorization': encryption_context.sign_public_key( transform=encryption_context.generichash_hex), 'Content-Type': 'application/x-protobuf' } def call_grants(): with requests_ssl_context(key_bundle) as cert: return requests.post('{}/api/mdm/grants'.format( config.get_spacebridge_domain()), headers=headers, data=request_proto.SerializeToString(), proxies=config.get_proxies(), timeout=constants.TIMEOUT_SECONDS, cert=cert.name) try: response = call_grants() except requests.exceptions.Timeout as e: raise Errors.SpacebridgeServerError( 'Failed to receive a response from spacebridge', 500) sb_response_proto = http_pb2.MdmAuthenticationGrantResponse() sb_response_proto.ParseFromString(response.content) retries = 0 while 500 <= response.status_code < 600 and retries < 3: wait = 2**retries retries = retries + 1 time.sleep(wait) try: response = call_grants() except requests.exceptions.Timeout as e: raise Errors.SpacebridgeServerError( 'Failed to receive a response from spacebridge', 500) sb_response_proto = http_pb2.MdmAuthenticationGrantResponse() sb_response_proto.ParseFromString(response.content) if sb_response_proto.HasField('error'): if response.status_code == 500: raise Errors.SpacebridgeServerError( 'Spacebridge encountered an internal error:{}'.format( sb_response_proto.error.message), 500) raise Errors.SpacebridgeServerError( 'Spacebridge request error: {}'.format( sb_response_proto.error.message, response.status_code)) if not (200 <= response.status_code < 300): raise Errors.SpacebridgeServerError( "Spacebridge error: {}".format(response.content), response.status_code)
class SplunkEncryptionContext(EncryptionContext): """ Context object for handling generating and fetching public and private keys. (Currently only public key is supported) """ def __init__(self, session_key, app_name, sodium_client=None): """ Pass a session token and create a KV Store Handler to be able to write and fetch public keys from KV Store. The session token itself is not exposed, only the handler. """ self.mode = SdkMode.SPLUNK if sodium_client: self.sodium_client = sodium_client else: self.sodium_client = SodiumClient() self._key_cache = {} self.session_key = session_key self.app_name = app_name self.generate_keys() def set_encryption_keys(self, keys_dict): self._key_cache = keys_dict def _cache_keys(self): result = False raw_data = splunk_client.fetch_sensitive_data(self.session_key, ENCRYPTION_KEYS, self.app_name) try: self._key_cache = EncryptionKeys.from_json(json.loads(raw_data)).__dict__ except Exception: self._key_cache = {} if SIGN_PUBLIC_KEY in self._key_cache: result = True return result def _create_key_bucket(self): try: splunk_client.create_sensitive_data(self.session_key, ENCRYPTION_KEYS, '{}', self.app_name) except RESTException as e: if e.statusCode != 409: raise e def generate_keys(self): """ Stores the required signing and encryption keys to KV Store in the meta collection. If the public key already exists, then this is a no op. There is a synchronization issue where when you call generate keys, KV Store might not yet have been initialized. The way we handle this is if we get a 503 HTTP error back from KV Store, this means the store is not yet available, in which case we callback generate keys to run in 5 seconds. """ self._create_key_bucket() keys_cached = self._cache_keys() if not keys_cached: [sign_pk, sign_sk] = [k for k in self.sodium_client.sign_generate_keypair()] [box_pk, box_sk] = [k for k in self.sodium_client.box_generate_keypair()] encryption_keys = EncryptionKeys(sign_pk, sign_sk, box_pk, box_sk) key_data = json.dumps(encryption_keys.to_json()) splunk_client.update_sensitive_data(self.session_key, ENCRYPTION_KEYS, key_data, self.app_name) self._key_cache = encryption_keys.__dict__
def handle_query(auth_code, device_name, user, system_authtoken): """ Handler for the initial AuthenticationQueryRequest call. This function: 1. Makes the AuthenticationQueryRequest request to the server 2. Checks if app_type has been disabled 3. Stores a temporary record in the kvstore :param auth_code: User-entered authorization code to be returned to Spacebridge :param device_name: Name of the new device :return: Confirmation code to be displayed to user, and id of temporary kvstore record to be returned later """ LOGGER.info('Received new registration query request by user=%s' % user) # Makes the AuthenticationQueryRequest request to the server sodium_client = SodiumClient(LOGGER.getChild('sodium_client')) encryption_context = SplunkEncryptionContext(system_authtoken, SPACEBRIDGE_APP_NAME, sodium_client) client_device_info = authenticate_code(auth_code, encryption_context, resolve_app_name, config=config) app_name = client_device_info.app_name app_id = client_device_info.app_id platform = client_device_info.platform # if platform not set and we know platform based on app id, use that. if not platform and app_id in APP_ID_TO_PLATFORM_MAP: platform = get_app_platform(app_id) LOGGER.info("client_device_info={}".format(client_device_info)) user_devices = get_devices_for_user(user, system_authtoken) LOGGER.info("user_devices=%s" % user_devices) if any(device[DEVICE_NAME_LABEL] == device_name and device['device_type'] == app_name for device in user_devices): err_msg = ( 'Registration Error: user={} device_name={} of app_type={} already exists' .format(user, device_name, app_name)) LOGGER.info(err_msg) raise Errors.SpacebridgeRestError(err_msg, HTTPStatus.CONFLICT) # Stores a temporary record in the kvstore kvstore_unconfirmed = KvStore(UNCONFIRMED_DEVICES_COLLECTION_NAME, system_authtoken, owner=user) kvstore_payload = client_device_info.to_json() kvstore_payload['device_name'] = device_name kvstore_payload['device_type'] = app_name kvstore_payload['app_name'] = app_name kvstore_payload['app_id'] = app_id kvstore_payload['platform'] = platform _, content = kvstore_unconfirmed.insert_single_item(kvstore_payload) return { 'payload': { 'temp_key': json.loads(content)['_key'], 'conf_code': client_device_info.confirmation_code }, 'status': HTTPStatus.OK, }
def do_run(self, input_config): shard_id = default_shard_id() self.logger.info("Starting libsodium child process") sodium_logger = self.logger.getChild('sodium_client') sodium_logger.setLevel(logging.WARN) sodium_client = SodiumClient(sodium_logger) self.logger.info("Loading encryption context") encryption_context = SplunkEncryptionContext(self.session_key, SPACEBRIDGE_APP_NAME, sodium_client) self.logger.info( "Running Subscription Manager modular input on search head") # Fetch load balancer address if configured, otherwise use default URI try: uri = get_uri(self.session_key) self.logger.debug( "Successfully verified load_balancer_address={}".format(uri)) except Exception as e: self.logger.exception( "Failed to verify load_balancer_address. {}".format(e)) if not uri: return try: minimum_iteration_time_seconds = float(input_config[ self.input_config_key][self.minimum_iteration_time_seconds]) warn_threshold_seconds = float(input_config[self.input_config_key][ self.warn_threshold_seconds]) subscription_processor_parallelism_str = input_config[ self.input_config_key][self.subscription_processor_parallelism] subscription_parallelism = self._resolve_parallelism( subscription_processor_parallelism_str) except: self.logger.exception( "Failed to load required configuration values") return try: self.logger.info("Processing subscriptions with parallelism=%s", subscription_parallelism) auth_header = SplunkAuthHeader(self.session_key) self.logger.debug( "Using search processor python=%s, script=%s, args=%s", self.python_path, self.script_path, self.subprocess_args) start_job = start_job_using_subprocess(self.python_path, self.subprocess_args) if subscription_parallelism == self.CONFIG_VALUE_SINGLE_PROCESS: start_job = start_job_single_process(sodium_client, encryption_context) process_manager = ProcessManager(subscription_parallelism, start_job) job_context = JobContext( auth_header, uri, encryption_context.get_encryption_keys().to_json()) subscription_manager = SubscriptionManager( input_config=input_config, encryption_context=encryption_context, auth_header=auth_header, shard_id=shard_id, job_context=job_context, search_loader=loader.load_search_bundle, parent_process_monitor=ParentProcessMonitor(), minimum_iteration_time_seconds=minimum_iteration_time_seconds, warn_threshold_seconds=warn_threshold_seconds, process_manager=process_manager) subscription_manager.run() except: self.logger.exception( "Unhandled exception during subscription processing")
def process_tv_interaction_request(request_context, client_single_request, single_server_response, async_client_factory): """ This Method will process a MPC Broadcast Request :param request_context: Used to authenticate kvstore requests :param client_single_request: client request object protobuf :param single_server_response: server response object protobuf :param async_client factory: factory class used to generate kvstore and spacebridge clients """ async_kvstore_client = async_client_factory.kvstore_client() device_id = client_single_request.tvInteractionRequest.tvInteraction.device_id tv_interaction_proto = client_single_request.tvInteractionRequest.tvInteraction # determine type of interaction interaction_type = TVInteractionType.NONE speed = None if tv_interaction_proto.HasField(constants.SLIDESHOW_GOTO): interaction_type = TVInteractionType.GOTO elif tv_interaction_proto.HasField(constants.SLIDESHOW_STOP): interaction_type = TVInteractionType.STOP elif tv_interaction_proto.HasField(constants.SLIDESHOW_FORWARD): interaction_type = TVInteractionType.FORWARD elif tv_interaction_proto.HasField(constants.SLIDESHOW_BACK): interaction_type = TVInteractionType.BACK elif tv_interaction_proto.HasField(constants.SLIDESHOW_SPEED): interaction_type = TVInteractionType.SPEED speed = tv_interaction_proto.slideshow_speed.speed subscriptions = yield fetch_subscriptions(request_context.auth_header, async_kvstore_client, user_list=[request_context.current_user], subscription_type=constants.DRONE_MODE_TV, device_ids=[device_id]) if not subscriptions: raise SpacebridgeApiRequestError('No active subscriptions for device_id={}' .format(device_id), status_code=http.BAD_REQUEST) async_spacebridge_client = async_client_factory.spacebridge_client() active_subscription = subscriptions[0] sodium_client = SodiumClient() encryption_context = SplunkEncryptionContext(request_context.system_auth_header.session_token, constants.SPACEBRIDGE_APP_NAME, sodium_client) tv_interaction = TVInteraction(device_id=device_id, interaction_type=interaction_type, speed=speed) subscription_update = DroneModeTVEvent(data_object=tv_interaction, event_type=TVEventType.TV_INTERACTION) response, subscription_key = yield send_drone_mode_subscription_update(request_context.system_auth_header, active_subscription, subscription_update, encryption_context, async_spacebridge_client, async_kvstore_client) yield check_and_raise_error(response, request_context, 'MPC Broadcast Request') single_server_response.tvInteractionResponse.SetInParent() LOGGER.info('Successfully sent mpc broadcast message to device_id=%s with subscription_key=%s', device_id, subscription_key)
def __init__(self, command_line, command_arg): BaseRestHandler.__init__(self) self.sodium_client = SodiumClient(LOGGER.getChild('sodium_client'))
class DeploymentBundle(BaseRestHandler, PersistentServerConnectionApplication): """ Main class for handling the deployment bundle endpoint. Subclasses the spacebridge_app BaseRestHandler. """ def __init__(self, command_line, command_arg): BaseRestHandler.__init__(self) self.sodium_client = SodiumClient() def get(self, request): """ Handler which returns mdm signing public key """ response = {} try: kvstore_service = kvstore(collection=USER_META_COLLECTION_NAME, session_key=request[SESSION][AUTHTOKEN], owner=request[SESSION][USER]) result = json.loads( kvstore_service.get_item_by_key(MDM_KEYPAIR_GENERATION_TIME) [1]) response.update({TIMESTAMP: result[TIMESTAMP]}) except Exception as e: # If key not in kvstore if hasattr(e, 'statusCode') and e.statusCode == HTTPStatus.NOT_FOUND: return { 'payload': { 'message': 'Could not find mdm keypair update time in kvstore', 'status': HTTPStatus.NOT_FOUND } } return { 'payload': { 'message': str(e), 'status': HTTPStatus.BAD_REQUEST } } try: public_key = fetch_sensitive_data(request[SESSION][AUTHTOKEN], MDM_SIGN_PUBLIC_KEY) private_key = fetch_sensitive_data(request[SESSION][AUTHTOKEN], MDM_SIGN_PRIVATE_KEY) response.update({ 'sign_public_key': public_key, 'sign_private_key': private_key }) except Exception as e: # If key not in storage/passwords if hasattr(e, 'statusCode') and e.statusCode == HTTPStatus.NOT_FOUND: return { 'payload': { 'message': 'Could not find one or both of key={} and key={} in /storage/passwords' .format(MDM_SIGN_PUBLIC_KEY, MDM_SIGN_PRIVATE_KEY), 'status': HTTPStatus.NOT_FOUND } } return { 'payload': { 'message': str(e), 'status': HTTPStatus.BAD_REQUEST } } return {'payload': response, 'status': HTTPStatus.OK} def post(self, request): """ Handler which generates and returns an mdm keypair """ # generate mdm credentials LOGGER.info("Generating MDM Credentials") system_authtoken = request[SYSTEM_AUTHTOKEN] key_bundle = _load_key_bundle(system_authtoken) [public_key, private_key] = self.sodium_client.sign_generate_keypair() now = int(datetime.now().strftime('%s')) response = {} response['message'] = [] status = HTTPStatus.OK try: # send public signing key to spacebridge send_mdm_signing_key_to_spacebridge(request[SESSION][AUTHTOKEN], public_key, key_bundle) except Exception as e: status = HTTPStatus.INTERNAL_SERVER_ERROR LOGGER.warn( "Failed to register mdm keys with spacebridge. error=%s", e) return { 'payload': { 'failed_save': True, 'message': e }, 'status': status, } # update key generation timestamp try: kvstore_service = kvstore(collection=USER_META_COLLECTION_NAME, session_key=request[SESSION][AUTHTOKEN], owner=request[SESSION][USER]) entry = {KEY: MDM_KEYPAIR_GENERATION_TIME, TIMESTAMP: now} kvstore_service.insert_or_update_item_containing_key(entry) except Exception as e: status = HTTPStatus.INTERNAL_SERVER_ERROR response['failed_timesave'] = True response['message'].append(e.message) # store to storage/passwords try: [_, created_public_key] = update_or_create_sensitive_data( request[SESSION][AUTHTOKEN], MDM_SIGN_PUBLIC_KEY, py23.b64encode_to_str(public_key)) except Exception as e: status = HTTPStatus.INTERNAL_SERVER_ERROR response['failed_public_localsave'] = True response['message'].append(str(e)) try: [_, created_private_key] = update_or_create_sensitive_data( request[SESSION][AUTHTOKEN], MDM_SIGN_PRIVATE_KEY, py23.b64encode_to_str(private_key)) except Exception as e: status = HTTPStatus.INTERNAL_SERVER_ERROR response['failed_private_localsave'] = True response['message'].append(str(e)) # don't pass back the message if we have no errors if not response['message']: del response['message'] response[SIGN_PUBLIC_KEY] = py23.b64encode_to_str(public_key) response[SIGN_PRIVATE_KEY] = py23.b64encode_to_str(private_key) response[TIMESTAMP] = now return { 'payload': response, 'status': status, }
def handle_query(auth_code, device_name, user, system_authtoken): """ Handler for the initial AuthenticationQueryRequest call. This function: 1. Makes the AuthenticationQueryRequest request to the server 2. Checks if app_type has been disabled 3. Stores a temporary record in the kvstore :param auth_code: User-entered authorization code to be returned to Spacebridge :param device_name: Name of the new device :return: Confirmation code to be displayed to user, and id of temporary kvstore record to be returned later """ LOGGER.info('Received new registration query request by user=%s' % user) # Makes the AuthenticationQueryRequest request to the server sodium_client = SodiumClient(LOGGER.getChild('sodium_client')) encryption_context = SplunkEncryptionContext(system_authtoken, SPACEBRIDGE_APP_NAME, sodium_client) client_device_info = authenticate_code(auth_code, encryption_context, resolve_app_name, config=config) app_name = client_device_info.app_name app_id = client_device_info.app_id LOGGER.info("client_device_info={}".format(client_device_info)) user_devices = get_devices_for_user(user, system_authtoken) LOGGER.info("user_devices=%s" % user_devices) if any(device[DEVICE_NAME_LABEL] == device_name and device['device_type'] == app_name for device in user_devices): err_msg = ( 'Registration Error: user={} device_name={} of app_type={} already exists' .format(user, device_name, app_name)) LOGGER.info(err_msg) raise Errors.SpacebridgeRestError(err_msg, http.CONFLICT) # Checks if app_type has been disabled if not retrieve_state_of_app(app_name, system_authtoken): disabled_message = 'Registration Error: Application type app_name="{}" is disabled'.format( app_name) LOGGER.info(disabled_message) return { 'payload': { 'message': disabled_message, 'is_admin': user_is_administrator(user, system_authtoken), 'app_name': app_name, }, 'status': 422, } # Stores a temporary record in the kvstore kvstore_unconfirmed = KvStore(UNCONFIRMED_DEVICES_COLLECTION_NAME, system_authtoken, owner=user) kvstore_payload = client_device_info.to_json() kvstore_payload['device_name'] = device_name kvstore_payload['device_type'] = app_name kvstore_payload['app_id'] = app_id _, content = kvstore_unconfirmed.insert_single_item(kvstore_payload) return { 'payload': { 'temp_key': json.loads(content)['_key'], 'conf_code': client_device_info.confirmation_code }, 'status': http.OK, }
def handle_saml_mdm_request(self, user, session_token, system_authtoken, mdm_signing_bundle, body): """ Handles the MDM SAML Registration Request. Validates signature sent from client, validates session token, generates a JWT token, and sends it encrypted using splapp's keys and the client public key :param user: string provided by rest handler :param session_token: string :param system_authtoken: string :param mdm_signing_bundle: Object :param body: JSON :return: Reponse object with payload and status """ public_key = base64.b64decode( extract_parameter(body, PUBLIC_KEY_LABEL, BODY_LABEL)) mdm_signature = base64.b64decode( extract_parameter(body, MDM_SIGNATURE_LABEL, BODY_LABEL)) client_keys = EncryptionKeys(None, None, public_key, None) client_encryption_context = EncryptionContext(client_keys) try: valid_signature = yield sign_verify( SodiumClient(LOGGER.getChild("sodium_client")), base64.b64decode( mdm_signing_bundle['sign_public_key'].encode('utf8')), client_encryption_context.encrypt_public_key(), mdm_signature) except Exception as e: LOGGER.exception( "Exception verifying signature from client for user={}".format( user)) defer.returnValue({ 'payload': { 'token': "", 'user': user, 'status': http.UNAUTHORIZED }, 'status': http.OK }) async_splunk_client = self.async_client_factory.splunk_client() valid_request = yield valid_session_token(user, session_token, async_splunk_client) LOGGER.info( "Received new mdm registration request by user={}".format(user)) if valid_signature and valid_request: try: credentials = SplunkJWTCredentials(user) credentials.load_jwt_token(SplunkAuthHeader(system_authtoken)) LOGGER.info("Successfully fetched jwt token") except Exception as e: LOGGER.exception( "Exception fetching jwt token for user={} with message={}". format(user, e)) defer.returnValue({ 'payload': { 'token': "", 'user': user, 'status': 422 }, 'status': http.OK }) splapp_encryption_context = SplunkEncryptionContext( system_authtoken, constants.SPACEBRIDGE_APP_NAME, SodiumClient(LOGGER.getChild("sodium_client"))) # Encrypt session token using splapp keys secured_session_token = splapp_encryption_context.secure_session_token( credentials.get_credentials()) # Encrypt session token using client's given public key encrypted_jwt_token = yield encrypt_for_send( SodiumClient(LOGGER.getChild("sodium_client")), client_encryption_context.encrypt_public_key(), secured_session_token) base64_encrypted_jwt_token = base64.b64encode(encrypted_jwt_token) defer.returnValue({ 'payload': { 'token': base64_encrypted_jwt_token, 'user': user, 'status': http.OK }, 'status': http.OK }) else: LOGGER.info( "Error: Mismatched user={} and session token".format(user)) defer.returnValue({ 'payload': { 'token': "", 'user': user, 'status': http.UNAUTHORIZED }, 'status': http.OK })
def __init__(self, command_line, command_arg): BaseRestHandler.__init__(self) self.sodium_client = SodiumClient()
def run_search_process(job_contexts, sodium_client): d = task.deferLater(reactor, 0, _run, job_contexts, sodium_client) def handle_success(result): LOGGER.debug("Search job process finished") reactor.stop() def handle_error(error): LOGGER.error("Search job finished with error=%s", error) reactor.stop() d.addCallback(handle_success) d.addErrback(handle_error) LOGGER.debug("Starting reactor") reactor.run() sys.exit(0) if __name__ == "__main__": # entry point for single search processing LOGGER.debug("Starting subscription os process") try: SODIUM_CLIENT = SodiumClient() for line in fileinput.input(): pickle_format = base64.b64decode(line) input_contexts = pickle.loads(pickle_format) run_search_process(input_contexts, SODIUM_CLIENT) except Exception as e: LOGGER.exception("Failed to start subscription os process") raise e
def send_push_notifications(request_context, notification, recipient_devices, async_kvstore_client, async_spacebridge_client, async_splunk_client): """ Given a notification object and a list of device ids, sends a post request to the Spacebridge notifications api for each device id :param request_context: :param notification: notification object to be sent :param recipient_devices: list of device id strings :param async_kvstore_client: AsyncKVStoreClient :param async_spacebridge_client: AsyncSpacebridgeClient :return: """ sodium_client = SodiumClient(LOGGER.getChild('sodium_client')) sign_keys_response = yield async_splunk_client.async_get_sign_credentials( request_context.auth_header) if sign_keys_response[0] == http.OK: encryption_keys = EncryptionKeys( b64decode(sign_keys_response[1]['sign_public_key']), b64decode(sign_keys_response[1]['sign_private_key']), None, None) encryption_context = EncryptionContext(encryption_keys, sodium_client) else: LOGGER.exception( "Unable to fetch encryption keys with error_code={}".format( sign_keys_response[0])) raise EncryptionKeyError(sign_keys_response[1], sign_keys_response[0]) sender_id = encryption_context.sign_public_key( transform=encryption_context.generichash_raw) sender_id_hex = py23.encode_hex_str(sender_id) headers = { 'Content-Type': 'application/x-protobuf', 'Authorization': sender_id_hex } recipient_devices = [device.encode("utf8") for device in recipient_devices] deferred_responses = [] signer = partial(sign_detached, sodium_client, encryption_context.sign_private_key()) for device_id in recipient_devices: device_id_raw = b64decode(device_id) try: _, receiver_encrypt_public_key = yield public_keys_for_device( device_id_raw, request_context.auth_header, async_kvstore_client) encryptor = partial(encrypt_for_send, sodium_client, receiver_encrypt_public_key) LOGGER.info("Sending notification alert_id=%s, device_id=%s" % (notification.alert_id, device_id)) notification_request = notifications.build_notification_request( device_id, device_id_raw, sender_id, notification, encryptor, signer) # Send post request asynchronously deferred_responses.append( async_spacebridge_client.async_send_notification_request( auth_header=SpacebridgeAuthHeader(sender_id), data=notification_request.SerializeToString(), headers=headers)) except KeyNotFoundError: LOGGER.info("Public key not found for device_id=%s" % device_id) except SodiumOperationError: LOGGER.warn("Sodium operation failed! device_id=%s" % device_id) # Wait until all the post requests have returned deferred_list = yield defer.DeferredList(deferred_responses) responses = yield [response[1] for response in deferred_list] results = [(recipient_devices[i], responses[i].code, responses[i].text()) for i in range(len(responses))] LOGGER.info(("Finished sending push notifications with responses=%s" % str(results)))