diff --git a/README.md b/README.md
index 2ca0bf7a23e587c0e638e8247b877255a6597d44..810a27b72a7ee9c5975cf87f9813aff322419d6d 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 d30338b513da2f524d3af099d50fae618ea9de2b..547ae579be477d69ca40a37178b59d6d7d6b20ab 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 176bff26243c272d60e065ed3f1690a80d158638..d5d9a652804a2eeef0c77db6b97c1edc6e6baf79 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 f7f47241827d83b270cefb205a1c691d648758e9..b09bd47d8312be3e637d88fe4c095b5145e136cf 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 0000000000000000000000000000000000000000..3a14315602e7572360d2b7e1f44be2cb48bd202e
--- /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 be8febd3423686c49393396a1183a8c7ef808d9a..3baebf11eb8e80b676306697a4e5ec7531675775 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 0000000000000000000000000000000000000000..3e1f300df35185ec0e7ab0ea65ed998d32970809
--- /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);