def update(self, param, reset_failcount=True): """ update - process initialization parameters :param param: dict of initialization parameters :type param: dict :return: nothing """ if is_true(getParam(param, 'genkey', optional)): raise ParameterError("Generating OTP keys is not supported") upd_param = param.copy() # If the OTP key is given, it is given as a 496-character hex string which # encodes a 248-byte blob. As we want to set a 248-byte OTPKey (= Blob), # we unhexlify the OTP key if 'otpkey' in param: if len(param['otpkey']) != 496: raise ParameterError( 'Expected OTP key as 496-character hex string, but length is {!s}' .format(len(param['otpkey']))) try: upd_param['otpkey'] = binascii.unhexlify(upd_param['otpkey']) except (binascii.Error, TypeError): raise ParameterError( 'Expected OTP key as 496-character hex string, but it is malformed' ) TokenClass.update(self, upd_param, reset_failcount)
def decode_base32check(encoded_data, always_upper=True): """ Decode arbitrary data which is given in the following format:: strip_padding(base32(sha1(payload)[:4] + payload)) Raise a ParameterError if the encoded payload is malformed. :param encoded_data: The base32 encoded data. :type encoded_data: basestring :param always_upper: If we should convert lowercase to uppercase :type always_upper: bool :return: hex-encoded payload """ # First, add the padding to have a multiple of 8 bytes if always_upper: encoded_data = encoded_data.upper() encoded_length = len(encoded_data) if encoded_length % 8 != 0: encoded_data += "=" * (8 - (encoded_length % 8)) assert len(encoded_data) % 8 == 0 # Decode as base32 try: decoded_data = base64.b32decode(encoded_data) except TypeError: raise ParameterError("Malformed base32check data: Invalid base32") # Extract checksum and payload if len(decoded_data) < 4: raise ParameterError("Malformed base32check data: Too short") checksum, payload = decoded_data[:4], decoded_data[4:] payload_hash = hashlib.sha1(payload).digest() if payload_hash[:4] != checksum: raise ParameterError("Malformed base32check data: Incorrect checksum") return binascii.hexlify(payload)
def update(self, param, reset_failcount=True): """ process the initialization parameters We need to distinguish the first authentication step and the second authentication step. 1. step: ``param`` contains: - ``type`` - ``genkey`` 2. step: ``param`` contains: - ``serial`` - ``fbtoken`` - ``pubkey`` :param param: dict of initialization parameters :type param: dict :return: nothing """ upd_param = {} for k, v in param.items(): upd_param[k] = v if "serial" in upd_param and "fbtoken" in upd_param and "pubkey" in upd_param: # We are in step 2: if self.token.rollout_state != "clientwait": raise ParameterError("Invalid state! The token you want to enroll is not in the state 'clientwait'.") enrollment_credential = getParam(upd_param, "enrollment_credential", optional=False) if enrollment_credential != self.get_tokeninfo("enrollment_credential"): raise ParameterError("Invalid enrollment credential. You are not authorized to finalize this token.") self.del_tokeninfo("enrollment_credential") self.token.rollout_state = "enrolled" self.token.active = True self.add_tokeninfo(PUBLIC_KEY_SMARTPHONE, upd_param.get("pubkey")) self.add_tokeninfo("firebase_token", upd_param.get("fbtoken")) # create a keypair for the server side. pub_key, priv_key = generate_keypair(4096) self.add_tokeninfo(PUBLIC_KEY_SERVER, pub_key) self.add_tokeninfo(PRIVATE_KEY_SERVER, priv_key, "password") elif "genkey" in upd_param: # We are in step 1: upd_param["2stepinit"] = 1 self.add_tokeninfo("enrollment_credential", geturandom(20, hex=True)) # We also store the firebase config, that was used during the enrollment. self.add_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG, param.get(PUSH_ACTION.FIREBASE_CONFIG)) else: raise ParameterError("Invalid Parameters. Either provide (genkey) or (serial, fbtoken, pubkey).") TokenClass.update(self, upd_param, reset_failcount)
def check_user_or_serial_in_request_wrapper(*args, **kwds): user = self.request.all_data.get("user", "").strip() serial = self.request.all_data.get("serial", "").strip() if not serial and not user: raise ParameterError(_("You need to specify a serial or a user.")) if "*" in serial: raise ParameterError(_("Invalid serial number.")) if "%" in user: raise ParameterError(_("Invalid user.")) f_result = func(*args, **kwds) return f_result
def set_periodic_task(name=None, interval=None, nodes=None, taskmodule=None, ordering=0, options=None, active=True, id=None, retry_if_failed=True): """ Set a periodic task configuration. If ``id`` is None, this creates a new database entry. Otherwise, an existing entry is overwritten. We actually ensure that such an entry exists and throw a ``ParameterError`` otherwise. This also checks if ``interval`` is a valid cron expression, and throws a ``ParameterError`` if it is not. :param name: Unique name of the periodic task :type name: unicode :param interval: Periodicity as a string in crontab format :type interval: unicode :param nodes: List of nodes on which this task should be run :type nodes: list of unicode :param taskmodule: Name of the task module :type taskmodule: unicode :param ordering: Ordering of the periodic task (>= 0). Lower numbers are executed first. :type ordering: int :param options: Additional options for the task module :type options: Dictionary mapping unicodes to values that can be converted to unicode or None :param active: Flag determining whether the periodic task is active :type active: bool :param retry_if_failed: true if privacyidea should retry to execute this periodic task if it fails false if privacyidea should just try onetime regardless the failing of the task :type retry_if_failed: bool :param id: ID of the existing entry, or None :type id: int or None :return: ID of the entry """ try: croniter(interval) except ValueError as e: raise ParameterError("Invalid interval: {!s}".format(e)) if ordering < 0: raise ParameterError("Invalid ordering: {!s}".format(ordering)) if id is not None: # This will throw a ParameterError if there is no such entry get_periodic_task_by_id(id) periodic_task = PeriodicTask(name, active, interval, nodes, taskmodule, ordering, options, id, retry_if_failed) return periodic_task.id
def get_offline_otps(token_obj, otppin, amount, rounds=ROUNDS): """ Retrieve the desired number of passwords (= PIN + OTP), hash them and return them in a dictionary. Increase the token counter. :param token_obj: token in question :param otppin: The OTP PIN to prepend in the passwords. The PIN is not validated! :param amount: Number of OTP values (non-negative!) :param rounds: Number of PBKDF2 rounds :return: dictionary """ if amount < 0: raise ParameterError("Invalid refill amount: {!r}".format(amount)) (res, err, otp_dict) = token_obj.get_multi_otp(count=amount, counter_index=True) otps = otp_dict.get("otp") for key in otps.keys(): # Return the hash of OTP PIN and OTP values otps[key] = pbkdf2_sha512.using( rounds=rounds, salt_size=10).hash(otppin + otps.get(key)) # We do not disable the token, so if all offline OTP values # are used, the token can be used the authenticate online again. # token_obj.enable(False) # increase the counter by the consumed values and # also store it in tokeninfo. token_obj.inc_otp_counter(increment=amount) return otps
def get_refill(token_obj, password, options=None): """ Returns new authentication OTPs to refill the client To do so we also verify the password, which may consist of PIN + OTP. :param token_obj: Token object :param password: PIN + OTP :param options: dict that might contain "count" and "rounds" :return: a dictionary of auth items """ options = options or {} count = int(options.get("count", 100)) rounds = int(options.get("rounds", ROUNDS)) _r, otppin, otpval = token_obj.split_pin_pass(password) if not _r: raise ParameterError("Could not split password") current_token_counter = token_obj.token.count first_offline_counter = current_token_counter - count if first_offline_counter < 0: first_offline_counter = 0 # find the value in the offline OTP values! This resets the token.count! matching_count = token_obj.check_otp(otpval, first_offline_counter, count) token_obj.set_otp_count(current_token_counter) # Raise an exception *after* we reset the token counter if matching_count < 0: raise ValidateError("You provided a wrong OTP value.") # We have to add 1 here: Assume *first_offline_counter* is the counter value of the first offline OTP # we sent to the client. Assume the client then requests a refill with that exact OTP value. # Then, we need to respond with a refill of one OTP value, as the client has consumed one OTP value. counter_diff = matching_count - first_offline_counter + 1 otps = MachineApplication.get_offline_otps(token_obj, otppin, counter_diff, rounds) token_obj.add_tokeninfo(key="offline_counter", value=count) return otps
def get_init_detail(self, params=None, user=None): """ This returns the init details during enrollment. In the 1st step the QR Code is returned. """ response_detail = TokenClass.get_init_detail(self, params, user) if "otpkey" in response_detail: del response_detail["otpkey"] params = params or {} user = user or User() tokenlabel = params.get("tokenlabel", "<s>") tokenissuer = params.get("tokenissuer", "privacyIDEA") sslverify = getParam(params, PUSH_ACTION.SSL_VERIFY, allowed_values=["0", "1"], default="1") # Add rollout state the response response_detail['rollout_state'] = self.token.rollout_state extra_data = {"enrollment_credential": self.get_tokeninfo("enrollment_credential")} imageurl = params.get("appimageurl") if imageurl: extra_data.update({"image": imageurl}) if self.token.rollout_state == "clientwait": # Get the values from the configured PUSH config fb_identifier = params.get(PUSH_ACTION.FIREBASE_CONFIG) firebase_configs = get_smsgateway(identifier=fb_identifier, gwtype=GWTYPE) if len(firebase_configs) != 1: raise ParameterError("Unknown Firebase configuration!") fb_options = firebase_configs[0].option_dict for k in [FIREBASE_CONFIG.PROJECT_NUMBER, FIREBASE_CONFIG.PROJECT_ID, FIREBASE_CONFIG.APP_ID, FIREBASE_CONFIG.API_KEY, FIREBASE_CONFIG.APP_ID_IOS, FIREBASE_CONFIG.API_KEY_IOS]: extra_data[k] = fb_options.get(k) # this allows to upgrade our crypto extra_data["v"] = 1 extra_data["serial"] = self.get_serial() extra_data["sslverify"] = sslverify # We display this during the first enrollment step! qr_url = create_push_token_url(url=fb_options.get(FIREBASE_CONFIG.REGISTRATION_URL), user=user.login, realm=user.realm, serial=self.get_serial(), tokenlabel=tokenlabel, issuer=tokenissuer, user_obj=user, extra_data=extra_data, ttl=fb_options.get(FIREBASE_CONFIG.TTL)) response_detail["pushurl"] = {"description": _("URL for privacyIDEA Push Token"), "value": qr_url, "img": create_img(qr_url, width=250) } self.add_tokeninfo(FIREBASE_CONFIG.PROJECT_ID, fb_options.get(FIREBASE_CONFIG.PROJECT_ID)) response_detail["enrollment_credential"] = self.get_tokeninfo("enrollment_credential") elif self.token.rollout_state == "enrolled": # in the second enrollment step we return the public key of the server to the smartphone. pubkey = strip_key(self.get_tokeninfo(PUBLIC_KEY_SERVER)) response_detail["public_key"] = pubkey return response_detail
def api_endpoint(cls, request, g): """ This provides a function to be plugged into the API endpoint /ttype/u2f The u2f token can return the facet list at this URL. :param request: The Flask request :param g: The Flask global object g :return: Flask Response or text """ configured_app_id = get_from_config("u2f.appId") if configured_app_id is None: raise ParameterError("u2f is not configured") app_id = configured_app_id.strip("/") # Read the facets from the policies pol_facets = Match.action_only( g, scope=SCOPE.AUTH, action=U2FACTION.FACETS).action_values(unique=False) facet_list = ["https://{0!s}".format(x) for x in pol_facets] facet_list.append(app_id) log.debug("Sending facets lists for appId {0!s}: {1!s}".format( app_id, facet_list)) res = { "trustedFacets": [{ "version": { "major": 1, "minor": 0 }, "ids": facet_list }] } return "fido.trusted-apps+json", res
def get_scheduled_periodic_tasks(node, current_timestamp=None, interval_tzinfo=None): """ Collect all periodic tasks that should be run on a specific node, ordered by their ordering. This function is usually called by the local cron runner which is aware of the current local node name. :param node: Node name :type node: unicode :param current_timestamp: The current timestamp, defaults to the current time :type current_timestamp: timezone-aware datetime :param interval_tzinfo: timezone in which the crontab expression should be interpreted :type interval_tzinfo: tzinfo, defaults to local time :return: List of periodic task dictionaries """ active_ptasks = get_periodic_tasks(node=node, active=True) if current_timestamp is None: current_timestamp = datetime.now(tzutc()) if current_timestamp.tzinfo is None: raise ParameterError(u"expected timezone-aware datetime, got {!r}".format(current_timestamp)) scheduled_ptasks = [] log.debug(u"Collecting periodic tasks to run at {!s}".format(current_timestamp.isoformat())) for ptask in active_ptasks: try: next_timestamp = calculate_next_timestamp(ptask, node, interval_tzinfo) log.debug(u"Next scheduled run of {!r}: {!s}".format(ptask["name"], next_timestamp.isoformat())) if next_timestamp <= current_timestamp: log.debug(u"Scheduling periodic task {!r}".format(ptask["name"])) scheduled_ptasks.append(ptask) except Exception as e: log.warning(u"Ignoring periodic task {!r}: {!r}".format(ptask["name"], e)) return scheduled_ptasks
def generate_symmetric_key(self, server_component, client_component, options=None): """ Generate a composite key from a server and client component using a PBKDF2-based scheme. :param server_component: The component usually generated by privacyIDEA :type server_component: hex string :param client_component: The component usually generated by the client (e.g. smartphone) :type client_component: hex string :param options: :return: the new generated key as hex string """ # As /token/init has already been called before, self.hashlib # is already set. keysize = keylen[self.hashlib] rounds = int(self.get_tokeninfo('2step_difficulty')) decoded_client_component = binascii.unhexlify(client_component) expected_client_size = int(self.get_tokeninfo('2step_clientsize')) if expected_client_size != len(decoded_client_component): raise ParameterError('Client Secret Size is expected to be {}, but is {}'.format( expected_client_size, len(decoded_client_component) )) # Based on the two components, we generate a symmetric key using PBKDF2 # We pass the hex-encoded server component as the password and the # client component as the salt. secret = pbkdf2(server_component.lower(), decoded_client_component, rounds, keysize) return binascii.hexlify(secret)
def check_user_or_serial_in_request_wrapper(*args, **kwds): user = request.all_data.get("user") serial = request.all_data.get("serial") if not serial and not user: raise ParameterError(_("You need to specify a serial or a user.")) f_result = func(*args, **kwds) return f_result
def offlinerefill(): """ This endpoint allows to fetch new offline OTP values for a token, that is already offline. According to the definition it will send the missing OTP values, so that the client will have as much otp values as defined. :param serial: The serial number of the token, that should be refilled. :param refilltoken: The authorization token, that allows refilling. :param pass: the last password (maybe password+OTP) entered by the user :return: """ serial = getParam(request.all_data, "serial", required) refilltoken = getParam(request.all_data, "refilltoken", required) password = getParam(request.all_data, "pass", required) tokenobj_list = get_tokens(serial=serial) if len(tokenobj_list) != 1: raise ParameterError("The token does not exist") else: tokenobj = tokenobj_list[0] tokenattachments = list_machine_tokens(serial=serial, application="offline") if tokenattachments: # TODO: Currently we do not distinguish, if a token had more than one offline attachment # We need the options to pass the count and the rounds for the next offline OTP values, # which could have changed in the meantime. options = tokenattachments[0].get("options") # check refill token: if tokenobj.get_tokeninfo("refilltoken") == refilltoken: # refill otps = MachineApplication.get_refill(tokenobj, password, options) refilltoken = MachineApplication.generate_new_refilltoken( tokenobj) response = send_result(True) content = response.json content["auth_items"] = { "offline": [{ "refilltoken": refilltoken, "response": otps }] } response.set_data(json.dumps(content)) return response raise ParameterError( "Token is not an offline token or refill token is incorrect")
def offlinerefill(): """ This endpoint allows to fetch new offline OTP values for a token, that is already offline. According to the definition it will send the missing OTP values, so that the client will have as much otp values as defined. :param serial: The serial number of the token, that should be refilled. :param refilltoken: The authorization token, that allows refilling. :param pass: the last password (maybe password+OTP) entered by the user :return: """ result = False otps = {} serial = getParam(request.all_data, "serial", required) refilltoken = getParam(request.all_data, "refilltoken", required) password = getParam(request.all_data, "pass", required) tokenobj_list = get_tokens(serial=serial) if len(tokenobj_list) != 1: raise ParameterError("The token does not exist") else: tokenobj = tokenobj_list[0] machine_defs = list_token_machines(serial) # check if is still an offline token: for mdef in machine_defs: if mdef.get("application") == "offline": # check refill token: if tokenobj.get_tokeninfo("refilltoken") == refilltoken: # refill otps = MachineApplication.get_refill( tokenobj, password, mdef.get("options")) refilltoken = MachineApplication.generate_new_refilltoken( tokenobj) response = send_result(True) content = json.loads(response.data) content["auth_items"] = { "offline": [{ "refilltoken": refilltoken, "response": otps }] } response.data = json.dumps(content) return response raise ParameterError( "Token is not an offline token or refill token is incorrect")
def set_periodic_task_api(): """ Create or replace an existing periodic task definition. :param id: ID of an existing periodic task definition that should be updated :param name: Name of the periodic task :param active: true if the periodic task should be active :param retry_if_failed: privacyIDEA will retry to execute the task if failed :param interval: Interval at which the periodic task should run (in cron syntax) :param nodes: Comma-separated list of nodes on which the periodic task should run :param taskmodule: Task module name of the task :param ordering: Ordering of the task, must be a number >= 0. :param options: A dictionary (possibly JSON) of periodic task options, mapping unicodes to unicodes :return: ID of the periodic task """ param = request.all_data ptask_id = getParam(param, "id", optional=True) if ptask_id is not None: ptask_id = int(ptask_id) name = getParam(param, "name", optional=False) active = is_true(getParam(param, "active", default=True)) retry_if_failed = is_true(getParam(param, "retry_if_failed", default=True)) interval = getParam(param, "interval", optional=False) node_string = getParam(param, "nodes", optional=False) if node_string.strip(): node_list = [node.strip() for node in node_string.split(",")] else: raise ParameterError(u"nodes: expected at least one node") taskmodule = getParam(param, "taskmodule", optional=False) if taskmodule not in get_available_taskmodules(): raise ParameterError("Unknown task module: {!r}".format(taskmodule)) ordering = int(getParam(param, "ordering", optional=False)) options = getParam(param, "options", optional=True) if options is None: options = {} elif not isinstance(options, dict): options = json.loads(options) if not isinstance(options, dict): raise ParameterError(u"options: expected dictionary, got {!r}".format(options)) result = set_periodic_task(name, interval, node_list, taskmodule, ordering, options, active, ptask_id, retry_if_failed) g.audit_object.log({"success": True, "info": result}) return send_result(result)
def get_taskmodule(identifier): """ Return an instance of the given task module. Raise ParameterError if it does not exist. :param identifier: identifier of the task module :return: instance of a BaseTask subclass """ if identifier not in TASK_MODULES: raise ParameterError(u"Unknown task module: {!r}".format(identifier)) else: return TASK_MODULES[identifier]()
def get_periodic_task_by_name(name): """ Get a periodic task by name. Raise ParameterError if the task could not be found. :param name: task name, unicode :return: dictionary """ periodic_tasks = get_periodic_tasks(name) if len(periodic_tasks) != 1: raise ParameterError("The periodic task with unique name {!r} does not exist".format(name)) return periodic_tasks[0]
def vasco_deserialize(tokendata): ''' Convert the given bytestring to a ``TDigipassBlob`` object and return it :param tokendata: A string of 248 bytes :return: The Vasco data blob ''' if len(tokendata) != 248: raise ParameterError("Data blob has incorrect size") return TDigipassBlob.from_buffer_copy(tokendata)
def _get_periodic_task_entry(ptask_id): """ Get a periodic task entry by ID. Raise ParameterError if the task could not be found. This is only for internal use. :param id: task ID as integer :return: PeriodicTask object """ periodic_task = PeriodicTask.query.filter_by(id=ptask_id).first() if periodic_task is None: raise ParameterError("The periodic task with id {!r} does not exist".format(ptask_id)) return periodic_task
def check_serial_valid(serial): """ This function checks the given serial number for allowed values. Raises an exception if the format of the serial number is not allowed :param serial: :return: True or Exception """ if not re.match(ALLOWED_SERIAL, serial): raise ParameterError( "Invalid serial number. Must comply to {0!s}.".format( ALLOWED_SERIAL)) return True
def user_or_serial_wrapper(*args, **kwds): # If there is no user and serial keyword parameter and if # there is no normal argument, we do not have enough information serial = kwds.get("serial") user = kwds.get("user") # We have no serial! The serial would be the first arg if (serial is None and (len(args) == 0 or args[0] is None) and (user is None or (user is not None and user.is_empty()))): # We either have an empty User object or None raise ParameterError(ParameterError.USER_OR_SERIAL) f_result = func(*args, **kwds) return f_result
def update(self, param, reset_failcount=True): """ This method is called during the initialization process. :param param: parameters from the token init :type param: dict :return: None """ TokenClass.update(self, param) reg_data = getParam(param, "regdata") verify_cert = is_true(getParam(param, "u2f.verify_cert", default=True)) if not reg_data: self.token.rollout_state = ROLLOUTSTATE.CLIENTWAIT # Set the description in the first enrollment step if "description" in param: self.set_description(getParam(param, "description", default="")) elif reg_data and self.token.rollout_state == ROLLOUTSTATE.CLIENTWAIT: attestation_cert, user_pub_key, key_handle, \ signature, automatic_description = parse_registration_data(reg_data, verify_cert=verify_cert) client_data = getParam(param, "clientdata", required) client_data_str = url_decode(client_data) app_id = self.get_tokeninfo("appId", "") # Verify the registration data # In case of any crypto error, check_data raises an exception check_registration_data(attestation_cert, app_id, client_data_str, user_pub_key, key_handle, signature) self.set_otpkey(key_handle) self.add_tokeninfo("pubKey", user_pub_key) # add attestation certificate info issuer = x509name_to_string(attestation_cert.get_issuer()) serial = "{!s}".format(attestation_cert.get_serial_number()) subject = x509name_to_string(attestation_cert.get_subject()) self.add_tokeninfo("attestation_issuer", issuer) self.add_tokeninfo("attestation_serial", serial) self.add_tokeninfo("attestation_subject", subject) # Reset rollout state self.token.rollout_state = "" # If no description has already been set, set the automatic description or the # description given in the 2nd request if not self.token.description: self.set_description( getParam(param, "description", default=automatic_description)) else: raise ParameterError( "regdata provided but token not in clientwait rollout_state.")
def get_authentication_item(token_type, serial, challenge=None, options=None, filter_param=None): """ :param token_type: the type of the token. At the moment we only support "HOTP" token. Supporting time based tokens is difficult, since we would have to return a looooong list of OTP values. Supporting "yubikey" token (AES) would be possible, too. :param serial: the serial number of the token. :param challenge: This can contain the password (otp pin + otp value) so that we can put the OTP PIN into the hashed response. :type challenge: basestring :return auth_item: A list of hashed OTP values """ ret = {} options = options or {} password = challenge if token_type.lower() == "hotp": tokens = get_tokens(serial=serial) if len(tokens) == 1: token_obj = tokens[0] if password: _r, otppin, _ = token_obj.split_pin_pass(password) if not _r: raise ParameterError("Could not split password") else: otppin = "" otps = MachineApplication.get_offline_otps( token_obj, otppin, int(options.get("count", 100)), int(options.get("rounds", ROUNDS))) refilltoken = MachineApplication.generate_new_refilltoken( token_obj) ret["response"] = otps ret["refilltoken"] = refilltoken user_object = token_obj.user if user_object: uInfo = user_object.info if "username" in uInfo: ret["username"] = uInfo.get("username") else: log.info("Token %r, type %r is not supported by " "OFFLINE application module" % (serial, token_type)) return ret
def enable_event(event_id, enable=True): """ Enable or disable the and event :param event_id: ID of the event :return: """ ev = EventHandler.query.filter_by(id=event_id).first() if not ev: raise ParameterError("The event with id '{0!s}' does not " "exist".format(event_id)) # Update the event ev.active = enable r = ev.save() return r
def do(self, action, options=None): """ This method executes the defined action in the given event. :param action: :param environment: :param options: :return: """ ret = True g = options.get("g") request = options.get("request") logged_in_user = g.logged_in_user user = get_user_from_param(request.all_data) if action.lower() == "sendmail" and logged_in_user.get("role") == \ ROLE.ADMIN and not user.is_empty() and user.login: emailconfig = options.get("emailconfig") if not emailconfig: log.error("Missing parameter 'emailconfig'") raise ParameterError("Missing parameter 'emailconfig'") useremail = user.info.get("email") subject = options.get("subject") or "An action was performed on " \ "your token." body = options.get("body") or DEFAULT_BODY body = body.format( admin=logged_in_user.get("username"), realm=logged_in_user.get("realm"), action=request.path, serial=g.audit_object.audit_data.get("serial"), url=request.url_root, user=user.info.get("givenname") ) try: ret = send_email_identifier(emailconfig, recipient=useremail, subject=subject, body=body) except Exception as exx: log.error("Failed to send email: {0!s}".format(exx)) ret = False if ret: log.info("Sent a notification email to user {0}".format(user)) else: log.warning("Failed to send a notification email to user " "{0}".format(user)) return ret
def update(self, param): """ This method is called during the initialization process. :param param: parameters from the token init :type param: dict :return: None """ TokenClass.update(self, param) realms = getParam(param, "4eyes", required) separator = getParam(param, "separator", optional, default=" ") if len(separator) > 1: raise ParameterError("The separator must only be one single " "character") realms = self.realms_dict_to_string(realms) self.convert_realms(realms) self.add_tokeninfo("separator", separator) self.add_tokeninfo("4eyes", realms)
def update(self, param): """ This method is called during the initialization process. :param param: parameters from the token init :type param: dict :return: None """ # We should only initialize such a token, when the user is # immediately given in the init process, since the token on the # smartphone needs to contain a userId. if not self.user: # The user and realms should have already been set in init_token() raise ParameterError("Missing parameter: {0!r}".format("user"), id=905) ocrasuite = get_from_config("tiqr.ocrasuite") or OCRA_DEFAULT_SUITE OCRASuite(ocrasuite) self.add_tokeninfo("ocrasuite", ocrasuite) TokenClass.update(self, param)
def _api_endpoint_post(cls, request_data): """ Handle all POST requests to the api endpoint :param request_data: Dictionary containing the parameters of the request :type request_data: dict :returns: The result of handling the request and a dictionary containing the details of the request handling :rtype: (bool, dict) """ details = {} result = False serial = getParam(request_data, "serial", optional=False) if all(k in request_data for k in ("fbtoken", "pubkey")): log.debug("Do the 2nd step of the enrollment.") try: token_obj = get_one_token( serial=serial, tokentype="push", rollout_state=ROLLOUTSTATE.CLIENTWAIT) token_obj.update(request_data) except ResourceNotFoundError: raise ResourceNotFoundError( "No token with this serial number " "in the rollout state 'clientwait'.") init_detail_dict = request_data details = token_obj.get_init_detail(init_detail_dict) result = True elif all(k in request_data for k in ("nonce", "signature")): log.debug( "Handling the authentication response from the smartphone.") challenge = getParam(request_data, "nonce") signature = getParam(request_data, "signature") # get the token_obj for the given serial: token_obj = get_one_token(serial=serial, tokentype="push") pubkey_obj = _build_verify_object( token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE)) # Do the 2nd step of the authentication # Find valid challenges challengeobject_list = get_challenges(serial=serial, challenge=challenge) if challengeobject_list: # There are valid challenges, so we check this signature for chal in challengeobject_list: # verify the signature of the nonce sign_data = u"{0!s}|{1!s}".format(challenge, serial) try: pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) # The signature was valid log.debug( "Found matching challenge {0!s}.".format(chal)) chal.set_otp_status(True) chal.save() result = True except InvalidSignature as _e: pass elif all(k in request_data for k in ('new_fb_token', 'timestamp', 'signature')): timestamp = getParam(request_data, 'timestamp', optional=False) signature = getParam(request_data, 'signature', optional=False) # first check if the timestamp is in the required span cls._check_timestamp_in_range(timestamp, UPDATE_FB_TOKEN_WINDOW) try: tok = get_one_token(serial=serial, tokentype=cls.get_class_type()) pubkey_obj = _build_verify_object( tok.get_tokeninfo(PUBLIC_KEY_SMARTPHONE)) sign_data = u"{new_fb_token}|{serial}|{timestamp}".format( **request_data) pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) # If the timestamp and signature are valid we update the token tok.add_tokeninfo('firebase_token', request_data['new_fb_token']) result = True except (ResourceNotFoundError, ParameterError, TypeError, InvalidSignature, ConfigAdminError, BinasciiError) as e: # to avoid disclosing information we always fail with an invalid # signature error even if the token with the serial could not be found log.debug('{0!s}'.format(traceback.format_exc())) log.info('The following error occurred during the signature ' 'check: "{0!r}"'.format(e)) raise privacyIDEAError('Could not verify signature!') else: raise ParameterError("Missing parameters!") return result, details
def api_endpoint(cls, request, g): """ This provides a function which is called by the API endpoint ``/ttype/push`` which is defined in :doc:`../../api/ttype` The method returns a tuple ``("json", {})`` This endpoint provides several functionalities: - It is used for the 2nd enrollment step of the smartphone. It accepts the following parameters: .. sourcecode:: http POST /ttype/push HTTP/1.1 Host: https://yourprivacyideaserver serial=<token serial> fbtoken=<firebase token> pubkey=<public key> - It is also used when the smartphone sends the signed response to the challenge during authentication. The following parameters ar accepted: .. sourcecode:: http POST /ttype/push HTTP/1.1 Host: https://yourprivacyideaserver serial=<token serial> nonce=<the actual challenge> signature=<the signed nonce> - And it also acts as an endpoint for polling challenges: .. sourcecode:: http GET /ttype/push HTTP/1.1 Host: https://yourprivacyideaserver serial=<tokenserial> timestamp=<timestamp> signature=SIGNATURE(<tokenserial>|<timestamp>) More on polling can be found here: https://github.com/privacyidea/privacyidea/wiki/concept%3A-pushtoken-poll :param request: The Flask request :param g: The Flask global object g :return: The json string representing the result dictionary :rtype: tuple("json", str) """ details = {} result = False if request.method == 'POST': serial = getParam(request.all_data, "serial", optional=False) if serial and "fbtoken" in request.all_data and "pubkey" in request.all_data: log.debug("Do the 2nd step of the enrollment.") try: token_obj = get_one_token(serial=serial, tokentype="push", rollout_state="clientwait") token_obj.update(request.all_data) except ResourceNotFoundError: raise ResourceNotFoundError( "No token with this serial number " "in the rollout state 'clientwait'.") init_detail_dict = request.all_data details = token_obj.get_init_detail(init_detail_dict) result = True elif serial and "nonce" in request.all_data and "signature" in request.all_data: log.debug( "Handling the authentication response from the smartphone." ) challenge = getParam(request.all_data, "nonce") serial = getParam(request.all_data, "serial") signature = getParam(request.all_data, "signature") # get the token_obj for the given serial: token_obj = get_one_token(serial=serial, tokentype="push") pubkey_obj = _build_verify_object( token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE)) # Do the 2nd step of the authentication # Find valid challenges challengeobject_list = get_challenges(serial=serial, challenge=challenge) if challengeobject_list: # There are valid challenges, so we check this signature for chal in challengeobject_list: # verify the signature of the nonce sign_data = u"{0!s}|{1!s}".format(challenge, serial) try: pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) # The signature was valid log.debug( "Found matching challenge {0!s}.".format(chal)) chal.set_otp_status(True) chal.save() result = True except InvalidSignature as _e: pass else: raise ParameterError("Missing parameters!") elif request.method == 'GET': # This is only used for polling # By default we allow polling if the policy is not set. allow_polling = get_action_values_from_options( SCOPE.AUTH, PUSH_ACTION.ALLOW_POLLING, options={'g': g}) or PushAllowPolling.ALLOW if allow_polling == PushAllowPolling.DENY: raise PolicyError('Polling not allowed!') serial = getParam(request.all_data, "serial", optional=False) timestamp = getParam(request.all_data, 'timestamp', optional=False) signature = getParam(request.all_data, 'signature', optional=False) # first check if the timestamp is in the required span try: ts = isoparse(timestamp) except (ValueError, TypeError) as _e: log.debug('{0!s}'.format(traceback.format_exc())) raise privacyIDEAError( 'Could not parse timestamp {0!s}. ' 'ISO-Format required.'.format(timestamp)) # TODO: make time delta configurable td = timedelta(minutes=POLL_TIME_WINDOW) # We don't know if the passed timestamp is timezone aware. If no # timezone is passed, we assume UTC if ts.tzinfo: now = datetime.now(utc) else: now = datetime.utcnow() if not (now - td <= ts <= now + td): raise privacyIDEAError( 'Timestamp {0!s} not in valid range.'.format(timestamp)) # now check the signature # first get the token try: tok = get_one_token(serial=serial, tokentype=cls.get_class_type()) # If the push_allow_polling policy is set to "token" we also # need to check the POLLING_ALLOWED tokeninfo. If it evaluated # to 'False', polling is not allowed for this token. If the # tokeninfo value evaluates to 'True' or is not set at all, # polling is allowed for this token. if allow_polling == PushAllowPolling.TOKEN: if not is_true( tok.get_tokeninfo(POLLING_ALLOWED, default='True')): log.debug( 'Polling not allowed for pushtoken {0!s} due to ' 'tokeninfo.'.format(serial)) raise PolicyError('Polling not allowed!') pubkey_obj = _build_verify_object( tok.get_tokeninfo(PUBLIC_KEY_SMARTPHONE)) sign_data = u"{serial}|{timestamp}".format(**request.all_data) pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) # The signature was valid now check for an open challenge # we need the private server key to sign the smartphone data pem_privkey = tok.get_tokeninfo(PRIVATE_KEY_SERVER) # we also need the FirebaseGateway for this token fb_identifier = tok.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG) if not fb_identifier: raise ResourceNotFoundError( 'The pushtoken {0!s} has no Firebase configuration ' 'assigned.'.format(serial)) fb_gateway = create_sms_instance(fb_identifier) options = {'g': g} challenges = [] challengeobject_list = get_challenges(serial=serial) for chal in challengeobject_list: # check if the challenge is active and not already answered _cnt, answered = chal.get_otp_status() if not answered and chal.is_valid(): # then return the necessary smartphone data to answer # the challenge sp_data = _build_smartphone_data( serial, chal.challenge, fb_gateway, pem_privkey, options) challenges.append(sp_data) # return the challenges as a list in the result value result = challenges except (ResourceNotFoundError, ParameterError, InvalidSignature, ConfigAdminError, BinasciiError) as e: # to avoid disclosing information we always fail with an invalid # signature error even if the token with the serial could not be found log.debug('{0!s}'.format(traceback.format_exc())) log.info('The following error occurred during the signature ' 'check: "{0!r}"'.format(e)) raise privacyIDEAError('Could not verify signature!') else: raise privacyIDEAError( 'Method {0!s} not allowed in \'api_endpoint\' ' 'for push token.'.format(request.method)) return "json", prepare_result(result, details=details)
def api_endpoint(cls, request, g): """ This provides a function which is called by the API endpoint /ttype/push which is defined in api/ttype.py The method returns return "json", {} This endpoint is used for the 2nd enrollment step of the smartphone. Parameters sent: * serial * fbtoken * pubkey This endpoint is also used, if the smartphone sends the signed response to the challenge during authentication Parameters sent: * serial * nonce (which is the challenge) * signature (which is the signed nonce) :param request: The Flask request :param g: The Flask global object g :return: dictionary """ details = {} result = False serial = getParam(request.all_data, "serial", optional=False) if serial and "fbtoken" in request.all_data and "pubkey" in request.all_data: # Do the 2nd step of the enrollment try: token_obj = get_one_token(serial=serial, tokentype="push", rollout_state="clientwait") token_obj.update(request.all_data) except ResourceNotFoundError: raise ResourceNotFoundError( "No token with this serial number in the rollout state 'clientwait'." ) init_detail_dict = request.all_data details = token_obj.get_init_detail(init_detail_dict) result = True elif serial and "nonce" in request.all_data and "signature" in request.all_data: challenge = getParam(request.all_data, "nonce") serial = getParam(request.all_data, "serial") signature = getParam(request.all_data, "signature") # get the token_obj for the given serial: token_obj = get_one_token(serial=serial, tokentype="push") pubkey_pem = token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE) # The public key of the smartphone was probably sent as urlsafe: pubkey_pem = pubkey_pem.replace("-", "+").replace("_", "/") # The public key was sent without any header pubkey_pem = "-----BEGIN PUBLIC KEY-----\n{0!s}\n-----END PUBLIC KEY-----".format( pubkey_pem.strip().replace(" ", "+")) # Do the 2nd step of the authentication # Find valid challenges challengeobject_list = get_challenges(serial=serial, challenge=challenge) if challengeobject_list: # There are valid challenges, so we check this signature for chal in challengeobject_list: # verify the signature of the nonce pubkey_obj = serialization.load_pem_public_key( to_bytes(pubkey_pem), default_backend()) sign_data = u"{0!s}|{1!s}".format(challenge, serial) try: pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()) # The signature was valid chal.set_otp_status(True) result = True except InvalidSignature as e: pass else: raise ParameterError("Missing parameters!") return "json", prepare_result(result, details=details)