2020-09-02 14:48:23 +00:00
|
|
|
import base64
|
|
|
|
import hmac
|
|
|
|
import io
|
|
|
|
import os
|
|
|
|
import struct
|
|
|
|
import time
|
2020-09-02 17:12:15 +00:00
|
|
|
import pyotp
|
2020-09-02 14:48:23 +00:00
|
|
|
import qrcode
|
2020-09-03 17:07:21 +00:00
|
|
|
from mailconfig import get_mfa_state, set_mru_totp_code
|
2020-09-02 14:48:23 +00:00
|
|
|
|
|
|
|
def get_secret():
|
|
|
|
return base64.b32encode(os.urandom(20)).decode('utf-8')
|
|
|
|
|
|
|
|
def get_otp_uri(secret, email):
|
2020-09-02 17:12:15 +00:00
|
|
|
return pyotp.TOTP(secret).provisioning_uri(
|
|
|
|
name=email,
|
|
|
|
issuer_name='mailinabox'
|
2020-09-02 14:48:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
def get_qr_code(data):
|
|
|
|
qr = qrcode.make(data)
|
|
|
|
byte_arr = io.BytesIO()
|
|
|
|
qr.save(byte_arr, format='PNG')
|
|
|
|
|
|
|
|
encoded = base64.b64encode(byte_arr.getvalue()).decode('utf-8')
|
|
|
|
return 'data:image/png;base64,{}'.format(encoded)
|
|
|
|
|
|
|
|
def validate(secret, token):
|
|
|
|
"""
|
|
|
|
@see https://tools.ietf.org/html/rfc6238#section-4
|
|
|
|
@see https://tools.ietf.org/html/rfc4226#section-5.4
|
|
|
|
"""
|
2020-09-02 17:12:15 +00:00
|
|
|
totp = pyotp.TOTP(secret)
|
2020-09-03 09:19:19 +00:00
|
|
|
return totp.verify(token, valid_window=1)
|
|
|
|
|
|
|
|
class MissingTokenError(ValueError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class BadTokenError(ValueError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class TOTPStrategy():
|
|
|
|
def __init__(self, email):
|
|
|
|
self.type = 'totp'
|
|
|
|
self.email = email
|
|
|
|
|
|
|
|
def store_successful_login(self, token, env):
|
2020-09-03 17:07:21 +00:00
|
|
|
return set_mru_totp_code(self.email, token, env)
|
2020-09-03 09:19:19 +00:00
|
|
|
|
|
|
|
def validate_request(self, request, env):
|
2020-09-03 17:07:21 +00:00
|
|
|
mfa_state = get_mfa_state(self.email, env)
|
2020-09-03 09:19:19 +00:00
|
|
|
|
|
|
|
# 2FA is not enabled, we can skip further checks
|
2020-09-03 17:07:21 +00:00
|
|
|
if mfa_state['type'] != 'totp':
|
2020-09-03 09:19:19 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
# If 2FA is enabled, raise if:
|
|
|
|
# 1. no token is provided via `x-auth-token`
|
|
|
|
# 2. a previously supplied token is used (to counter replay attacks)
|
|
|
|
# 3. the token is invalid
|
|
|
|
# in that case, we need to raise and indicate to the client to supply a TOTP
|
|
|
|
token_header = request.headers.get('x-auth-token')
|
|
|
|
|
2020-09-04 18:28:15 +00:00
|
|
|
if not token_header:
|
2020-09-03 09:19:19 +00:00
|
|
|
raise MissingTokenError("Two factor code missing (no x-auth-token supplied)")
|
|
|
|
|
|
|
|
# TODO: Should a token replay be handled as its own error?
|
2020-09-06 10:54:45 +00:00
|
|
|
if hmac.compare_digest(token_header, mfa_state['mru_token']) or validate(mfa_state['secret'], token_header) != True:
|
2020-09-03 09:19:19 +00:00
|
|
|
raise BadTokenError("Two factor code incorrect")
|
|
|
|
|
|
|
|
self.store_successful_login(token_header, env)
|
|
|
|
return True
|