From 98b361d73d1410c8da2a511c6a9195d28beacccb Mon Sep 17 00:00:00 2001 From: Mohamed El-Kalioby Date: Mon, 17 Oct 2022 20:56:09 +0300 Subject: [PATCH] Removed CBOR and moved to JSON for Communication --- CHANGELOG.md | 10 + README.md | 2 +- mfa/FIDO2.py | 73 +++--- mfa/static/mfa/js/base64url.js | 73 ++++++ mfa/static/mfa/js/cbor.js | 406 ------------------------------- mfa/static/mfa/js/helpers.js | 25 ++ mfa/templates/FIDO2/Add.html | 26 +- mfa/templates/FIDO2/Auth_JS.html | 33 ++- requirements.txt | 2 +- setup.py | 4 +- 10 files changed, 191 insertions(+), 463 deletions(-) create mode 100644 mfa/static/mfa/js/base64url.js delete mode 100644 mfa/static/mfa/js/cbor.js create mode 100644 mfa/static/mfa/js/helpers.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 179ef56..419778f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,14 @@ # Change Log +## 3.0 (Unreleased) +* Updated to fido2==1.1.0 +* Removed: CBOR and exchange is done in JSON now +* Added: the following settings + * `MFA_FIDO2_RESIDENT_KEY`: Defaults to `Discouraged` which was the old behaviour + * `MFA_FIDO2_AUTHENTICATOR_ATTACHMENT`: If you like to have a PLATFORM Authenticator, Defaults to NONE + * `MFA_FIDO2_USER_VERIFICATION`: If you need User Verification + * `MFA_FIDO2_ATTESTATION_PREFERENCE`: If you like to have an Attention +* Added: ConditionalUI support (check example) + ## 2.6.1 * Fix: CVE-2022-42731: related to the possibility of registration replay attack. Thanks to 'SSE (Secure Systems Engineering)' diff --git a/README.md b/README.md index dadccc8..ed1ec08 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Depends on * Added: `MFA_SUCCESS_REGISTRATION_MSG` & `MFA_REDIRECT_AFTER_REGISTRATION` * Starting version 2.6.0 * Added: `MFA_ALWAYS_GO_TO_LAST_METHOD`, `MFA_RENAME_METHODS`, `MFA_ENFORCE_RECOVERY_METHOD` & `RECOVERY_ITERATION` - * Starting version 2.7.0 + * Starting version 3.0 * Added: `MFA_FIDO2_RESIDENT_KEY`, `MFA_FIDO2_AUTHENTICATOR_ATTACHMENT`, `MFA_FIDO2_USER_VERIFICATION`, `MFA_FIDO2_ATTESTATION_PREFERENCE` 4. Break your login function diff --git a/mfa/FIDO2.py b/mfa/FIDO2.py index ef1cd32..09d446a 100644 --- a/mfa/FIDO2.py +++ b/mfa/FIDO2.py @@ -1,6 +1,6 @@ from fido2.client import Fido2Client from fido2.server import Fido2Server, PublicKeyCredentialRpEntity -from fido2.webauthn import AttestationObject, AuthenticatorData, CollectedClientData +from fido2.webauthn import AttestationObject, AuthenticatorData, CollectedClientData, RegistrationResponse from django.template.context_processors import csrf from django.views.decorators.csrf import csrf_exempt from django.shortcuts import render @@ -16,8 +16,14 @@ from .views import login, reset_cookie import datetime from .Common import get_redirect_url from django.utils import timezone +import fido2.features from django.http import JsonResponse +def enable_json_mapping(): + try: + fido2.features.webauthn_json_mapping.enabled = True + except: + pass def recheck(request): """Starts FIDO2 recheck""" context = csrf(request) @@ -36,6 +42,7 @@ def getServer(): def begin_registeration(request): """Starts registering a new FIDO Device, called from API""" + enable_json_mapping() server = getServer() from mfa import ResidentKey resident_key = getattr(settings,'MFA_FIDO2_RESIDENT_KEY', ResidentKey.DISCOURAGED) @@ -43,31 +50,30 @@ def begin_registeration(request): user_verification = getattr(settings,'MFA_FIDO2_USER_VERIFICATION', None) registration_data, state = server.register_begin({ u'id': request.user.username.encode("utf8"), - u'name': (request.user.first_name + " " + request.user.last_name), + u'name': request.user.username, u'displayName': request.user.username, }, getUserCredentials(request.user.username),user_verification = user_verification, resident_key_requirement = resident_key, authenticator_attachment = auth_attachment) - request.session['fido_state'] = state - - return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream') + request.session['fido2_state'] = state + return JsonResponse(dict(registration_data)) + #return HttpResponse(cbor.encode(registration_data), content_type = 'application/octet-stream') @csrf_exempt def complete_reg(request): """Completes the registeration, called by API""" try: - if not "fido_state" in request.session: + if not "fido2_state" in request.session: return JsonResponse({'status': 'ERR', "message": "FIDO Status can't be found, please try again"}) - data = cbor.decode(request.body) - - client_data = CollectedClientData(data['clientDataJSON']) - att_obj = AttestationObject((data['attestationObject'])) + enable_json_mapping() + data = simplejson.loads(request.body) server = getServer() - auth_data = server.register_complete( - request.session.pop('fido_state'), - client_data, - att_obj - ) + auth_data = server.register_complete(request.session["fido2_state"], response = data) + registration = RegistrationResponse.from_dict(data) + attestation_object = registration.response.attestation_object + #auth_data = attestation_object.auth_data + att_obj = attestation_object + encoded = websafe_encode(auth_data.credential_data) uk = User_Keys() uk.username = request.user.username @@ -103,10 +109,7 @@ def start(request): def getUserCredentials(username): - credentials = [] - for uk in User_Keys.objects.filter(username = username, key_type = "FIDO2"): - credentials.append(AttestedCredentialData(websafe_decode(uk.properties["device"]))) - return credentials + return [AttestedCredentialData(websafe_decode(uk.properties["device"])) for uk in User_Keys.objects.filter(username = username, key_type = "FIDO2")] def auth(request): @@ -115,6 +118,7 @@ def auth(request): def authenticate_begin(request): + enable_json_mapping() server = getServer() credentials=[] username = None @@ -125,13 +129,14 @@ def authenticate_begin(request): if username: credentials = getUserCredentials(request.session.get("base_username", request.user.username)) auth_data, state = server.authenticate_begin(credentials) - request.session['fido_state'] = state - return HttpResponse(cbor.encode(auth_data), content_type = "application/octet-stream") + request.session['fido2_state'] = state + return JsonResponse(dict(auth_data)) @csrf_exempt def authenticate_complete(request): try: + enable_json_mapping() credentials = [] username = None keys = None @@ -140,26 +145,28 @@ def authenticate_complete(request): if request.user.is_authenticated: username = request.user.username server = getServer() - data = cbor.decode(request.body) - credential_id = data['credentialId'] - if credential_id and username is None: + data = simplejson.loads(request.body) + userHandle = data.get("response",{}).get('userHandle') + credential_id = data['id'] + + if userHandle: + if User_Keys.objects.filter(username=userHandle).exists(): + credentials = getUserCredentials(userHandle) + username=userHandle + else: + keys = User_Keys.objects.filter(user_handle = userHandle) + if keys.exists(): + credentials = [AttestedCredentialData(websafe_decode(keys[0].properties["device"]))] + elif credential_id and username is None: keys = User_Keys.objects.filter(user_handle = credential_id) if keys.exists(): credentials=[AttestedCredentialData(websafe_decode(keys[0].properties["device"]))] else: credentials = getUserCredentials(username) - client_data = CollectedClientData(data['clientDataJSON']) - auth_data = AuthenticatorData(data['authenticatorData']) - signature = data['signature'] try: cred = server.authenticate_complete( - request.session.pop('fido_state'), - credentials, - credential_id, - client_data, - auth_data, - signature + request.session.pop('fido2_state'), credentials = credentials, response = data ) except ValueError: return HttpResponse(simplejson.dumps({'status': "ERR", diff --git a/mfa/static/mfa/js/base64url.js b/mfa/static/mfa/js/base64url.js new file mode 100644 index 0000000..067b7d9 --- /dev/null +++ b/mfa/static/mfa/js/base64url.js @@ -0,0 +1,73 @@ +(function(){ + 'use strict'; + + let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + + // Use a lookup table to find the index. + let lookup = new Uint8Array(256); + for (let i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; + } + + let encode = function(arraybuffer) { + let bytes = new Uint8Array(arraybuffer), + i, len = bytes.length, base64url = ''; + + for (i = 0; i < len; i+=3) { + base64url += chars[bytes[i] >> 2]; + base64url += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64url += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64url += chars[bytes[i + 2] & 63]; + } + + if ((len % 3) === 2) { + base64url = base64url.substring(0, base64url.length - 1); + } else if (len % 3 === 1) { + base64url = base64url.substring(0, base64url.length - 2); + } + + return base64url; + }; + + let decode = function(base64string) { + let bufferLength = base64string.length * 0.75, + len = base64string.length, i, p = 0, + encoded1, encoded2, encoded3, encoded4; + + let bytes = new Uint8Array(bufferLength); + + for (i = 0; i < len; i+=4) { + encoded1 = lookup[base64string.charCodeAt(i)]; + encoded2 = lookup[base64string.charCodeAt(i+1)]; + encoded3 = lookup[base64string.charCodeAt(i+2)]; + encoded4 = lookup[base64string.charCodeAt(i+3)]; + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return bytes.buffer + }; + + let methods = { + 'decode': decode, + 'encode': encode + } + + /** + * Exporting and stuff + */ + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = methods; + + } else { + if (typeof define === 'function' && define.amd) { + define([], function() { + return methods + }); + } else { + window.base64url = methods; + } + } + })(); \ No newline at end of file diff --git a/mfa/static/mfa/js/cbor.js b/mfa/static/mfa/js/cbor.js deleted file mode 100644 index 3e1f300..0000000 --- a/mfa/static/mfa/js/cbor.js +++ /dev/null @@ -1,406 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2014-2016 Patrick Gansterer - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -(function(global, undefined) { "use strict"; -var POW_2_24 = 5.960464477539063e-8, - POW_2_32 = 4294967296, - POW_2_53 = 9007199254740992; - -function encode(value) { - var data = new ArrayBuffer(256); - var dataView = new DataView(data); - var lastLength; - var offset = 0; - - function prepareWrite(length) { - var newByteLength = data.byteLength; - var requiredLength = offset + length; - while (newByteLength < requiredLength) - newByteLength <<= 1; - if (newByteLength !== data.byteLength) { - var oldDataView = dataView; - data = new ArrayBuffer(newByteLength); - dataView = new DataView(data); - var uint32count = (offset + 3) >> 2; - for (var i = 0; i < uint32count; ++i) - dataView.setUint32(i << 2, oldDataView.getUint32(i << 2)); - } - - lastLength = length; - return dataView; - } - function commitWrite() { - offset += lastLength; - } - function writeFloat64(value) { - commitWrite(prepareWrite(8).setFloat64(offset, value)); - } - function writeUint8(value) { - commitWrite(prepareWrite(1).setUint8(offset, value)); - } - function writeUint8Array(value) { - var dataView = prepareWrite(value.length); - for (var i = 0; i < value.length; ++i) - dataView.setUint8(offset + i, value[i]); - commitWrite(); - } - function writeUint16(value) { - commitWrite(prepareWrite(2).setUint16(offset, value)); - } - function writeUint32(value) { - commitWrite(prepareWrite(4).setUint32(offset, value)); - } - function writeUint64(value) { - var low = value % POW_2_32; - var high = (value - low) / POW_2_32; - var dataView = prepareWrite(8); - dataView.setUint32(offset, high); - dataView.setUint32(offset + 4, low); - commitWrite(); - } - function writeTypeAndLength(type, length) { - if (length < 24) { - writeUint8(type << 5 | length); - } else if (length < 0x100) { - writeUint8(type << 5 | 24); - writeUint8(length); - } else if (length < 0x10000) { - writeUint8(type << 5 | 25); - writeUint16(length); - } else if (length < 0x100000000) { - writeUint8(type << 5 | 26); - writeUint32(length); - } else { - writeUint8(type << 5 | 27); - writeUint64(length); - } - } - - function encodeItem(value) { - var i; - - if (value === false) - return writeUint8(0xf4); - if (value === true) - return writeUint8(0xf5); - if (value === null) - return writeUint8(0xf6); - if (value === undefined) - return writeUint8(0xf7); - - switch (typeof value) { - case "number": - if (Math.floor(value) === value) { - if (0 <= value && value <= POW_2_53) - return writeTypeAndLength(0, value); - if (-POW_2_53 <= value && value < 0) - return writeTypeAndLength(1, -(value + 1)); - } - writeUint8(0xfb); - return writeFloat64(value); - - case "string": - var utf8data = []; - for (i = 0; i < value.length; ++i) { - var charCode = value.charCodeAt(i); - if (charCode < 0x80) { - utf8data.push(charCode); - } else if (charCode < 0x800) { - utf8data.push(0xc0 | charCode >> 6); - utf8data.push(0x80 | charCode & 0x3f); - } else if (charCode < 0xd800) { - utf8data.push(0xe0 | charCode >> 12); - utf8data.push(0x80 | (charCode >> 6) & 0x3f); - utf8data.push(0x80 | charCode & 0x3f); - } else { - charCode = (charCode & 0x3ff) << 10; - charCode |= value.charCodeAt(++i) & 0x3ff; - charCode += 0x10000; - - utf8data.push(0xf0 | charCode >> 18); - utf8data.push(0x80 | (charCode >> 12) & 0x3f); - utf8data.push(0x80 | (charCode >> 6) & 0x3f); - utf8data.push(0x80 | charCode & 0x3f); - } - } - - writeTypeAndLength(3, utf8data.length); - return writeUint8Array(utf8data); - - default: - var length; - if (Array.isArray(value)) { - length = value.length; - writeTypeAndLength(4, length); - for (i = 0; i < length; ++i) - encodeItem(value[i]); - } else if (value instanceof Uint8Array) { - writeTypeAndLength(2, value.length); - writeUint8Array(value); - } else { - var keys = Object.keys(value); - length = keys.length; - writeTypeAndLength(5, length); - for (i = 0; i < length; ++i) { - var key = keys[i]; - encodeItem(key); - encodeItem(value[key]); - } - } - } - } - - encodeItem(value); - - if ("slice" in data) - return data.slice(0, offset); - - var ret = new ArrayBuffer(offset); - var retView = new DataView(ret); - for (var i = 0; i < offset; ++i) - retView.setUint8(i, dataView.getUint8(i)); - return ret; -} - -function decode(data, tagger, simpleValue) { - var dataView = new DataView(data); - var offset = 0; - - if (typeof tagger !== "function") - tagger = function(value) { return value; }; - if (typeof simpleValue !== "function") - simpleValue = function() { return undefined; }; - - function commitRead(length, value) { - offset += length; - return value; - } - function readArrayBuffer(length) { - return commitRead(length, new Uint8Array(data, offset, length)); - } - function readFloat16() { - var tempArrayBuffer = new ArrayBuffer(4); - var tempDataView = new DataView(tempArrayBuffer); - var value = readUint16(); - - var sign = value & 0x8000; - var exponent = value & 0x7c00; - var fraction = value & 0x03ff; - - if (exponent === 0x7c00) - exponent = 0xff << 10; - else if (exponent !== 0) - exponent += (127 - 15) << 10; - else if (fraction !== 0) - return (sign ? -1 : 1) * fraction * POW_2_24; - - tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13); - return tempDataView.getFloat32(0); - } - function readFloat32() { - return commitRead(4, dataView.getFloat32(offset)); - } - function readFloat64() { - return commitRead(8, dataView.getFloat64(offset)); - } - function readUint8() { - return commitRead(1, dataView.getUint8(offset)); - } - function readUint16() { - return commitRead(2, dataView.getUint16(offset)); - } - function readUint32() { - return commitRead(4, dataView.getUint32(offset)); - } - function readUint64() { - return readUint32() * POW_2_32 + readUint32(); - } - function readBreak() { - if (dataView.getUint8(offset) !== 0xff) - return false; - offset += 1; - return true; - } - function readLength(additionalInformation) { - if (additionalInformation < 24) - return additionalInformation; - if (additionalInformation === 24) - return readUint8(); - if (additionalInformation === 25) - return readUint16(); - if (additionalInformation === 26) - return readUint32(); - if (additionalInformation === 27) - return readUint64(); - if (additionalInformation === 31) - return -1; - throw "Invalid length encoding"; - } - function readIndefiniteStringLength(majorType) { - var initialByte = readUint8(); - if (initialByte === 0xff) - return -1; - var length = readLength(initialByte & 0x1f); - if (length < 0 || (initialByte >> 5) !== majorType) - throw "Invalid indefinite length element"; - return length; - } - - function appendUtf16Data(utf16data, length) { - for (var i = 0; i < length; ++i) { - var value = readUint8(); - if (value & 0x80) { - if (value < 0xe0) { - value = (value & 0x1f) << 6 - | (readUint8() & 0x3f); - length -= 1; - } else if (value < 0xf0) { - value = (value & 0x0f) << 12 - | (readUint8() & 0x3f) << 6 - | (readUint8() & 0x3f); - length -= 2; - } else { - value = (value & 0x0f) << 18 - | (readUint8() & 0x3f) << 12 - | (readUint8() & 0x3f) << 6 - | (readUint8() & 0x3f); - length -= 3; - } - } - - if (value < 0x10000) { - utf16data.push(value); - } else { - value -= 0x10000; - utf16data.push(0xd800 | (value >> 10)); - utf16data.push(0xdc00 | (value & 0x3ff)); - } - } - } - - function decodeItem() { - var initialByte = readUint8(); - var majorType = initialByte >> 5; - var additionalInformation = initialByte & 0x1f; - var i; - var length; - - if (majorType === 7) { - switch (additionalInformation) { - case 25: - return readFloat16(); - case 26: - return readFloat32(); - case 27: - return readFloat64(); - } - } - - length = readLength(additionalInformation); - if (length < 0 && (majorType < 2 || 6 < majorType)) - throw "Invalid length"; - - switch (majorType) { - case 0: - return length; - case 1: - return -1 - length; - case 2: - if (length < 0) { - var elements = []; - var fullArrayLength = 0; - while ((length = readIndefiniteStringLength(majorType)) >= 0) { - fullArrayLength += length; - elements.push(readArrayBuffer(length)); - } - var fullArray = new Uint8Array(fullArrayLength); - var fullArrayOffset = 0; - for (i = 0; i < elements.length; ++i) { - fullArray.set(elements[i], fullArrayOffset); - fullArrayOffset += elements[i].length; - } - return fullArray; - } - return readArrayBuffer(length); - case 3: - var utf16data = []; - if (length < 0) { - while ((length = readIndefiniteStringLength(majorType)) >= 0) - appendUtf16Data(utf16data, length); - } else - appendUtf16Data(utf16data, length); - return String.fromCharCode.apply(null, utf16data); - case 4: - var retArray; - if (length < 0) { - retArray = []; - while (!readBreak()) - retArray.push(decodeItem()); - } else { - retArray = new Array(length); - for (i = 0; i < length; ++i) - retArray[i] = decodeItem(); - } - return retArray; - case 5: - var retObject = {}; - for (i = 0; i < length || length < 0 && !readBreak(); ++i) { - var key = decodeItem(); - retObject[key] = decodeItem(); - } - return retObject; - case 6: - return tagger(decodeItem(), length); - case 7: - switch (length) { - case 20: - return false; - case 21: - return true; - case 22: - return null; - case 23: - return undefined; - default: - return simpleValue(length); - } - } - } - - var ret = decodeItem(); - if (offset !== data.byteLength) - throw "Remaining bytes"; - return ret; -} - -var obj = { encode: encode, decode: decode }; - -if (typeof define === "function" && define.amd) - define("cbor/cbor", obj); -else if (typeof module !== "undefined" && module.exports) - module.exports = obj; -else if (!global.CBOR) - global.CBOR = obj; - -})(this); diff --git a/mfa/static/mfa/js/helpers.js b/mfa/static/mfa/js/helpers.js new file mode 100644 index 0000000..d6b70ab --- /dev/null +++ b/mfa/static/mfa/js/helpers.js @@ -0,0 +1,25 @@ +var publicKeyCredentialToJSON = (pubKeyCred) => { + if(pubKeyCred instanceof Array) { + let arr = []; + for(let i of pubKeyCred) + arr.push(publicKeyCredentialToJSON(i)); + + return arr + } + + if(pubKeyCred instanceof ArrayBuffer) { + return base64url.encode(pubKeyCred) + } + + if(pubKeyCred instanceof Object) { + let obj = {}; + + for (let key in pubKeyCred) { + obj[key] = publicKeyCredentialToJSON(pubKeyCred[key]) + } + + return obj + } + + return pubKeyCred + } \ No newline at end of file diff --git a/mfa/templates/FIDO2/Add.html b/mfa/templates/FIDO2/Add.html index d401aac..487a2cc 100644 --- a/mfa/templates/FIDO2/Add.html +++ b/mfa/templates/FIDO2/Add.html @@ -1,17 +1,31 @@ {% extends "base.html" %} {% load static %} {% block head %} - + + + +