Exemple #1
0
    def encrypt(self, data, iv=None, id=0):
        '''
        security module methods: encrypt

        :param data: the to be encrypted data
        :type  data:byte string

        :param iv: initialisation vector (salt)
        :type  iv: random bytes

        :param  id: slot of the key array
        :type   id: int - slotid

        :return: encrypted data
        :rtype:  byte string
        '''

        if self.is_ready is False:
            raise Exception('setup of security module incomplete')

        key = self.getSecret(id)
        # convert input to ascii, so we can securely append bin data
        input = binascii.b2a_hex(data)
        input += '\x01\x02'
        padding = (16 - len(input) % 16) % 16
        input += padding * "\0"
        aes = AES.new(key, AES.MODE_CBC, iv)

        res = aes.encrypt(input)

        if self.crypted is False:
            zerome(key)
            del key
        return res
Exemple #2
0
    def _setupKey_(self):
        if not hasattr(self, 'bkey'):
            self.bkey = None

        if self.bkey is None:
            akey = decrypt(self.val, self.iv, hsm=self.hsm)
            self.bkey = binascii.unhexlify(akey)
            zerome(akey)
            del akey
Exemple #3
0
    def _clearKey_(self, preserve=False):
        if preserve is False:

            if not hasattr(self, 'bkey'):
                self.bkey = None

            if self.bkey is not None:
                zerome(self.bkey)
                del self.bkey
Exemple #4
0
    def checkOtp(self, anOtpVal, window=10, options=None):
        '''
        check a provided otp value

        :param anOtpVal: the to be tested otp value
        :param window: the +/- window around the test time
        :param options: generic container for additional values \
                        here only used for seltest: setting the initTime

        :return: -1 for fail else the identified counter/time
        '''

        res = -1
        window = window * 2

        initTime = 0
        if options is not None and type(options) == dict:
            initTime = int(options.get('initTime', 0))

        if (initTime == 0):
            otime = int(time.time() / 10)
        else:
            otime = int(initTime)


        if self.secretObject is None:
            key = self.key
            pin = self.pin
        else:
            key = self.secretObject.getKey()
            pin = self.secPin.getKey()


        for i in range(otime - window, otime + window):
            otp = unicode(self.calcOtp(i, key, pin))
            if unicode(anOtpVal) == otp:
                res = i
                break

        if self.secretObject is not None:
            zerome(key)
            zerome(pin)
            del key
            del pin

        ## prevent access twice with last motp
        if res <= self.oldtime:
            log.warning("[checkOtp] otpvalue %s checked once before (%r<=%r)" %
                        (anOtpVal, res, self.oldtime))
            res = -1
        if res == -1:
            msg = 'checking motp failed'
        else:
            msg = 'checking motp sucess'

        return res
Exemple #5
0
    def checkOtp(self, anOtpVal):
        res = -1

        key = self.secretObject.getKey()

        if key == anOtpVal:
            res = 0

        zerome(key)
        del key

        return res
Exemple #6
0
    def verfiyMessageSignature(self,
                               message,
                               hex_mac,
                               method=sha256,
                               slot_id=DEFAULT_KEY):
        """
        verify the hex mac is same for the message -
           the comparison is done in a constant time comparison

        :param message: the original message
        :param hex_mac: the to compared mac in hex
        :param method: the hash method - we use by default sha256
        :param slot_id: which key should be used

        :return: boolean
        """
        sign_key = None
        result = True

        try:
            sign_key = self.getSecret(slot_id)
            hmac_obj = hmac.new(sign_key, message.encode('utf-8'), method)
            sign_mac = hmac.new(sign_key, message.encode('utf-8'),
                                method).hexdigest()

            res = 0
            # as we compare on hex, we have to multiply by 2
            digest_size = hmac_obj.digest_size * 2

            for x, y in zip(hex_mac, sign_mac):
                res |= ord(x) ^ ord(y)

            if len(sign_mac) != digest_size:
                result = False

            if res:
                result = False

        except ValueError as err:
            log.exception("Signature check: Mac Comparison failed! %r", err)

        except Exception as exx:
            log.exception("Signature check: Unknown exception happened %r",
                          exx)

        finally:
            if sign_key:
                zerome(sign_key)
                del sign_key

        return result
Exemple #7
0
    def checkOtp(self, anOtpVal, window=10, options=None):
        """
        check a provided otp value

        :param anOtpVal: the to be tested otp value
        :param window: the +/- window around the test time
        :param options: generic container for additional values
        :return: -1 for fail else the identified counter/time
        """

        res = -1
        window = window * 2
        otime = int(time.time() // 10)

        if self.secretObject is None:
            key = self.key
            pin = self.pin
        else:
            key = self.secretObject.getKey()
            pin = self.secPin.getKey()

        for i in range(otime - window, otime + window):
            otp = str(self.calcOtp(i, key, pin))
            if anOtpVal == otp:
                res = i
                break

        if self.secretObject is not None:
            zerome(key)
            zerome(pin)
            del key
            del pin

        # prevent access twice with last motp
        if res <= self.oldtime:
            log.warning(
                "[checkOtp] otpvalue %s checked once before (%r<=%r)",
                anOtpVal,
                res,
                self.oldtime,
            )
            res = -1
        if res == -1:
            msg = "checking motp failed"
        else:
            msg = "checking motp sucess"

        return res
Exemple #8
0
    def calc_dh(self, partition, data):
        """
        encapsulate the Diffi Helmann calculation

        as the server secret key is a sensitive data, we try to encapsulate
        it and care for the cleanup

        :param partition: the id of the server secret key
        :param :
        """
        server_secret_key = get_dh_secret_key(partition)
        hmac_secret = calc_dh(server_secret_key, data)

        zerome(server_secret_key)

        return hmac_secret
Exemple #9
0
    def decrypt(self, input, iv=None, id=0):
        '''
        security module methods: decrypt

        :param data: the to be decrypted data
        :type  data:byte string

        :param iv: initialisation vector (salt)
        :type  iv: random bytes

        :param  id: slot of the key array
        :type   id: int

        :return: decrypted data
        :rtype:  byte string
        '''

        if self.is_ready is False:
            raise Exception('setup of security module incomplete')

        key = self.getSecret(id)
        aes = AES.new(key, AES.MODE_CBC, iv)
        output = aes.decrypt(input)

        eof = len(output) - 1
        if eof == -1:
            raise Exception('invalid encoded secret!')

        while output[eof] == '\0':
            eof -= 1

        if output[eof - 1:eof + 1] != '\x01\x02':
            raise Exception('invalid encoded secret!')

        # convert output from ascii, back to bin data
        data = binascii.a2b_hex(output[:eof - 1])

        if self.crypted is False:
            zerome(key)
            del key

        return data
Exemple #10
0
    def decrypt(self, value: bytes, iv: bytes, id: int = 0) -> bytes:
        '''
        security module methods: decrypt

        :param data: the to be decrypted data
        :type  data:byte string

        :param iv: initialisation vector (salt)
        :type  iv: random bytes

        :param  id: slot of the key array
        :type   id: int

        :return: decrypted data
        :rtype:  byte string
        '''

        if self.is_ready is False:
            raise Exception('setup of security module incomplete')

        key = self.getSecret(id)
        aes = AES.new(key, AES.MODE_CBC, iv)
        output = aes.decrypt(value)

        eof = len(output) - 1
        if eof == -1:
            raise Exception('invalid encoded secret!')

        while output[eof] == 0x00:
            eof -= 1

        if not (output[eof - 1] == 0x01 and output[eof] == 0x02):
            raise Exception('invalid encoded secret!')

        data = output[:eof - 1]

        if self.crypted is False:
            zerome(key)
            del key

        return binascii.a2b_hex(data)
Exemple #11
0
    def encrypt(self, data: bytes, iv: bytes, id: int = 0) -> bytes:
        '''
        security module methods: encrypt

        This module performs the following operations on
        the input data, which is a string:
            * convert data to hexidcimal representation
            * add termination string
            * pad with null to a multiple of 16 bytes
            * aes encrypt

        :param data: the to be encrypted data
        :type  data:byte string

        :param iv: initialisation vector (salt)
        :type  iv: random bytes

        :param  id: slot of the key array
        :type   id: int - slotid

        :return: encrypted data
        :rtype:  byte string
        '''

        if self.is_ready is False:
            raise Exception('setup of security module incomplete')

        key = self.getSecret(id)
        input_data = binascii.b2a_hex(data)
        input_data += b'\x01\x02'
        padding = (16 - len(input_data) % 16) % 16
        input_data += padding * b'\0'
        aes = AES.new(key, AES.MODE_CBC, iv)

        res = aes.encrypt(input_data)

        if self.crypted is False:
            zerome(key)
            del key
        return res
Exemple #12
0
    def signMessage(self, message, method=sha256, slot_id=DEFAULT_KEY):
        """
        create the hex mac for the message -

        :param message: the original message
        :param method: the hash method - we use by default sha256
        :param slot_id: which key should be used

        :return: hex mac
        """

        sign_key = None

        try:
            sign_key = self.getSecret(slot_id)
            hex_mac = hmac.new(sign_key, message, method).hexdigest()
        finally:
            if sign_key:
                zerome(sign_key)
                del sign_key

        return hex_mac
Exemple #13
0
def main():

    class DummySecLock():

        def release(self):
            return

        def acquire_write(self):
            return

    # hook for local provider test
    sep = SecurityProvider(secLock=DummySecLock())
    sep.load_config({})
    sep.createHSMPool('default')
    sep.setupModule('default', {'passwd': 'test123'})

    # runtime catch an hsm for session
    hsm = sep.getSecurityModule()

    passwo = 'password'
    encpass = hsm.encryptPassword(passwo)
    passw = hsm.decryptPassword(encpass)

    zerome(passw)

    hsm2 = sep.getSecurityModule(sessionId='session2')

    passwo = 'password'
    encpass = hsm2.encryptPassword(passwo)
    passw = hsm2.decryptPassword(encpass)

    zerome(passw)

    # session shutdown
    sep.dropSecurityModule(sessionId='session2')
    sep.dropSecurityModule()

    return True
Exemple #14
0
    def create_challenge_url(self,
                             transaction_id,
                             content_type,
                             callback_url='',
                             message=None,
                             login=None,
                             host=None):
        """
        creates a challenge url (looking like lseqr://push/<base64string>),
        returns the url and the unencrypted challenge data

        :param transaction_id: The transaction id generated by LinOTP

        :param content_type: One of the types CONTENT_TYPE_SIGNREQ,
            CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN

        :param callback_url: callback url (optional), default is
            empty string

        :param message: the transaction message, that should be signed
            by the client. Only for content type CONTENT_TYPE_SIGNREQ

        :param login: the login name of the user. Only for content type
            CONTENT_TYPE_LOGIN

        :param host: hostname of the user. Only for content type
            CONTENT_TYPE_LOGIN

        :returns: tuple (challenge_url, sig_base), with challenge_url being
            the push url and sig_base the message, that is used for
            the client signature
        """

        serial = self.getSerial()

        # ------------------------------------------------------------------- --

        # sanity/format checks

        if content_type not in [
                CONTENT_TYPE_SIGNREQ, CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN
        ]:
            raise InvalidFunctionParameter(
                'content_type', 'content_type must '
                'be CONTENT_TYPE_SIGNREQ, '
                'CONTENT_TYPE_PAIRING or '
                'CONTENT_TYPE_LOGIN.')

        # ------------------------------------------------------------------- --

        #  after the lseqr://push/ prefix the following data is encoded
        #  in urlsafe base64:

        #            ---------------------------------------------------
        #  fields   | version | user token id |  R  | ciphertext | sign |
        #            ---------------------------------------------------
        #           |          header         |          body           |
        #            ---------------------------------------------------
        #  size     |    1    |       4       |  32 |      ?     |  64  |
        #            ---------------------------------------------------
        #

        # create header

        user_token_id = self.getFromTokenInfo('user_token_id')
        data_header = struct.pack('<bI', CHALLENGE_URL_VERSION, user_token_id)

        # ------------------------------------------------------------------- --

        # create body

        r = secrets.token_bytes(32)
        R = calc_dh_base(r)

        b64_user_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
        user_dsa_public_key = b64decode(b64_user_dsa_public_key)
        user_dh_public_key = dsa_to_dh_public(user_dsa_public_key)

        ss = calc_dh(r, user_dh_public_key)
        U = SHA256.new(ss).digest()
        zerome(ss)

        sk = U[0:16]
        nonce = U[16:32]
        zerome(U)

        # ------------------------------------------------------------------- --

        # create plaintext section

        # ------------------------------------------------------------------- --

        # generate plaintext header

        #            ------------------------------------------------
        #  fields   | content_type  | transaction_id | timestamp | ..
        #            ------------------------------------------------
        #  size     |       1       |        8       |     8     |  ?
        #            -------------------------------------------------

        transaction_id = transaction_id_to_u64(transaction_id)
        plaintext = struct.pack('<bQQ', content_type, transaction_id,
                                int(time.time()))

        # ------------------------------------------------------------------- --

        utf8_callback_url = callback_url.encode('utf8')

        # enforce max url length as specified in protocol

        if len(utf8_callback_url) > 511:
            raise InvalidFunctionParameter(
                'callback_url', 'max string '
                'length (encoded as utf8) is '
                '511')

        # ------------------------------------------------------------------- --

        # create data package depending on content type

        # ------------------------------------------------------------------- --

        if content_type == CONTENT_TYPE_PAIRING:

            #            -----------------------------------------
            #  fields   | header | serial | NUL | callback | NUL |
            #            -----------------------------------------
            #  size     |   9    |    ?   |  1  |     ?    |  1  |
            #            -----------------------------------------

            utf8_serial = serial.encode('utf8')

            if len(utf8_serial) > 63:
                raise ValueError('serial (encoded as utf8) can only be 63 '
                                 'characters long')

            plaintext += utf8_serial + b'\00' + utf8_callback_url + b'\00'

        # ------------------------------------------------------------------- --

        if content_type == CONTENT_TYPE_SIGNREQ:

            if message is None:
                raise InvalidFunctionParameter(
                    'message', 'message must be '
                    'supplied for content type '
                    'SIGNREQ')

            #            ------------------------------------------
            #  fields   | header | message | NUL | callback | NUL |
            #            ------------------------------------------
            #  size     |   9    |    ?    |  1  |     ?    |  1  |
            #            ------------------------------------------

            utf8_message = message.encode('utf8')

            # enforce max sizes specified by protocol

            if len(utf8_message) > 511:
                raise InvalidFunctionParameter(
                    'message', 'max string '
                    'length (encoded as utf8) is '
                    '511')

            plaintext += utf8_message + b'\00' + utf8_callback_url + b'\00'

        # ------------------------------------------------------------------- --

        if content_type == CONTENT_TYPE_LOGIN:

            if login is None:
                raise InvalidFunctionParameter(
                    'login', 'login must be '
                    'supplied for content type '
                    'LOGIN')
            if host is None:
                raise InvalidFunctionParameter(
                    'host', 'host must be '
                    'supplied for content type '
                    'LOGIN')

            #            -----------------------------------------------------
            #  fields   | header | login | NUL | host | NUL | callback | NUL |
            #            -----------------------------------------------------
            #  size     |   9    |   ?   |  1  |   ?  |  1  |     ?    |  1  |
            #            -----------------------------------------------------

            utf8_login = login.encode('utf8')
            utf8_host = host.encode('utf8')

            # enforce max sizes specified by protocol

            if len(utf8_login) > 127:
                raise InvalidFunctionParameter(
                    'login', 'max string '
                    'length (encoded as utf8) is '
                    '127')
            if len(utf8_host) > 255:
                raise InvalidFunctionParameter(
                    'host', 'max string '
                    'length (encoded as utf8) is '
                    '255')

            plaintext += utf8_login + b'\00'
            plaintext += utf8_host + b'\00'
            plaintext += utf8_callback_url + b'\00'

        # ------------------------------------------------------------------- --

        # encrypt inner layer

        nonce_as_int = int_from_bytes(nonce, byteorder='big')
        ctr = Counter.new(128, initial_value=nonce_as_int)
        cipher = AES.new(sk, AES.MODE_CTR, counter=ctr)
        ciphertext = cipher.encrypt(plaintext)
        unsigned_raw_data = data_header + R + ciphertext

        # ------------------------------------------------------------------- --

        # create signature

        partition = self.getFromTokenInfo('partition')
        secret_key = get_secret_key(partition)
        signature = crypto_sign_detached(unsigned_raw_data, secret_key)
        raw_data = unsigned_raw_data + signature

        protocol_id = config.get('mobile_app_protocol_id', 'lseqr')
        url = protocol_id + '://push/' + encode_base64_urlsafe(raw_data)

        return url, (signature + plaintext)
Exemple #15
0
 def cleanup(self):
     zerome(self.key)
     del self.key
Exemple #16
0
def decrypt_pairing_response(enc_pairing_response):
    """
    Parses and decrypts a pairing response into a named tuple PairingResponse
    consisting of

    * user_public_key - the user's public key
    * user_token_id   - an id for the client to uniquely identify the token.
                        this id is necessary, because the client could
                        communicate with more than one linotp, so serials
                        could overlap.
    * serial - the serial identifying the token in linotp
    * user_login - the user login name

    It is possible that either user_login or serial is None. Both
    being None is a valid response according to this function but
    will be considered an error in the calling method.

    The following parameters are needed:

    :param enc_pairing_response:
        The urlsafe-base64 encoded string received from the client

    The following exceptions can be raised:

    :raises ParameterError:
        If the pairing response has an invalid format

    :raises ValueError:
        If the pairing response has a different version
        than this implementation (currently hardcoded)

    :raises ValueError:
        If the pairing response indicates a different
        token type than QRToken (also hardcoded)

    :raises ValueError:
        If the pairing response field "partition" is not
        identical to the field "token_type"
        ("partition" is currently used for the token
        type id. It is reserved for multiple key usage
        in a future implementation.)

    :raises ValueError:
        If the MAC of the response didn't match

    :return:
        Parsed/decrypted PairingReponse
    """

    data = decode_base64_urlsafe(enc_pairing_response)

    # ---------------------------------------------------------------------- --

    #            ------------------------------------------- --
    #  fields   | version | partition | R  | ciphertext | MAC |
    #            ------------------------------------------- --
    #  size     |    1    |     4     | 32 |      ?     | 16  |
    #            ------------------------------------------- --

    if len(data) < 1 + 4 + 32 + 16:
        raise ParameterError('Malformed pairing response')

    # ---------------------------------------------------------------------- --

    # parse header

    header = data[0:5]
    version, partition = struct.unpack('<bI', header)

    if version != PAIR_RESPONSE_VERSION:
        raise ValueError('Unexpected pair-response version, '
                         'expected: %d, got: %d' %
                         (PAIR_RESPONSE_VERSION, version))

    # ---------------------------------------------------------------------- --

    R = data[5:32 + 5]
    ciphertext = data[32 + 5:-16]
    mac = data[-16:]

    # ---------------------------------------------------------------------- --

    # calculate the shared secret

    # - --

    secret_key = get_dh_secret_key(partition)
    ss = calc_dh(secret_key, R)

    # derive encryption key and nonce from the shared secret
    # zero the values from memory when they are not longer needed
    U = SHA256.new(ss).digest()
    zerome(ss)
    encryption_key = U[0:16]
    nonce = U[16:32]
    zerome(U)

    # decrypt response
    cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
    cipher.update(header)
    plaintext = cipher.decrypt_and_verify(ciphertext, mac)
    zerome(encryption_key)

    # ---------------------------------------------------------------------- --

    # check format boundaries for type peaking
    # (token type specific length boundaries are checked
    #  in the appropriate functions)

    plaintext_min_length = 1
    if len(data) < plaintext_min_length:
        raise ParameterError('Malformed pairing response')

    # ---------------------------------------------------------------------- --

    # get token type and parse decrypted response

    #            -------------------- --
    #  fields   | token type |   ...   |
    #            -------------------- --
    #  size     |     1      |    ?    |
    #            -------------------- --

    token_type = struct.unpack('<b', plaintext[0])[0]

    if token_type not in SUPPORTED_TOKEN_TYPES:
        raise ValueError('unsupported token type %d, supported types '
                         'are %s' % (token_type, SUPPORTED_TOKEN_TYPES))

    # ---------------------------------------------------------------------- --

    # delegate the data parsing of the plaintext
    # to the appropriate function and return the result

    data_parser = get_pairing_data_parser(token_type)
    pairing_data = data_parser(plaintext)
    zerome(plaintext)

    # get the appropriate high level type

    try:
        token_type_as_str = INV_TOKEN_TYPES[token_type]
    except KeyError:
        raise ProgrammingError(
            'token_type %d is in SUPPORTED_TOKEN_TYPES',
            'however an appropriate mapping entry in '
            'TOKEN_TYPES is missing' % token_type)

    return PairingResponse(token_type_as_str, pairing_data)
Exemple #17
0
    def create_challenge_url(self,
                             transaction_id,
                             content_type,
                             message,
                             callback_url,
                             callback_sms_number,
                             use_compression=False,
                             reset_url=False):
        """
        creates a challenge url (looking like lseqr://chal/<base64string>)
        from a challenge dictionary as provided by Challanges.create_challenge
        in lib.challenge

        the version identifier of the challenge url is currently hardcoded
        to 1.
        """

        serial = self.getSerial()

        if content_type is None:
            content_type = CONTENT_TYPE_FREE

        # ------------------------------------------------------------------- --

        # sanity/format checks

        if content_type not in [
                CONTENT_TYPE_PAIRING, CONTENT_TYPE_AUTH, CONTENT_TYPE_FREE
        ]:
            raise InvalidFunctionParameter(
                'content_type', 'content_type must '
                'be CONTENT_TYPE_PAIRING, '
                'CONTENT_TYPE_AUTH or '
                'CONTENT_TYPE_FREE.')

        if content_type == CONTENT_TYPE_PAIRING and \
           message != serial:
            raise InvalidFunctionParameter(
                'message', 'message must be equal '
                'to serial in pairing mode')

        if content_type == CONTENT_TYPE_AUTH:
            if '@' not in message:
                raise InvalidFunctionParameter(
                    'message', 'For content type '
                    'auth, message must have format '
                    '<login>@<server>')

        # ------------------------------------------------------------------- --

        #  after the lseqr://chal/ prefix the following data is encoded
        #  in urlsafe base64:

        #            ---------------------------------------------------
        #  fields   | version | user token id |  R  | ciphertext | MAC |
        #            ---------------------------------------------------
        #           |          header         |     |    EAX enc data  |
        #            ---------------------------------------------------
        #  size     |    1    |       4       |  32 |      ?     | 16  |
        #            ---------------------------------------------------
        #

        r = secrets.token_bytes(32)
        R = calc_dh_base(r)

        user_token_id = self.getFromTokenInfo('user_token_id')
        data_header = struct.pack('<bI', QRTOKEN_VERSION, user_token_id)

        # the user public key is saved as base64 in
        # the token info since the byte format is
        # incompatible with the json backend.

        b64_user_public_key = self.getFromTokenInfo('user_public_key')
        user_public_key = b64decode(b64_user_public_key)

        ss = calc_dh(r, user_public_key)
        U1 = sha256(ss).digest()
        U2 = sha256(U1).digest()
        zerome(ss)

        skA = U1[0:16]
        skB = U2[0:16]
        nonce = U2[16:32]
        zerome(U1)
        zerome(U2)

        # ------------------------------------------------------------------- --

        # create plaintext section

        # ------------------------------------------------------------------- --

        # create the bitmap for flags

        flags = 0

        if use_compression:
            flags |= CHALLENGE_HAS_COMPRESSION

        # FIXME: sizecheck for message, callback url, sms number
        # wiki specs are utf-8 byte length (without \0)

        if callback_url is not None:
            flags |= CHALLENGE_HAS_URL

        if callback_sms_number is not None:
            flags |= CHALLENGE_HAS_SMS_NUMBER

        if (content_type == CONTENT_TYPE_PAIRING):
            flags |= CHALLENGE_HAS_SIGNATURE

        if reset_url:
            flags |= CHALLENGE_SHOULD_RESET_URL
            flags |= CHALLENGE_HAS_SIGNATURE

        # ------------------------------------------------------------------- --

        # generate plaintext header

        #            ----------------------------------------------
        #  fields   | content_type  | flags | transaction_id | ... |
        #            ----------------------------------------------
        #  size     |       1       |   1   |        8       |  ?  |
        #            ----------------------------------------------

        transaction_id = transaction_id_to_u64(transaction_id)
        pt_header = struct.pack('<bbQ', content_type, flags, transaction_id)
        plaintext = pt_header

        # ------------------------------------------------------------------- --

        # create data package

        #            -------------------------------
        #  fields   | header  | message | NUL | ... |
        #            -------------------------------
        #  size     |   10    |    ?    |  1  |  ?  |
        #            -------------------------------

        data_package = b''
        utf8_message = message.encode('utf8')

        # enforce max sizes specified by protocol

        if content_type == CONTENT_TYPE_FREE and len(utf8_message) > 511:
            raise ParameterError('message (encoded as utf8) can only be 511 '
                                 'characters long')

        elif content_type == CONTENT_TYPE_PAIRING and len(utf8_message) > 63:
            raise InvalidFunctionParameter(
                'message', 'max string length '
                '(encoded as utf8) is 511 for '
                'content type PAIRING')

        elif content_type == CONTENT_TYPE_AUTH and len(utf8_message) > 511:
            raise InvalidFunctionParameter(
                'message', 'max string length '
                '(encoded as utf8) is 511 for '
                'content type AUTH')

        data_package += utf8_message + b'\x00'

        # ------------------------------------------------------------------- --

        # depending on function parameters add callback url
        # and/or callback sms number

        #            -----------------------------------------------------
        #  fields   | ... | callback url | NUL | callback sms | NUL | ... |
        #            -----------------------------------------------------
        #  size     |  ?  |       ?      |  1  |       ?      |  1  |  ?  |
        #            -----------------------------------------------------

        # ------------------------------------------------------------------- --

        if callback_url is not None:

            utf8_callback_url = callback_url.encode('utf8')

            # enforce max url length as specified in protocol

            if len(utf8_callback_url) > 511:
                raise InvalidFunctionParameter(
                    'callback_url', 'max string '
                    'length (encoded as utf8) is '
                    '511')

            data_package += utf8_callback_url + b'\x00'

        # ------------------------------------------------------------------- --

        if callback_sms_number is not None:

            utf8_callback_sms_number = callback_sms_number.encode('utf8')

            if len(utf8_callback_sms_number) > 31:
                raise InvalidFunctionParameter(
                    'callback_sms_number', 'max string length (encoded '
                    'as utf8) is 31')

            data_package += utf8_callback_sms_number + b'\x00'

        # ------------------------------------------------------------------- --

        if use_compression:
            maybe_compressed_data_package = zlib.compress(data_package, 9)
        else:
            maybe_compressed_data_package = data_package

        # ------------------------------------------------------------------- --

        # when content type is pairing the protocol specifies that
        # the server must send a hmac based signature with the
        # response

        sig = b''
        sec_obj = self._get_secret_object()

        if flags & CHALLENGE_HAS_SIGNATURE:

            hmac_message = nonce + pt_header + maybe_compressed_data_package

            sig = sec_obj.hmac_digest(data_input=hmac_message,
                                      bkey=self.server_hmac_secret,
                                      hash_algo=sha256)

            plaintext += sig

        # ------------------------------------------------------------------- --

        plaintext += maybe_compressed_data_package

        # ------------------------------------------------------------------- --

        user_message = nonce + pt_header + sig + data_package

        user_sig = sec_obj.hmac_digest(data_input=user_message,
                                       bkey=skB,
                                       hash_algo=sha256)

        # the user sig will be given as urlsafe base64 in the
        # challenge response. for this reasons (and because we
        # need to serialize it into json) we convert the user_sig
        # into this format.

        user_sig = encode_base64_urlsafe(user_sig)

        # ------------------------------------------------------------------- --

        cipher = AES.new(skA, AES.MODE_EAX, nonce)
        cipher.update(data_header)
        ciphertext, tag = cipher.encrypt_and_digest(plaintext)

        raw_data = data_header + R + ciphertext + tag
        protocol_id = config.get('mobile_app_protocol_id', 'lseqr')
        url = protocol_id + '://chal/' + encode_base64_urlsafe(raw_data)

        return url, user_sig