diff --git a/README.md b/README.md
index 967ceae..b2ac4c3 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,8 @@ Depends on
`python manage.py collectstatic`
3. Add the following settings to your file
- ```python
+ ```python
+ from django.conf.global_settings import PASSWORD_HASHERS as DEFAULT_PASSWORD_HASHERS #Preferably at the same place where you import your other modules
MFA_UNALLOWED_METHODS=() # Methods that shouldn't be allowed for the user
MFA_LOGIN_CALLBACK="" # A function that should be called by username to login the user in session
MFA_RECHECK=True # Allow random rechecking of the user
@@ -77,6 +78,9 @@ Depends on
MFA_QUICKLOGIN=True # Allow quick login for returning users by provide only their 2FA
MFA_HIDE_DISABLE=('FIDO2',) # Can the user disable his key (Added in 1.2.0).
MFA_OWNED_BY_ENTERPRISE = FALSE # Who owns security keys
+ PASSWORD_HASHERS = DEFAULT_PASSWORD_HASHERS #Comment if PASSWORD_HASHER already set
+ PASSWORD_HASHERS += ['mfa.recovery.Hash']
+ RECOVERY_ITERATION = 350000 #Number of iteration for recovery code, higher is more secure, but uses more resources for generation and check...
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
diff --git a/example/example/settings.py b/example/example/settings.py
index 6c1b772..b55196d 100644
--- a/example/example/settings.py
+++ b/example/example/settings.py
@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/2.0/ref/settings/
"""
import os
+from django.conf.global_settings import PASSWORD_HASHERS as DEFAULT_PASSWORD_HASHERS
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -142,7 +143,9 @@ MFA_QUICKLOGIN=True # Allow quick login for returning users by provide on
MFA_HIDE_DISABLE=('',) # Can the user disable his key (Added in 1.2.0).
MFA_REDIRECT_AFTER_REGISTRATION="registered"
MFA_SUCCESS_REGISTRATION_MSG="Go to Home"
-
+PASSWORD_HASHERS = DEFAULT_PASSWORD_HASHERS #Comment if PASSWORD_HASHER already set
+PASSWORD_HASHERS += ['mfa.recovery.Hash']
+RECOVERY_ITERATION = 1 #Number of iteration for recovery code, higher is more secure, but uses more resources for generation and check...
TOKEN_ISSUER_NAME="PROJECT_NAME" #TOTP Issuer name
U2F_APPID="https://localhost" #URL For U2F
diff --git a/mfa/Email.py b/mfa/Email.py
index 7bbb26d..010d6d5 100644
--- a/mfa/Email.py
+++ b/mfa/Email.py
@@ -7,7 +7,6 @@ from .models import *
#from django.template.context import RequestContext
from .views import login
from .Common import send
-from . import recovery
def sendEmail(request,username,secret):
"""Send Email to the user after rendering `mfa_email_token_template`"""
@@ -35,7 +34,6 @@ def start(request):
from django.core.urlresolvers import reverse
except:
from django.urls import reverse
- recovery.genTokens(request, True) #recovery tokens
return HttpResponseRedirect(reverse(getattr(settings,'MFA_REDIRECT_AFTER_REGISTRATION','mfa_home')))
context["invalid"] = True
else:
diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py
index e41c27c..dcdf9f2 100644
--- a/mfa/FIDO2.py
+++ b/mfa/FIDO2.py
@@ -16,7 +16,6 @@ from .views import login, reset_cookie
import datetime
from .Common import get_redirect_url
from django.utils import timezone
-from . import recovery
def recheck(request):
@@ -67,7 +66,6 @@ def complete_reg(request):
uk.owned_by_enterprise = getattr(settings, "MFA_OWNED_BY_ENTERPRISE", False)
uk.key_type = "FIDO2"
uk.save()
- recovery.genTokens(request, True) #recovery tokens
return HttpResponse(simplejson.dumps({'status': 'OK'}))
except Exception as exp:
import traceback
diff --git a/mfa/TrustedDevice.py b/mfa/TrustedDevice.py
index aac4f6c..94b2136 100644
--- a/mfa/TrustedDevice.py
+++ b/mfa/TrustedDevice.py
@@ -7,7 +7,6 @@ from django.template.context_processors import csrf
from .models import *
import user_agents
from django.utils import timezone
-from . import recovery
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
x=''.join(random.choice(chars) for _ in range(size))
@@ -76,7 +75,6 @@ def add(request):
tk.properties["user_agent"]=ua
tk.save()
context["success"]=True
- recovery.genTokens(request, True) #recovery tokens
# tk.properties["user_agent"]=ua
# tk.save()
# context["success"]=True
diff --git a/mfa/U2F.py b/mfa/U2F.py
index 18387d2..0eb04f0 100644
--- a/mfa/U2F.py
+++ b/mfa/U2F.py
@@ -15,7 +15,6 @@ from .views import login
from .Common import get_redirect_url
import datetime
from django.utils import timezone
-from . import recovery
def recheck(request):
context = csrf(request)
@@ -99,7 +98,6 @@ def bind(request):
uk.properties = {"device":simplejson.loads(device.json),"cert":cert_hash}
uk.key_type = "U2F"
uk.save()
- recovery.genTokens(request, True) #recovery tokens
return HttpResponse("OK")
def sign(username):
diff --git a/mfa/recovery.py b/mfa/recovery.py
index 46d60a3..07a0b66 100644
--- a/mfa/recovery.py
+++ b/mfa/recovery.py
@@ -1,6 +1,7 @@
from django.shortcuts import render
from django.views.decorators.cache import never_cache
from django.template.context_processors import csrf
+from django.contrib.auth.hashers import make_password, PBKDF2PasswordHasher
from django.http import HttpResponse
from .Common import get_redirect_url
from .models import *
@@ -9,6 +10,9 @@ import random
import string
import datetime
+class Hash(PBKDF2PasswordHasher):
+ algorithm = 'pbkdf2_sha256_custom'
+ iterations = settings.RECOVERY_ITERATION
def token_left(request, username=None):
if not username and request:
@@ -32,46 +36,50 @@ def delTokens(request):
def randomGen(n):
return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(n))
+
+@never_cache
def genTokens(request, softGen=False):
if not softGen or (softGen and token_left(request) == 0):
#Delete old ones
delTokens(request)
number = 5
#Then generate new one
- newKeys = []
+ salt = randomGen(15)
+ hashedKeys = []
+ clearKeys = []
for i in range(5):
token = randomGen(5) + "-" + randomGen(5)
- newKeys.append(token)
+ hashedToken = make_password(token, salt, 'pbkdf2_sha256_custom')
+ hashedKeys.append(hashedToken)
+ clearKeys.append(token)
uk=User_Keys()
uk.username = request.user.username
- uk.properties={"secret_keys":newKeys, "enabled":[True for j in range(5)]}
+ uk.properties={"secret_keys":hashedKeys, "enabled":[True for j in range(5)], "salt":salt}
uk.key_type="RECOVERY"
+ uk.enabled = False
uk.save()
- return HttpResponse("Success")
+ return HttpResponse(simplejson.dumps({"keys":clearKeys}))
def verify_login(request, username,token):
for key in User_Keys.objects.filter(username=username, key_type = "RECOVERY"):
secret_keys = key.properties["secret_keys"]
+ salt = key.properties["salt"]
+ hashedToken = make_password(token, salt, "pbkdf2_sha256_custom")
for i in range(len(secret_keys)):
- if token == secret_keys[i] and key.properties["enabled"][i]:
+ if hashedToken == secret_keys[i] and key.properties["enabled"][i]:
key.properties["enabled"][i] = False
key.save()
- lastToken = False
- if token_left(None, username) == 0:
- lastToken = True
- return [True, key.id, lastToken]
+ return [True, key.id, token_left(None, username) == 0]
return [False]
-def getTokens(request):
- tokens = []
- enable = []
+def getTokenLeft(request):
+ enable = 0
for key in User_Keys.objects.filter(username=request.user.username, key_type = "RECOVERY"):
- secret_keys = key.properties["secret_keys"]
- for i in range(len(secret_keys)):
- tokens.append(secret_keys[i])
- enable.append(key.properties["enabled"][i])
- return HttpResponse(simplejson.dumps({"keys":tokens, "enable":enable}))
+ tokenStatus = key.properties["enabled"]
+ for i in range(len(tokenStatus)):
+ enable += 1
+ return HttpResponse(simplejson.dumps({"left":enable}))
@never_cache
def auth(request):
@@ -79,7 +87,7 @@ def auth(request):
context=csrf(request)
if request.method=="POST":
tokenLength = len(request.POST["otp"])
- if tokenLength == 10 and "RECOVERY" not in settings.MFA_UNALLOWED_METHODS:
+ if tokenLength == 11 and "RECOVERY" not in settings.MFA_UNALLOWED_METHODS:
#Backup code check
resBackup=verify_login(request, request.session["base_username"], token=request.POST["otp"])
if resBackup[0]:
diff --git a/mfa/templates/RECOVERY/Add.html b/mfa/templates/RECOVERY/Add.html
index 4f419ad..1b871e5 100644
--- a/mfa/templates/RECOVERY/Add.html
+++ b/mfa/templates/RECOVERY/Add.html
@@ -25,39 +25,36 @@