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
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
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
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
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
def checkOtp(self, anOtpVal): res = -1 key = self.secretObject.getKey() if key == anOtpVal: res = 0 zerome(key) del key return res
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
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
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
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
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
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
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
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
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
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
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
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)
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
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)
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)
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
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