コード例 #1
0
def main():
    ## hook for local provider test
    sep = SecurityProvider()
    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
コード例 #2
0
ファイル: default.py プロジェクト: gsnbng/LinOTP
    def signMessage(self, message, method=None, 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

        if method is None:
            method = SHA256

        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
コード例 #3
0
ファイル: provider.py プロジェクト: hopil/LinOTP
def main():
    ## hook for local provider test
    sep = SecurityProvider()
    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
コード例 #4
0
    def signMessage(self, message, method=None, 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

        if method is None:
            method = SHA256

        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
コード例 #5
0
ファイル: default.py プロジェクト: super-rain/LinOTP
    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
コード例 #6
0
ファイル: mOTP.py プロジェクト: RDLM-01/Elm
    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
        '''

        log.debug("[checkOtp] begin. checking the otpvalue %s window:%r, \
                            options:%r" % (anOtpVal, window, options))
        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
                log.debug("[checkOtp] otpvalue %r found at: %r" %
                          (anOtpVal, res))
                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'

        log.debug("[checkOtp] end. %s : returning result: %r, " % (msg, res))
        return res
コード例 #7
0
ファイル: secret_obj.py プロジェクト: rb12345/Elm
    def checkOtp(self, anOtpVal):
        res = -1

        key = self.secretObject.getKey()

        if key == anOtpVal:
            res = 0

        zerome(key)
        del key

        return res
コード例 #8
0
ファイル: secret_obj.py プロジェクト: MuhamadYULIANTO/LinOTP
    def checkOtp(self, anOtpVal):
        res = -1

        key = self.secretObject.getKey()

        if key == anOtpVal:
            res = 0

        zerome(key)
        del key

        return res
コード例 #9
0
    def verfiyMessageSignature(self,
                               message,
                               hex_mac,
                               method=None,
                               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

        if method is None:
            method = SHA256

        try:
            sign_key = self.getSecret(slot_id)
            hmac = HMAC.new(sign_key, message, method)
            sign_mac = HMAC.new(sign_key, message, method).hexdigest()

            res = 0
            # as we compare on hex, we have to multiply by 2
            digest_size = hmac.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.error("Mac Comparison failed! %r", err)

        except Exception as exx:
            pass

        finally:
            if sign_key:
                zerome(sign_key)
                del sign_key

        return result
コード例 #10
0
ファイル: qrtoken.py プロジェクト: rb12345/LinOTP
    def server_hmac_secret(self):
        """ the server hmac secret for this specific token """

        server_secret_key = get_qrtoken_dh_secret_key()

        # user public key is saved base64 encoded

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

        hmac_secret = calc_dh(server_secret_key, user_public_key)
        zerome(server_secret_key)

        return hmac_secret
コード例 #11
0
ファイル: qrtoken.py プロジェクト: gsnbng/LinOTP
    def server_hmac_secret(self):
        """ the server hmac secret for this specific token """

        server_secret_key = get_qrtoken_dh_secret_key()

        # user public key is saved base64 encoded

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

        hmac_secret = calc_dh(server_secret_key, user_public_key)
        zerome(server_secret_key)

        return hmac_secret
コード例 #12
0
ファイル: default.py プロジェクト: gsnbng/LinOTP
    def verfiyMessageSignature(self, message, hex_mac, method=None,
                               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

        if method is None:
            method = SHA256

        try:
            sign_key = self.getSecret(slot_id)
            hmac = HMAC.new(sign_key, message, method)
            sign_mac = HMAC.new(sign_key, message, method).hexdigest()

            res = 0
            # as we compare on hex, we have to multiply by 2
            digest_size = hmac.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.error("Mac Comparison failed! %r", err)

        except Exception as exx:
            pass

        finally:
            if sign_key:
                zerome(sign_key)
                del sign_key

        return result
コード例 #13
0
ファイル: default.py プロジェクト: RDLM-01/Elm
    def decrypt(self, input, iv, 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
        '''

        log.debug('decrypt()')

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

        key = self.getSecret(id)
        aes = AES.new(key, AES.MODE_CBC, iv)
        # cko
        #import linotp.lib.yhsm as yhsm
        #y = yhsm.YubiHSM(0x1111, password="******")
        #y.unlock(password="******")
        #log.debug("CKO in: %s" % input)
        #output = binascii.hexlify(y.decrypt(input))
        #log.debug("CKO out: %s" % output)
        #
        output = aes.decrypt(input)
        #log.debug("CKO: output2: %s" % output)
        eof = output.rfind(u"\x01\x02")
        if eof >= 0: output = output[:eof]

        ## convert output from ascii, back to bin data
        data = binascii.a2b_hex(output)

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

        return data
コード例 #14
0
ファイル: default.py プロジェクト: gsnbng/LinOTP
    def decrypt(self, input, iv, 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
        '''

        log.debug('decrypt()')

        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
コード例 #15
0
    def decrypt(self, input, iv, 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
        '''

        log.debug('decrypt()')

        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
コード例 #16
0
ファイル: dpwOTP.py プロジェクト: ae-m/LinOTP
    def getOtp(self, date_string=None):

        key = self.secretObject.getKey()

        if date_string == None:
            date_string = datetime.now().strftime("%d%m%y")

        input = key + date_string

        md = hexlify(md5(input).digest())
        md = md[len(md) - self.digits:]
        otp = int(md, 16)
        otp = unicode(otp)
        otp = otp[len(otp) - self.digits:]

        zerome(key)
        del key

        return otp
コード例 #17
0
    def getOtp(self, date_string=None):

        key = self.secretObject.getKey()

        if date_string == None:
            date_string = datetime.now().strftime("%d%m%y")

        input = key + date_string

        md = hexlify(md5(input).digest())
        md = md[len(md) - self.digits:]
        otp = int(md, 16)
        otp = unicode(otp)
        otp = otp[len(otp) - self.digits:]

        zerome(key)
        del key

        return otp
コード例 #18
0
ファイル: default.py プロジェクト: RDLM-01/Elm
    def encrypt(self, data, iv, 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

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

        log.debug('encrypt()')

        if self.is_ready == 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 += u"\x01\x02"
        padding = (16 - len(input) % 16) % 16
        input += padding * "\0"
        aes = AES.new(key, AES.MODE_CBC, iv)

        # cko: ARGH: Only ECB!
        #import linotp.lib.yhsm as yhsm
        #y = yhsm.YubiHSM(0x1111, password="******")
        #y.unlock(password="******")
        #res = y.encrypt(input)
        #
        res = aes.encrypt(input)

        if self.crypted == False:
            zerome(key)
            del key
        return res
コード例 #19
0
ファイル: default.py プロジェクト: markrey/LinOTP
    def decrypt(self, input, iv, 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
        '''

        log.debug('decrypt()')

        if self.is_ready == 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 = output.rfind(u"\x01\x02")
        if eof >= 0:
            output = output[:eof]

        # convert output from ascii, back to bin data
        data = binascii.a2b_hex(output)

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

        return data
コード例 #20
0
ファイル: provider.py プロジェクト: super-rain/LinOTP
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
コード例 #21
0
ファイル: dpwOTP.py プロジェクト: ae-m/LinOTP
    def checkOtp(self, anOtpVal, window=0, options=None):
        '''
        window is the seconds before and after the current time
        '''
        res = -1

        key = self.secretObject.getKey()

        date_string = datetime.now().strftime("%d%m%y")
        input = key + date_string

        md = hexlify(md5(input).digest())
        md = md[len(md) - self.digits:]
        otp = int(md, 16)
        otp = unicode(otp)
        otp = otp[len(otp) - self.digits:]

        if unicode(anOtpVal) == otp:
            res = 1

        zerome(key)
        del key

        return res
コード例 #22
0
ファイル: default.py プロジェクト: choth02/LinOTP
    def encrypt(self, data, iv, 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

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

        log.debug('encrypt()')

        if self.is_ready == 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 += u"\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 == False:
            zerome(key)
            del key
        return res
コード例 #23
0
    def checkOtp(self, anOtpVal, window=0, options=None):
        '''
        window is the seconds before and after the current time
        '''
        res = -1

        key = self.secretObject.getKey()

        date_string = datetime.now().strftime("%d%m%y")
        input = key + date_string

        md = hexlify(md5(input).digest())
        md = md[len(md) - self.digits:]
        otp = int(md, 16)
        otp = unicode(otp)
        otp = otp[len(otp) - self.digits:]

        if unicode(anOtpVal) == otp:
            res = 1

        zerome(key)
        del key

        return res
コード例 #24
0
ファイル: qrtoken.py プロジェクト: jimmytuc/LinOTP
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 MAC of the response didn't match

    :return:
        Parsed/encrpted PairingReponse
    """

    data = decode_base64_urlsafe(enc_pairing_response)

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

    #            -----------------------
    #  fields   | R  | ciphertext | MAC |
    #            -----------------------
    #  size     | 32 |      ?     | 16  |
    #            -----------------------

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

    R = data[0:32]
    ciphertext = data[32:-16]
    mac = data[-16:]

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

    # calculate the shared secret

    # ----

    secret_key = get_qrtoken_secret_key()
    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)
    plaintext = cipher.decrypt_and_verify(ciphertext, mac)

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

    # parse decrypted response

    # ----

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

    # Parse Pairing Reponse Header (First 6 Bytes)

    #            -------------------------------------------
    #  fields   | version  | type | user token id |   ...   |
    #            -------------------------------------------
    #  size     |    1     |  1   |       4       |    ?    |
    #            -------------------------------------------

    resp_header = plaintext[0:6]
    version, token_type, user_token_id = struct.unpack('<bbI', resp_header)

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

    if token_type != 2:
        raise ValueError('wrong token type in user response, '
                         'expected: %d, got: %d' % (2, token_type))

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

    # get user public key (next 32 bytes)

    #            -----------------------------
    #  fields   | ... | user public key | ... |
    #            -----------------------------
    #  size     |  6  |       32        |  ?  |
    #            -----------------------------

    user_public_key = plaintext[6:6+32]

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

    # get serial and/or user login

    #            ---------------------------------
    #  fields   | ... | serial | NUL | user login |
    #            ---------------------------------
    #  size     | 38  |   ?    |  1  |     ?      |
    #            ---------------------------------

    # parse token_serial and user identification

    serial_user_data = plaintext[6+32:].split(b'\x00')
    serial = serial_user_data[0].decode('utf8')
    user_login = serial_user_data[1].decode('utf8')

    return PairingResponse(user_public_key, user_token_id, serial, user_login)
コード例 #25
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, challenge_data), with challenge_url
            being the push url and challenge data being the data, that
            will be used as message in the signing step.
        """

        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 = urandom(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 | ... |
        #            --------------------------------------
        #  size     |       1       |        8       |  ?  |
        #            --------------------------------------

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

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

        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

        url = 'lseqr://push/' + encode_base64_urlsafe(raw_data)

        return url, plaintext
コード例 #26
0
ファイル: mOTP.py プロジェクト: choth02/LinOTP
    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
        '''

        log.debug("[checkOtp] begin. checking the otpvalue %s window:%r, \
                            options:%r" % (anOtpVal, window, options))
        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
                log.debug("[checkOtp] otpvalue %r found at: %r" %
                            (anOtpVal, res))
                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'

        log.debug("[checkOtp] end. %s : returning result: %r, " % (msg, res))
        return res
コード例 #27
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 MAC of the response didn't match

    :return:
        Parsed/encrpted PairingReponse
    """

    data = decode_base64_urlsafe(enc_pairing_response)

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

    #            -----------------------
    #  fields   | R  | ciphertext | MAC |
    #            -----------------------
    #  size     | 32 |      ?     | 16  |
    #            -----------------------

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

    R = data[0:32]
    ciphertext = data[32:-16]
    mac = data[-16:]

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

    # calculate the shared secret

    # ----

    secret_key = get_qrtoken_dh_secret_key()
    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)
    plaintext = cipher.decrypt_and_verify(ciphertext, mac)

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

    # parse decrypted response

    # ----

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

    # Parse Pairing Reponse Header (First 6 Bytes)

    #            -------------------------------------------
    #  fields   | version  | type | user token id |   ...   |
    #            -------------------------------------------
    #  size     |    1     |  1   |       4       |    ?    |
    #            -------------------------------------------

    resp_header = plaintext[0:6]
    version, token_type, user_token_id = struct.unpack('<bbI', resp_header)

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

    if token_type != 2:
        raise ValueError('wrong token type in user response, '
                         'expected: %d, got: %d' % (2, token_type))

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

    # get user public key (next 32 bytes)

    #            -----------------------------
    #  fields   | ... | user public key | ... |
    #            -----------------------------
    #  size     |  6  |       32        |  ?  |
    #            -----------------------------

    user_public_key = plaintext[6:6+32]

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

    # get serial and/or user login

    #            ---------------------------------
    #  fields   | ... | serial | NUL | user login |
    #            ---------------------------------
    #  size     | 38  |   ?    |  1  |     ?      |
    #            ---------------------------------

    # parse token_serial and user identification

    serial_user_data = plaintext[6+32:].split(b'\x00')
    serial = serial_user_data[0].decode('utf8')
    user_login = serial_user_data[1].decode('utf8')

    return PairingResponse(user_public_key, user_token_id, serial, user_login)
コード例 #28
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)
コード例 #29
0
ファイル: qrtoken.py プロジェクト: gsnbng/LinOTP
    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 = urandom(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.new(ss).digest()
        U2 = SHA256.new(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 = ''

        if flags & CHALLENGE_HAS_SIGNATURE:

            hmac_message = nonce + pt_header + maybe_compressed_data_package

            sig = HMAC.new(self.server_hmac_secret, hmac_message,
                           digestmod=SHA256).digest()

            plaintext += sig

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

        plaintext += maybe_compressed_data_package

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

        user_message = nonce + pt_header + sig + data_package
        user_sig = HMAC.new(skB, user_message, digestmod=SHA256).digest()

        # 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
        url = 'lseqr://chal/' + encode_base64_urlsafe(raw_data)

        return url, user_sig
コード例 #30
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 = urandom(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 = ''
        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
        url = 'lseqr://chal/' + encode_base64_urlsafe(raw_data)

        return url, user_sig