def test_translation_single_interpretation(self): otp_str1 = 'cccfgvgitchndibrrtuhdrgeufrdkrjfgutfjbnhhglv' otp_str2 = 'cccagvgitchndibrrtuhdrgeufrdkrjfgutfjbnhhglv' otp1 = OTP(otp_str1) otp2 = OTP(otp_str2) self.assertEqual(otp1.otp, otp_str1) self.assertEqual(otp2.otp, otp_str2)
def test_otp_class(self): otp1 = OTP('tlerefhcvijlngibueiiuhkeibbcbecehvjiklltnbbl') otp2 = OTP('jjjjjjjjnhe.ngcgjeiuujjjdtgihjuecyixinxunkhj', translate_otp=True) self.assertEqual(otp1.device_id, 'tlerefhcvijl') self.assertEqual(otp2.otp, 'ccccccccljdeluiucdgffccchkugjcfditgbglbflvjc')
def validate_code(self, code): otp = OTP(code) if not self.obj.secret or self.obj.secret != otp.device_id: return False if not self._validate_yubikey_otp(code): return False return True
def confirm_activation(self, code): """ After successful confirmation store `device_id` in field `secret` MFAMethod model. """ otp = OTP(code) self.obj.secret = otp.device_id self.obj.save(update_fields=('secret',))
def verify_multi(self, otp_list, max_time_window=DEFAULT_MAX_TIME_WINDOW, sl=None, timeout=None): """ Verify a provided list of OTPs. :param max_time_window: Maximum number of seconds which can pass between the first and last OTP generation for the OTP to still be considered valid. :type max_time_window: ``int`` """ # Create the OTP objects otps = [] for otp in otp_list: otps.append(OTP(otp, self.translate_otp)) if len(otp_list) < 2: raise ValueError('otp_list needs to contain at least two OTPs') device_ids = set() for otp in otps: device_ids.add(otp.device_id) # Check that all the OTPs contain same device id if len(device_ids) != 1: raise Exception('OTPs contain different device ids') # Now we verify the OTPs and save the server response for each OTP. # We need the server response, to retrieve the timestamp. # It's possible to retrieve this value locally, without querying the # server but in this case, user would need to provide his AES key. for otp in otps: response = self.verify(otp.otp, True, sl, timeout, return_response=True) if not response: return False otp.timestamp = int(response['timestamp']) count = len(otps) delta = otps[count - 1].timestamp - otps[0].timestamp # OTPs have an 8Hz timestamp counter so we need to divide it to get # seconds delta = delta / 8 if delta < 0: raise Exception('delta is smaller than zero. First OTP appears to ' 'be older than the last one') if delta > max_time_window: raise Exception('More than %s seconds have passed between ' 'generating the first and the last OTP.' % (max_time_window)) return True
def validate_code(self, code: str) -> bool: if (not self._mfa_method.secret or self._mfa_method.secret != OTP(code).device_id): return False return self._validate_yubikey_otp(code)
def confirm_activation(self, code: str) -> None: self._mfa_method.secret = OTP(code).device_id self._mfa_method.save(update_fields=("secret", ))
def test_translation_multiple_interpretations(self): otp_str1 = 'vvbtbtndhtlfguefgluvbdcetnitidgkvfkbicevgcin' otp1 = OTP(otp_str1) self.assertEqual(otp1.otp, otp_str1)
def offline_yubikey(monkeypatch, fake_yubikey): def mock_verify(*args, **kwargs): return True monkeypatch.setattr(target=Yubico, name="verify", value=mock_verify) assert OTP("123456").device_id == FAKE_YUBI_SECRET
def verify(self, otp, timestamp=False, sl=None, timeout=None, return_response=False): """ Verify a provided OTP. :param otp: OTP to verify. :type otp: ``str`` :param timestamp: True to include request timestamp and session counter in the response. Defaults to False. :type timestamp: ``bool`` :param sl: A value indicating percentage of syncing required by client. :type sl: ``int`` or ``str`` :param timeout: Number of seconds to wait for sync responses. :type timeout: ``int`` :param return_response: True to return a response object instead of the status code. Defaults to False. :type return_response: ``bool`` :return: True is the provided OTP is valid, False if the REPLAYED_OTP status value is returned or the response message signature verification failed and None for the rest of the status values. """ ca_bundle_path = self._get_ca_bundle_path() otp = OTP(otp, self.translate_otp) rand_str = b(os.urandom(30)) nonce = base64.b64encode(rand_str, b('xz'))[:25].decode('utf-8') query_string = self.generate_query_string(otp.otp, nonce, timestamp, sl, timeout) threads = [] timeout = timeout or DEFAULT_TIMEOUT for url in self.api_urls: thread = URLThread(url='%s?%s' % (url, query_string), timeout=timeout, verify_cert=self.verify_cert, ca_bundle_path=ca_bundle_path, max_retries=self.max_retries, retry_delay=self.retry_delay) thread.start() threads.append(thread) # Wait for a first positive or negative response start_time = time.time() # If there's only one server to talk to, raise thread exceptions. # Otherwise we end up ignoring a good answer from a different # server later. raise_exceptions = (len(threads) == 1) # pylint: disable=too-many-nested-blocks while threads and (start_time + timeout) > time.time(): for thread in threads: if not thread.is_alive(): if thread.exception and raise_exceptions: raise thread.exception elif thread.response: status = self.verify_response(thread.response, otp.otp, nonce, return_response) if status: # pylint: disable=no-else-return if return_response: return status else: return True threads.remove(thread) time.sleep(0.1) # Timeout or no valid response received raise Exception('NO_VALID_ANSWERS')