__all__ = ["OTP"]
[docs]class OTP:
"""This class handles everything to do with obtaining and
verifying a one-time-password"""
def __init__(self, secret=None):
"""This creates a new one-time-password"""
try:
import pyotp as _pyotp
except:
from Acquire.Crypto import OTPError
raise OTPError(
"Cannot create a one-time-password as the "
"pyotp module is not available. Please install and try again")
if secret:
self._secret = secret
else:
self._secret = _pyotp.random_base32()
def __str__(self):
"""Return a string representation of this OTP"""
return "OTP()"
[docs] @staticmethod
def decrypt(secret, key):
"""Construct a OTP from the passed encrypted secret
that will be decrypted with the passed private key"""
from Acquire.Crypto import PrivateKey as _PrivateKey
if not isinstance(key, _PrivateKey):
raise TypeError(
"You can only encrypt a OTP using a valid "
"PrivateKey - not using a %s" % key.__class__)
otp = OTP()
otp._secret = key.decrypt(secret)
return otp
[docs] def encrypt(self, key):
"""This uses the passed public key to encrypt and return the
secret"""
from Acquire.Crypto import PublicKey as _PublicKey
if not isinstance(key, _PublicKey):
raise TypeError(
"You can only encrypt a OTP using a valid "
"PublicKey - not using a %s" % key.__class__)
return key.encrypt(self._secret)
def _totp(self):
"""Return the time-based one-time-password based on this secret"""
try:
import pyotp as _pyotp
return _pyotp.totp.TOTP(self._secret)
except:
from Acquire.Crypto import OTPError
raise OTPError("You cannot get a null OTP - create one first!")
[docs] def provisioning_uri(self, username, issuer="Acquire"):
"""Return the provisioning URI, assuming this secret is
for the user called 'username' and is issued by 'issuer'"""
return self._totp().provisioning_uri(username, issuer_name=issuer)
[docs] def secret(self):
"""Return the otpsecret for this generator"""
return self._secret
[docs] def generate(self):
"""Generate and return the current OTP code"""
totp = self._totp()
return totp.now()
[docs] def verify(self, code, once_only=False):
"""Verify that the passed code is correct. This raises an exception
if the code is incorrect, or does nothing if the code is correct
If 'once_only' is True, then this will attempt to store global
state to ensure that the passed code can be used only once.
"""
# the OTP is valid for 1 minute. We will extend this so that
# it is valid for 3 minutes (1 minute before and after). This
# improves usability and tolerance for clock drift with only
# minor increase in OTP validity time
if not self._totp().verify(code, valid_window=1):
from Acquire.Crypto import OTPError
raise OTPError("The passed OTP code is incorrect")
# note that, ideally, we need to save whether or not this code
# has been used, as we need to prevent the case of someone
# eves-dropping on the password and code and using it again
# within the 3-minute window. We will leave this to the caller
# of this function to record!