| Server IP : 85.214.239.14 / Your IP : 216.73.216.210 Web Server : Apache/2.4.65 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Mon Sep 30 15:36:27 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 8.2.29 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /proc/3/task/3/root/srv/modoboa/env/lib/python3.5/site-packages/oath/ |
Upload File : |
# -*- Mode: Python; python-indent-offset: 4 -*-
'''
Google Authenticator API
------------------------
Google Authenticator is based on HOTP and TOTP. It provides a simple way of
provisionning an OTP generator through a new URL scheme.
This module provides parsing and high-level API over the classic HOTP and TOTP
APIs provided by the oath.hotp and oath.totp modules.
'''
try:
from urlparse import urlparse, parse_qs
from urllib import urlencode, quote
except ImportError:
from urllib.parse import urlparse, parse_qs, urlencode, quote
import base64
import hashlib
from oath import _hotp as hotp
from oath import _totp as totp
from . import _utils
import binascii
__all__ = ('GoogleAuthenticator', 'from_b32key', 'GoogleAuthenticatorURI')
LABEL = 'label'
TYPE = 'type'
ALGORITHM = 'algorithm'
DIGITS = 'digits'
SECRET = 'secret'
COUNTER = 'counter'
PERIOD = 'period'
TOTP = 'totp'
HOTP = 'hotp'
DRIFT = 'drift'
ISSUER = 'issuer'
def lenient_b32decode(data):
data = data.upper() # Ensure correct case
data += ('=' * ((8 - len(data)) % 8)) # Ensure correct padding
return base64.b32decode(data.encode('ascii'))
def parse_otpauth(otpauth_uri):
if not otpauth_uri.startswith('otpauth://'):
raise ValueError('Invalid otpauth URI', otpauth_uri)
# urlparse in python 2.6 can't handle the otpauth:// scheme, skip it
parsed_uri = urlparse(otpauth_uri[8:])
params = dict(((k, v[0]) for k, v in parse_qs(parsed_uri.query).items()))
params[LABEL] = parsed_uri.path[1:]
params[TYPE] = parsed_uri.hostname
if SECRET not in params:
raise ValueError('Missing secret field in otpauth URI', otpauth_uri)
try:
params[SECRET] = _utils.tohex(lenient_b32decode(params[SECRET]))
except TypeError:
raise ValueError('Invalid base32 encoding of the secret field in '
'otpauth URI', otpauth_uri)
if ALGORITHM in params:
params[ALGORITHM] = params[ALGORITHM].lower()
if params[ALGORITHM] not in ('sha1', 'sha256', 'sha512', 'md5'):
raise ValueError('Invalid value for algorithm field in otpauth '
'URI', otpauth_uri)
else:
params[ALGORITHM] = 'sha1'
try:
params[ALGORITHM] = getattr(hashlib, params[ALGORITHM])
except AttributeError:
raise ValueError('Unsupported algorithm %s in othauth URI' %
params[ALGORITHM], otpauth_uri)
for key in (DIGITS, PERIOD, COUNTER):
try:
if key in params:
params[key] = int(params[key])
except ValueError:
raise ValueError('Invalid value for field %s in otpauth URI, must '
'be a number' % key, otpauth_uri)
if COUNTER not in params:
params[COUNTER] = 0 # what else ?
if DIGITS in params:
if params[DIGITS] not in (6, 8):
raise ValueError('Invalid value for field digits in othauth URI, it '
'must 6 or 8', otpauth_uri)
else:
params[DIGITS] = 6
if params[TYPE] == HOTP and COUNTER not in params:
raise ValueError('Missing field counter in otpauth URI, it is '
'mandatory with the hotp type', otpauth_uri)
if params[TYPE] == TOTP and PERIOD not in params:
params[PERIOD] = 30
return params
def from_b32key(b32_key, state=None):
'''Some phone app directly accept a partial b32 encoding, we try to emulate that'''
try:
lenient_b32decode(b32_key)
except TypeError:
raise ValueError('invalid base32 value')
return GoogleAuthenticator('otpauth://totp/xxx?%s' %
urlencode({'secret': b32_key}), state=state)
class GoogleAuthenticator(object):
def __init__(self, otpauth_uri, state=None):
self.otpauth_uri = otpauth_uri
self.parsed_otpauth_uri = parse_otpauth(otpauth_uri)
self.generator_state = state or {}
self.acceptor_state = state or {}
@property
def label(self):
return self.parsed_otpauth_uri[LABEL]
def generate(self, t=None):
format = 'dec%s' % self.parsed_otpauth_uri[DIGITS]
hash = self.parsed_otpauth_uri[ALGORITHM]
secret = self.parsed_otpauth_uri[SECRET]
state = self.generator_state
if self.parsed_otpauth_uri[TYPE] == HOTP:
if COUNTER not in state:
state[COUNTER] = self.parsed_otpauth_uri[COUNTER]
otp = hotp.hotp(secret, state[COUNTER], format=format, hash=hash)
state[COUNTER] += 1
return otp
elif self.parsed_otpauth_uri[TYPE] == TOTP:
period = self.parsed_otpauth_uri[PERIOD]
return totp.totp(secret, format=format,
period=period,
hash=hash, t=t)
else:
raise NotImplementedError(self.parsed_otpauth_uri[TYPE])
def accept(self, otp, hotp_drift=3, hotp_backward_drift=0,
totp_forward_drift=1, totp_backward_drift=1, t=None):
format = 'dec%s' % self.parsed_otpauth_uri[DIGITS]
hash = self.parsed_otpauth_uri[ALGORITHM]
secret = self.parsed_otpauth_uri[SECRET]
state = self.acceptor_state
if self.parsed_otpauth_uri[TYPE] == HOTP:
if COUNTER not in state:
state[COUNTER] = self.parsed_otpauth_uri[COUNTER]
ok, state[COUNTER] = hotp.accept_hotp(otp, secret, state[COUNTER],
format=format, hash=hash, drift=hotp_drift,
backward_drift=hotp_backward_drift)
return ok
elif self.parsed_otpauth_uri[TYPE] == TOTP:
period = self.parsed_otpauth_uri[PERIOD]
if DRIFT not in state:
state[DRIFT] = 0
ok, state[DRIFT] = totp.accept_totp(secret, otp, format=format,
period=period, forward_drift=totp_forward_drift,
backward_drift=totp_backward_drift, drift=state[DRIFT],
t=t)
return ok
else:
raise NotImplementedError(self.parsed_otpauth_uri[TYPE])
class GoogleAuthenticatorURI(object):
""" used to create an URI for Google Authenticator, needs to be converted
in QR afterwards
"""
def __init__(self):
""" constructor """
return
def generate(self,
secret,
type='totp',
account='alex',
issuer=None,
algo='sha1',
digits=6,
init_counter=None):
"""
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
"""
args = {}
uri = 'otpauth://{0}/{1}?{2}'
try:
# converts the secret to a 16 cars string
a = binascii.unhexlify(secret)
args[SECRET] = base64.b32encode(a).decode('ascii')
except binascii.Error as ex:
raise ValueError(str(ex))
except Exception as ex:
print(ex)
raise ValueError('invalid secret format')
if type not in [TOTP, HOTP]:
raise ValueError('type should be totp or hotp, got ',
type)
if type != TOTP:
args['type'] = type
if algo not in ['sha1', 'sha256', 'sha512']:
raise ValueError('algo should be sha1, sha256 or sha512, got ',
algo)
if algo != 'sha1':
args['algorithm'] = algo
if init_counter is not None:
if type != HOTP:
raise ValueError('type should be hotp when ',
'setting init_counter')
if int(init_counter) < 0:
raise ValueError('init_counter should be positive')
args[COUNTER] = int(init_counter)
digits = int(digits)
if digits != 6 and digits != 8:
raise ValueError('digits should be 6 or 8')
if digits != 6:
args[DIGITS] = digits
args[PERIOD] = 30
account = quote(account)
if issuer is not None:
account = quote(issuer) + ':' + account
args[ISSUER] = issuer
uri = uri.format(type, account, urlencode(args).replace("+", "%20"))
return uri