recovery code hashing

This commit is contained in:
Spitap
2022-08-25 19:19:30 +02:00
parent fe06e4a34d
commit 98ca5e972d
11 changed files with 63 additions and 60 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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]:

View File

@@ -25,39 +25,36 @@
</style>
<script src="{% static 'mfa/js/qrious.min.js' %}" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function addToken() {
listToken()
});
function listToken() {
$.ajax({
"url":"{% url 'get_recovery_tokens' %}",dataType:"JSON",
success:function (data) {
let htmlkey="";
for (let i = 0; i < data.keys.length; i++) {
if (data.enable[i] == 0)
{
htmlkey +="<pre class='crossed'>" +data.keys[i] + "</pre>"
}
else
{
htmlkey +="<pre>" +data.keys[i] + "</pre>"
}
};
document.getElementById('tokens').innerHTML = htmlkey
$(document).ready(function checkTokenLeft() {
$.ajax({"url":"{% url 'get_recovery_token_left' %}", dataType:"JSON",
success:function (data) {
tokenLeft = data.left
let html
if (tokenLeft == 0) {
html = "<h6>You don't have any backup code left, please generate new ones !</h6>"
}
})
};
else {
html = "<p>You still have "+tokenLeft+" backup code left."
}
document.getElementById('tokens').innerHTML = html
}})
});
function confirmRegenerateTokens() {
htmlModal = "<h6>Caution! you can only note these token now, else you will need to generate new ones.</h6><button onclick='regenerateTokens()' class='btn btn-success'>Regenerate</button>"
$("#modal-title").html("Are you sure you want to regenerate your recovery tokens?")
$("#modal-body").html("<button onclick='regenerateTokens()' class='btn btn-success'>Regenerate</button>")
$("#modal-body").html(htmlModal)
$("#popUpModal").modal('show')
}
function regenerateTokens() {
$.ajax({
"url":"{% url 'regen_recovery_tokens' %}",
"url":"{% url 'regen_recovery_tokens' %}", dataType:"JSON",
success:function (data) {
console.warn("ksfvkjs")
listToken()
let htmlkey="";
for (let i = 0; i < data.keys.length; i++) {
htmlkey +="<pre>" +data.keys[i] + "</pre>"
}
document.getElementById('tokens').innerHTML = htmlkey
$("#popUpModal").modal('hide')
}
})

View File

@@ -22,10 +22,11 @@
})
}
function tryToAuth() {
if ($("#otp").val().length == 6) {
document.getElementById("formLogin").submit();
const otp_length = $("#otp").val().length
if (otp_length == 6) {
document.getElementById("formLogin").submit();
}
else if ($("#otp").val().length == 10) {
else if (otp_length == 11) {
const form = document.getElementById("formLogin");
form.setAttribute("ACTION", "{% url 'recovery_auth' %}")
form.submit();
@@ -70,7 +71,7 @@
<span class="input-group-addon input-group-text">
<i class="glyphicon glyphicon-lock bi bi-lock"></i>
</span>
<input class="form-control" size="6" MaxLength="10" value="" placeholder="e.g 55552" name="otp" type="text" id="otp" autofocus>
<input class="form-control" size="6" MaxLength="11" value="" placeholder="e.g 55552" name="otp" type="text" id="otp" autofocus>
</div>
</div>

View File

@@ -11,7 +11,6 @@ import pyotp
from .views import login
import datetime
from django.utils import timezone
from . import recovery
import random
@@ -31,7 +30,6 @@ def recheck(request):
if verify_login(request,request.user.username, token=request.POST["otp"]):
import time
request.session["mfa"]["rechecked_at"] = time.time()
recovery.genTokens(request, True) #recovery tokens
return HttpResponse(simplejson.dumps({"recheck": True}), content_type="application/json")
else:
return HttpResponse(simplejson.dumps({"recheck": False}), content_type="application/json")

View File

@@ -13,7 +13,7 @@ urlpatterns = [
url(r'totp/recheck', totp.recheck, name="totp_recheck"),
url(r'recovery/start', recovery.start, name="manage_recovery_codes"),
url(r'recovery/getTokens', recovery.getTokens, name="get_recovery_tokens"),
url(r'recovery/getTokenLeft', recovery.getTokenLeft, name="get_recovery_token_left"),
url(r'recovery/genTokens', recovery.genTokens, name="regen_recovery_tokens"),
url(r'recovery/auth', recovery.auth, name="recovery_auth"),