def clean(self): try: hex_validator()(self.key) if self.number: phone_number_validator(self.number) except DjangoValidationError as e: raise ValidationError(e)
class PhoneDevice(Device): """ Model with phone number and token seed linked to a user. """ number = models.CharField(max_length=16, validators=[phone_number_validator], verbose_name=_('number')) key = models.CharField(max_length=40, validators=[hex_validator()], default=lambda: random_hex(20), help_text="Hex-encoded secret key") method = models.CharField(max_length=4, choices=PHONE_METHODS, verbose_name=_('method')) @property def bin_key(self): return unhexlify(self.key.encode()) def verify_token(self, token): for drift in range(-5, 1): if totp(self.bin_key, drift=drift) == token: return True return False def generate_challenge(self): """ Sends the current TOTP token to `self.number` using `self.method`. """ token = '%06d' % totp(self.bin_key) if self.method == 'call': make_call(device=self, token=token) else: send_sms(device=self, token=token)
class EmailDevice(Device): """ A :class:`~django_otp.models.Device` that delivers a token to the user's registered email address (``user.email``). This is intended for demonstration purposes; if you allow users to reset their passwords via email, then this provides no security benefits. .. attribute:: key *CharField*: A hex-encoded secret key of up to 40 bytes. (Default: 20 random bytes) """ key = models.CharField( max_length=80, validators=[hex_validator()], default=lambda: random_hex(20), help_text='A hex-encoded secret key of up to 20 bytes.') @property def bin_key(self): return unhexlify(self.key.encode()) def generate_challenge(self): token = totp(self.bin_key) body = render_to_string('otp/email/token.txt', {'token': token}) send_mail(settings.OTP_EMAIL_SUBJECT, body, settings.OTP_EMAIL_SENDER, [self.user.email]) message = "sent by email" return message def verify_token(self, token): try: token = int(token) except Exception: verified = False else: verified = any( totp(self.bin_key, drift=drift) == token for drift in [0, -1]) return verified
def key_validator(*args, **kwargs): """Wraps hex_validator generator, to keep makemigrations happy.""" return hex_validator()(*args, **kwargs)
def key_validator(value): return hex_validator()(value)
def id_validator(value): return hex_validator(6)(value)
def key_validator(value): # pragma: no cover """ Obsolete code here for migrations. """ return hex_validator()(value)
def token_validator(*args, **kwargs): """ Wraps hex_validator generator satisfying `makemigrations` """ return hex_validator()(*args, **kwargs)
class TOTPDevice(Device): """ A generic TOTP :class:`~django_otp.models.Device`. The model fields mostly correspond to the arguments to :func:`django_otp.oath.totp`. They all have sensible defaults, including the key, which is randomly generated. .. attribute:: key *CharField*: A hex-encoded secret key of up to 40 bytes. (Default: 20 random bytes) .. attribute:: step *PositiveSmallIntegerField*: The time step in seconds. (Default: 30) .. attribute:: t0 *BigIntegerField*: The Unix time at which to begin counting steps. (Default: 0) .. attribute:: digits *PositiveSmallIntegerField*: The number of digits to expect in a token (6 or 8). (Default: 6) .. attribute:: tolerance *PositiveSmallIntegerField*: The number of time steps in the past or future to allow. For example, if this is 1, we'll accept any of three tokens: the current one, the previous one, and the next one. (Default: 1) .. attribute:: drift *SmallIntegerField*: The number of time steps the prover is known to deviate from our clock. If :setting:`OTP_TOTP_SYNC` is ``True``, we'll update this any time we match a token that is not the current one. (Default: 0) .. attribute:: last_t *BigIntegerField*: The time step of the last verified token. To avoid verifying the same token twice, this will be updated on each successful verification. Only tokens at a higher time step will be verified subsequently. (Default: -1) """ key = models.CharField( max_length=80, validators=[hex_validator()], default=lambda: random_hex(20), help_text="A hex-encoded secret key of up to 40 bytes.") step = models.PositiveSmallIntegerField( default=30, help_text="The time step in seconds.") t0 = models.BigIntegerField( default=0, help_text="The Unix time at which to begin counting steps.") digits = models.PositiveSmallIntegerField( choices=[(6, 6), (8, 8)], default=6, help_text="The number of digits to expect in a token.") tolerance = models.PositiveSmallIntegerField( default=1, help_text="The number of time steps in the past or future to allow.") drift = models.SmallIntegerField( default=0, help_text= "The number of time steps the prover is known to deviate from our clock." ) last_t = models.BigIntegerField( default=-1, help_text= "The t value of the latest verified token. The next token must be at a higher time step." ) class Meta(Device.Meta): verbose_name = "TOTP device" @property def bin_key(self): """ The secret key as a binary string. """ return unhexlify(self.key.encode()) def verify_token(self, token): OTP_TOTP_SYNC = getattr(settings, 'OTP_TOTP_SYNC', True) try: token = int(token) except Exception: verified = False else: key = self.bin_key totp = TOTP(key, self.step, self.t0, self.digits) totp.time = time.time() for offset in range(-self.tolerance, self.tolerance + 1): totp.drift = self.drift + offset if (totp.t() > self.last_t) and (totp.token() == token): self.last_t = totp.t() if (offset != 0) and OTP_TOTP_SYNC: self.drift += offset self.save() verified = True break else: verified = False return verified
class HOTPDevice(Device): """ A generic HOTP :class:`~django_otp.models.Device`. The model fields mostly correspond to the arguments to :func:`django_otp.oath.hotp`. They all have sensible defaults, including the key, which is randomly generated. .. attribute:: key *CharField*: A hex-encoded secret key of up to 40 bytes. (Default: 20 random bytes) .. attribute:: digits *PositiveSmallIntegerField*: The number of digits to expect from the token generator (6 or 8). (Default: 6) .. attribute:: tolerance *PositiveSmallIntegerField*: The number of missed tokens to tolerate. (Default: 5) .. attribute:: counter *BigIntegerField*: The next counter value to expect. (Initial: 0) """ key = models.CharField( max_length=80, validators=[hex_validator()], default=lambda: random_hex(20), help_text="A hex-encoded secret key of up to 40 bytes.") digits = models.PositiveSmallIntegerField( choices=[(6, 6), (8, 8)], default=6, help_text="The number of digits to expect in a token.") tolerance = models.PositiveSmallIntegerField( default=5, help_text="The number of missed tokens to tolerate.") counter = models.BigIntegerField( default=0, help_text="The next counter value to expect.") class Meta(Device.Meta): verbose_name = "HOTP device" @property def bin_key(self): """ The secret key as a binary string. """ return unhexlify(self.key.encode()) def verify_token(self, token): try: token = int(token) except Exception: verified = False else: key = self.bin_key for counter in range(self.counter, self.counter + self.tolerance + 1): if hotp(key, counter, self.digits) == token: verified = True self.counter = counter + 1 self.save() break else: verified = False return verified