From 642f3e2e5f1ad32d697c093d42267d1a5738617a Mon Sep 17 00:00:00 2001 From: Julian Rother <julianr@fsmpi.rwth-aachen.de> Date: Fri, 2 Oct 2020 22:21:00 +0200 Subject: [PATCH] added webauthn support Code is mostly based on python-fido2's example code. Note that webauthn requires the website to be delivered via HTTPS. Flask's development server automatically sets up a self-signed ssl cert with the `ssl_context="adhoc"` option. --- README.md | 1 + uffd/mfa/models.py | 24 +- uffd/mfa/templates/auth.html | 68 ++++- uffd/mfa/templates/setup.html | 13 +- uffd/mfa/templates/setup_webauthn.html | 65 ++++ uffd/mfa/views.py | 112 ++++++- uffd/static/cbor.js | 406 +++++++++++++++++++++++++ 7 files changed, 679 insertions(+), 10 deletions(-) create mode 100644 uffd/mfa/templates/setup_webauthn.html create mode 100644 uffd/static/cbor.js diff --git a/README.md b/README.md index 2ca0bf7a..810a27b7 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A web service to manage LDAP users, groups and permissions. - python3-flask - python3-flask-sqlalchemy - python3-qrcode +- python3-fido2 - git (cli utility, musst be in path) ## development diff --git a/uffd/mfa/models.py b/uffd/mfa/models.py index d30338b5..547ae579 100644 --- a/uffd/mfa/models.py +++ b/uffd/mfa/models.py @@ -3,13 +3,16 @@ import datetime import secrets, time, struct, hmac, hashlib, base64, urllib.parse from flask import request, current_app -from sqlalchemy import Column, Integer, Enum, Boolean, String, DateTime +from sqlalchemy import Column, Integer, Enum, Boolean, String, DateTime, Text + +from fido2.ctap2 import AuthenticatorData from uffd.database import db from uffd.user.models import User class MFAType(enum.Enum): TOTP = 1 + WEBAUTHN = 2 class MFAMethod(db.Model): __tablename__ = 'mfa_method' @@ -91,3 +94,22 @@ class TOTPMethod(MFAMethod): return True return False +class WebauthnMethod(MFAMethod): + _cred = Column('webauthn_cred', Text()) + + __mapper_args__ = { + 'polymorphic_identity': MFAType.WEBAUTHN + } + + def __init__(self, user, cred_data, name=None): + super().__init__(user, name) + self.cred_data = cred_data + + @property + def cred_data(self): + return AuthenticatorData(base64.b64decode(self._cred)) + + @cred_data.setter + def cred_data(self, d): + self._cred = base64.b64encode(bytes(d)) + diff --git a/uffd/mfa/templates/auth.html b/uffd/mfa/templates/auth.html index 176bff26..d5d9a652 100644 --- a/uffd/mfa/templates/auth.html +++ b/uffd/mfa/templates/auth.html @@ -1,6 +1,7 @@ {% extends 'base.html' %} {% block body %} + <form action="{{ url_for("mfa.auth_finish", ref=ref) }}" method="POST"> <div class="row mt-2 justify-content-center"> <div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;"> @@ -10,8 +11,20 @@ <div class="col-12"> <h2 class="text-center">Two-Factor Authentication</h2> </div> + {% if webauthn_methods %} + <div class="form-group col-12 d-none webauthn-group"> + <div id="webauthn-alert" class="alert alert-warning d-none" role="alert"> + </div> + <label for="webauthn-btn">FIDO token</label> + <button type="button" id="webauthn-btn" class="btn btn-primary btn-block"> + <span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> + <span id="webauthn-btn-text">Authenticate with FIDO token</span> + </button> + </div> + <div class="text-center text-muted d-none webauthn-group">- or -</div> + {% endif %} <div class="form-group col-12"> - <label for="mfa-code">Two-factor authentication code</label> + <label for="mfa-code">Authentication code</label> <input type="text" class="form-control" id="mfa-code" name="code" required="required" tabindex="1"> </div> <div class="form-group col-12"> @@ -20,4 +33,57 @@ </div> </div> </form> + +{% if webauthn_methods %} +<script src="{{ url_for('static', filename="cbor.js") }}"></script> +<script> +function begin_webauthn() { + $('#webauthn-alert').addClass('d-none'); + $('#webauthn-spinner').removeClass('d-none'); + $('#webauthn-btn-text').text('Fetching credential data'); + $('#webauthn-btn').prop('disabled', true); + fetch({{ url_for('mfa.auth_webauthn_begin')|tojson }}, { + method: 'POST', + }).then(function(response) { + if(response.ok) return response.arrayBuffer(); + throw new Error('No credential available to authenticate!'); + }).then(CBOR.decode).then(function(options) { + $('#webauthn-btn-text').text('Waiting for response from your device'); + return navigator.credentials.get(options); + }).then(function(assertion) { + $('#webauthn-btn-text').text('Verifing response'); + return fetch({{ url_for('mfa.auth_webauthn_begin')|tojson }}, { + method: 'POST', + headers: {'Content-Type': 'application/cbor'}, + body: CBOR.encode({ + "credentialId": new Uint8Array(assertion.rawId), + "authenticatorData": new Uint8Array(assertion.response.authenticatorData), + "clientDataJSON": new Uint8Array(assertion.response.clientDataJSON), + "signature": new Uint8Array(assertion.response.signature) + }) + }) + }).then(function(response) { + if (response.ok) { + $('#webauthn-spinner').addClass('d-none'); + $('#webauthn-btn-text').text('Success, redirecting'); + window.location = {{ (ref or url_for('index'))|tojson }}; + } else { + throw new Error('Response from authenticator rejected'); + } + }, function(reason) { + $('#webauthn-alert').text('Authentication with your FIDO token failed!'); + $('#webauthn-alert').removeClass('d-none'); + $('#webauthn-spinner').addClass('d-none'); + $('#webauthn-btn-text').text('Try FIDO token again'); + $('#webauthn-btn').prop('disabled', false); + }); +} + +$('#webauthn-btn').on('click', begin_webauthn); +if (typeof(PublicKeyCredential) != "undefined") { + $('.webauthn-group').removeClass('d-none'); +} +</script> +{% endif %} + {% endblock %} diff --git a/uffd/mfa/templates/setup.html b/uffd/mfa/templates/setup.html index f7f47241..b09bd47d 100644 --- a/uffd/mfa/templates/setup.html +++ b/uffd/mfa/templates/setup.html @@ -4,9 +4,10 @@ <div class="btn-toolbar"> <a class="btn btn-primary mb-2 ml-auto" href="{{ url_for('mfa.setup_totp') }}">Setup TOTP</a> + <a class="btn btn-primary mb-2 ml-2" href="{{ url_for('mfa.setup_webauthn') }}">Setup FIDO</a> </div> -{% if methods %} +{% if totp_methods or webauthn_methods %} <table class="table"> <thead> <tr> @@ -16,7 +17,7 @@ </tr> </thead> <tbody> - {% for method in methods %} + {% for method in totp_methods %} <tr> <td style="width: 0.5em;"><i class="fas fa-mobile-alt"></i></td> <td>{{ method.name }}</td> @@ -24,6 +25,14 @@ <td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_totp', id=method.id) }}">Delete</a></td> </tr> {% endfor %} + {% for method in webauthn_methods %} + <tr> + <td style="width: 0.5em;"><i class="fab fa-usb"></i></td> + <td>{{ method.name }}</td> + <td>{{ method.created }}</td> + <td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_webauthn', id=method.id) }}">Delete</a></td> + </tr> + {% endfor %} </tbody> </table> {% else %} diff --git a/uffd/mfa/templates/setup_webauthn.html b/uffd/mfa/templates/setup_webauthn.html new file mode 100644 index 00000000..3a143156 --- /dev/null +++ b/uffd/mfa/templates/setup_webauthn.html @@ -0,0 +1,65 @@ +{% extends 'base.html' %} + +{% block body %} + +<div id="register-alert" class="alert alert-warning d-none" role="alert"></div> + +<form action="{{ url_for('mfa.setup_webauthn') }}" method="POST" class="form"> +<div class="form-group"> + <label for="name">Authenticator Name</label> + <input name="name" type="text" id="method-name" class="form-control" required="required"> +</div> +<div class="form-group"> + <button type="submit" id="register-btn" class="btn btn-primary btn-block"> + <span id="register-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> + <span id="register-btn-text">Register token</span> + </button> +</div> +</form> + +<script src="{{ url_for('static', filename="cbor.js") }}"></script> +<script> + +$('form').on('submit', function(e) { + $('#register-alert').addClass('d-none'); + $('#register-spinner').removeClass('d-none'); + $('#register-btn-text').text('Contacting server'); + $('#register-btn').prop('disabled', true); + fetch({{ url_for('mfa.setup_webauthn_begin')|tojson }}, { + method: 'POST', + }).then(function(response) { + if(response.ok) return response.arrayBuffer(); + throw new Error('Error getting registration data!'); + }).then(CBOR.decode).then(function(options) { + $('#register-btn-text').text('Waiting for response from your device'); + return navigator.credentials.create(options); + }).then(function(attestation) { + return fetch({{ url_for('mfa.setup_webauthn_complete')|tojson }}, { + method: 'POST', + headers: {'Content-Type': 'application/cbor'}, + body: CBOR.encode({ + "attestationObject": new Uint8Array(attestation.response.attestationObject), + "clientDataJSON": new Uint8Array(attestation.response.clientDataJSON), + "name": $('#method-name').val() + }) + }); + }).then(function(response) { + if (response.ok) { + $('#register-spinner').addClass('d-none'); + $('#register-btn-text').text('Success'); + window.location = {{ url_for('mfa.setup')|tojson }}; + } else { + throw new Error('Server rejected authenticator response'); + } + }, function(reason) { + $('#register-alert').text('Registration failed!'); + $('#register-alert').removeClass('d-none'); + $('#register-spinner').addClass('d-none'); + $('#register-btn-text').text('Retry registration'); + $('#register-btn').prop('disabled', false); + }); + return false; +}); +</script> + +{% endblock %} diff --git a/uffd/mfa/views.py b/uffd/mfa/views.py index be8febd3..3baebf11 100644 --- a/uffd/mfa/views.py +++ b/uffd/mfa/views.py @@ -1,18 +1,25 @@ from flask import Blueprint, render_template, session, request, redirect, url_for, flash +import urllib.parse + +from fido2.webauthn import PublicKeyCredentialRpEntity, UserVerificationRequirement +from fido2.client import ClientData +from fido2.server import Fido2Server +from fido2.ctap2 import AttestationObject, AuthenticatorData +from fido2 import cbor from uffd.database import db -from uffd.mfa.models import TOTPMethod +from uffd.mfa.models import TOTPMethod, WebauthnMethod from uffd.session.views import get_current_user, login_required bp = Blueprint('mfa', __name__, template_folder='templates', url_prefix='/mfa/') - @bp.route('/', methods=['GET']) @login_required() def setup(): user = get_current_user() - methods = TOTPMethod.query.filter_by(dn=user.dn).all() - return render_template('setup.html', methods=methods) + totp_methods = TOTPMethod.query.filter_by(dn=user.dn).all() + webauthn_methods = WebauthnMethod.query.filter_by(dn=user.dn).all() + return render_template('setup.html', totp_methods=totp_methods, webauthn_methods=webauthn_methods) @bp.route('/setup/totp', methods=['GET']) @login_required() @@ -44,12 +51,105 @@ def delete_totp(id): db.session.commit() return redirect(url_for('mfa.setup')) +@bp.route('/setup/webauthn', methods=['GET']) +@login_required() +def setup_webauthn(): + user = get_current_user() + return render_template('setup_webauthn.html') + +def get_webauthn_server(): + return Fido2Server(PublicKeyCredentialRpEntity(urllib.parse.urlsplit(request.url).hostname, "uffd")) + +@bp.route('/setup/webauthn/begin', methods=['POST']) +@login_required() +def setup_webauthn_begin(): + user = get_current_user() + server = get_webauthn_server() + registration_data, state = server.register_begin( + { + "id": user.loginname.encode(), + "name": user.loginname, + "displayName": user.displayname, + "icon": "https://example.com/image.png", + }, + [], + user_verification=UserVerificationRequirement.DISCOURAGED, + authenticator_attachment="cross-platform", + ) + session["state"] = state + return cbor.encode(registration_data) + +@bp.route('/setup/webauthn/complete', methods=['POST']) +@login_required() +def setup_webauthn_complete(): + user = get_current_user() + server = get_webauthn_server() + data = cbor.decode(request.get_data()) + client_data = ClientData(data["clientDataJSON"]) + att_obj = AttestationObject(data["attestationObject"]) + auth_data = server.register_complete(session["state"], client_data, att_obj) + method = WebauthnMethod(user, auth_data, name=data['name']) + db.session.add(method) + db.session.commit() + print("REGISTERED CREDENTIAL:", auth_data.credential_data) + return cbor.encode({"status": "OK"}) + +@bp.route('/setup/webauthn/<int:id>/delete') +@login_required() +def delete_webauthn(id): + user = get_current_user() + method = WebauthnMethod.query.filter_by(dn=user.dn, id=id).first_or_404() + db.session.delete(method) + db.session.commit() + return redirect(url_for('mfa.setup')) + +@bp.route("/auth/webauthn/begin", methods=["POST"]) +def auth_webauthn_begin(): + user = get_current_user() + server = get_webauthn_server() + methods = WebauthnMethod.query.filter_by(dn=user.dn).all() + creds = [method.cred_data.credential_data for method in methods] + print(creds) + if not creds: + abort(404) + auth_data, state = server.authenticate_begin(creds, user_verification=UserVerificationRequirement.DISCOURAGED) + session["state"] = state + return cbor.encode(auth_data) + +@bp.route("/auth/webauthn/complete", methods=["POST"]) +def auth_webauthn_complete(): + user = get_current_user() + server = get_webauthn_server() + methods = WebauthnMethod.query.filter_by(dn=user.dn).all() + creds = [method.cred_data.credential_data for method in methods] + if not creds: + abort(404) + data = cbor.decode(request.get_data()) + credential_id = data["credentialId"] + client_data = ClientData(data["clientDataJSON"]) + auth_data = AuthenticatorData(data["authenticatorData"]) + signature = data["signature"] + print("clientData", client_data) + print("AuthenticatorData", auth_data) + server.authenticate_complete( + session.pop("state"), + creds, + credential_id, + client_data, + auth_data, + signature, + ) + print("ASSERTION OK") + return cbor.encode({"status": "OK"}) + @bp.route('/auth', methods=['GET']) @login_required() def auth(): user = get_current_user() - methods = TOTPMethod.query.filter_by(dn=user.dn).all() - return render_template('auth.html', ref=request.values.get('ref'), methods=methods) + totp_methods = TOTPMethod.query.filter_by(dn=user.dn).all() + webauthn_methods = WebauthnMethod.query.filter_by(dn=user.dn).all() + return render_template('auth.html', ref=request.values.get('ref'), totp_methods=totp_methods, + webauthn_methods=webauthn_methods) @bp.route('/auth', methods=['POST']) @login_required() diff --git a/uffd/static/cbor.js b/uffd/static/cbor.js new file mode 100644 index 00000000..3e1f300d --- /dev/null +++ b/uffd/static/cbor.js @@ -0,0 +1,406 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Patrick Gansterer <paroga@paroga.com> + * + * 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); -- GitLab