Browse Source

Merge pull request #159 from developStorm/master

Implement WebAuthn
Son Nguyen Kim 5 years ago
parent
commit
e35fb631cf

+ 1 - 0
app/auth/__init__.py

@@ -11,5 +11,6 @@ from .views import (
     facebook,
     change_email,
     mfa,
+    fido,
     social,
 )

+ 66 - 0
app/auth/templates/auth/fido.html

@@ -0,0 +1,66 @@
+{% extends "single.html" %}
+
+
+{% block title %}
+  Verify Your Security Key
+{% endblock %}
+
+{% block head %}
+  <script src="{{ url_for('static', filename='assets/js/vendors/base64.js') }}"></script>
+  <script src="{{ url_for('static', filename='assets/js/vendors/webauthn.js') }}"></script>
+{% endblock %}
+
+{% block single_content %}
+  <div class="bg-white p-6" style="margin: auto">
+
+    <div class="mb-2">
+      Your account is protected with your security key (WebAuthn). <br><br>
+      Follow your browser's steps to continue the sign-in process.
+    </div>
+
+    <form id="formRegisterKey" method="post">
+      {{ fido_token_form.csrf_token }}
+      {{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
+    </form>
+    <div class="text-center">
+      <button id="btnVerifyKey" class="btn btn-success mt-2">Use your security key</button>
+    </div>
+
+    {% if enable_otp %}
+      <div class="text-center text-muted mb-6" style="margin-top: 1em;">
+        Don't have your key with you? <br> <a href="{{ url_for('auth.mfa') }}">Verify by One-Time Password</a>
+      </div>
+    {% endif %}
+    
+    <script>
+      async function verifyKey () {
+        $("#btnVerifyKey").prop('disabled', true);
+        $("#btnVerifyKey").text('Waiting for Security Key...');
+
+        const credentialRequestOptions = transformCredentialRequestOptions(
+          JSON.parse('{{webauthn_assertion_options|tojson|safe}}')
+        )
+
+        let assertion;
+        try {
+          assertion = await navigator.credentials.get({
+            publicKey: credentialRequestOptions
+          });
+        } catch (err) {
+          toastr.error("An error occurred when we trying to verify your key.");
+          $("#btnVerifyKey").prop('disabled', false);
+          $("#btnVerifyKey").text('Use your security key');
+          return console.error("Error when trying to get credential:", err);
+        }
+
+        const skAssertion = transformAssertionForServer(assertion);
+        $('#sk_assertion').val(JSON.stringify(skAssertion));
+        $('#formRegisterKey').submit();
+      }
+
+      $("#btnVerifyKey").click(verifyKey);
+    </script>
+
+  </div>
+
+{% endblock %}

+ 6 - 0
app/auth/templates/auth/mfa.html

@@ -28,6 +28,12 @@
       <button class="btn btn-success mt-2">Validate</button>
     </form>
 
+    {% if enable_fido %}
+      <div class="text-center text-muted mb-6" style="margin-top: 1em;">
+        Having trouble with your authenticator? <br> <a href="{{ url_for('auth.fido') }}">Verify by your security key</a>
+      </div>
+    {% endif %}
+
   </div>
 
 {% endblock %}

+ 104 - 0
app/auth/views/fido.py

@@ -0,0 +1,104 @@
+import json
+import secrets
+import webauthn
+from app.config import RP_ID, URL
+
+from flask import request, render_template, redirect, url_for, flash, session
+from flask_login import login_user
+from flask_wtf import FlaskForm
+from wtforms import HiddenField, validators
+
+from app.auth.base import auth_bp
+from app.config import MFA_USER_ID
+from app.log import LOG
+from app.models import User
+from app.extensions import db
+
+
+class FidoTokenForm(FlaskForm):
+    sk_assertion = HiddenField("sk_assertion", validators=[validators.DataRequired()])
+
+
+@auth_bp.route("/fido", methods=["GET", "POST"])
+def fido():
+    # passed from login page
+    user_id = session.get(MFA_USER_ID)
+
+    # user access this page directly without passing by login page
+    if not user_id:
+        flash("Unknown error, redirect back to main page", "warning")
+        return redirect(url_for("auth.login"))
+
+    user = User.get(user_id)
+
+    if not (user and (user.fido_enabled())):
+        flash("Only user with security key linked should go to this page", "warning")
+        return redirect(url_for("auth.login"))
+
+    fido_token_form = FidoTokenForm()
+
+    next_url = request.args.get("next")
+
+    webauthn_user = webauthn.WebAuthnUser(
+        user.fido_uuid,
+        user.email,
+        user.name,
+        False,
+        user.fido_credential_id,
+        user.fido_pk,
+        user.fido_sign_count,
+        RP_ID,
+    )
+
+    # Handling POST requests
+    if fido_token_form.validate_on_submit():
+        try:
+            sk_assertion = json.loads(fido_token_form.sk_assertion.data)
+        except Exception as e:
+            flash("Key verification failed. Error: Invalid Payload", "warning")
+            return redirect(url_for("auth.login"))
+
+        challenge = session["fido_challenge"]
+
+        webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
+            webauthn_user, sk_assertion, challenge, URL, uv_required=False
+        )
+
+        try:
+            new_sign_count = webauthn_assertion_response.verify()
+        except Exception as e:
+            LOG.error(f"An error occurred in WebAuthn verification process: {e}")
+            flash("Key verification failed.", "warning")
+        else:
+            user.fido_sign_count = new_sign_count
+            db.session.commit()
+            del session[MFA_USER_ID]
+
+            login_user(user)
+            flash(f"Welcome back {user.name}!", "success")
+
+            # User comes to login page from another page
+            if next_url:
+                LOG.debug("redirect user to %s", next_url)
+                return redirect(next_url)
+            else:
+                LOG.debug("redirect user to dashboard")
+                return redirect(url_for("dashboard.index"))
+                
+    # Prepare information for key registration process
+    session.pop("challenge", None)
+    challenge = secrets.token_urlsafe(32)
+
+    session["fido_challenge"] = challenge.rstrip("=")
+
+    webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
+        webauthn_user, challenge
+    )
+    webauthn_assertion_options = webauthn_assertion_options.assertion_dict
+
+    return render_template(
+        "auth/fido.html",
+        fido_token_form=fido_token_form,
+        webauthn_assertion_options=webauthn_assertion_options,
+        enable_otp=user.enable_otp,
+    )

+ 9 - 1
app/auth/views/login_utils.py

@@ -14,7 +14,15 @@ def after_login(user, next_url):
     If user enables MFA: redirect user to MFA page
     Otherwise redirect to dashboard page if no next_url
     """
-    if user.enable_otp:
+    if user.fido_enabled():
+        # Use the same session for FIDO so that we can easily
+        # switch between these two 2FA option
+        session[MFA_USER_ID] = user.id
+        if next_url:
+            return redirect(url_for("auth.fido", next_url=next_url))
+        else:
+            return redirect(url_for("auth.fido"))
+    elif user.enable_otp:
         session[MFA_USER_ID] = user.id
         if next_url:
             return redirect(url_for("auth.mfa", next_url=next_url))

+ 5 - 1
app/auth/views/mfa.py

@@ -55,4 +55,8 @@ def mfa():
         else:
             flash("Incorrect token", "warning")
 
-    return render_template("auth/mfa.html", otp_token_form=otp_token_form)
+    return render_template(
+        "auth/mfa.html",
+        otp_token_form=otp_token_form,
+        enable_fido=(user.fido_enabled()),
+    )

+ 4 - 0
app/config.py

@@ -4,6 +4,7 @@ import string
 import subprocess
 
 from dotenv import load_dotenv
+from urllib.parse import urlparse
 
 SHA1 = subprocess.getoutput("git rev-parse HEAD")
 ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
@@ -38,6 +39,9 @@ DEBUG = os.environ["DEBUG"] if "DEBUG" in os.environ else False
 URL = os.environ["URL"]
 print(">>> URL:", URL)
 
+# Calculate RP_ID for WebAuthn
+RP_ID = urlparse(URL).hostname
+
 SENTRY_DSN = os.environ.get("SENTRY_DSN")
 
 # can use another sentry project for the front-end to avoid noises

+ 2 - 0
app/dashboard/__init__.py

@@ -11,6 +11,8 @@ from .views import (
     alias_contact_manager,
     mfa_setup,
     mfa_cancel,
+    fido_setup,
+    fido_cancel,
     domain_detail,
     lifetime_licence,
     directory,

+ 27 - 0
app/dashboard/templates/dashboard/fido_cancel.html

@@ -0,0 +1,27 @@
+{% extends 'default.html' %}
+{% set active_page = "setting" %}
+{% block title %}
+  Unlink Security Key
+{% endblock %}
+
+
+{% block default_content %}
+  <div class="bg-white p-6" style="max-width: 60em; margin: auto">
+    <h1 class="h2">Unlink Your Security Key</h1>
+    <p>
+      Please enter the password of your account so that we can ensure it's you.
+    </p>
+
+    <form method="post">
+      {{ password_check_form.csrf_token }}
+
+      <div class="font-weight-bold mt-5">Password</div>
+
+      {{ password_check_form.password(class="form-control", autofocus="true") }}
+      {{ render_field_errors(password_check_form.password) }}
+      <button class="btn btn-lg btn-danger mt-2">Unlink Key</button>
+    </form>
+
+
+  </div>
+{% endblock %}

+ 58 - 0
app/dashboard/templates/dashboard/fido_setup.html

@@ -0,0 +1,58 @@
+{% extends 'default.html' %}
+{% set active_page = "setting" %}
+{% block title %}
+  Security Key Setup
+{% endblock %}
+
+{% block head %}
+  <script src="{{ url_for('static', filename='node_modules/qrious/dist/qrious.min.js') }}"></script>
+  <script src="{{ url_for('static', filename='assets/js/vendors/base64.js') }}"></script>
+  <script src="{{ url_for('static', filename='assets/js/vendors/webauthn.js') }}"></script>
+{% endblock %}
+
+{% block default_content %}
+  <div class="bg-white p-6" style="max-width: 60em; margin: auto">
+    <h1 class="h2 text-center">Register Your Security Key</h1>
+    <p class="text-center">Follow your browser's steps to register your security key with SimpleLogin</p>
+
+    <form id="formRegisterKey" method="post">
+      {{ fido_token_form.csrf_token }}
+      {{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
+    </form>
+    <div class="text-center">
+      <button id="btnRegisterKey" class="btn btn-lg btn-primary mt-2">Register Key</button>
+    </div>
+
+    <script>
+      async function registerKey () {
+        $("#btnRegisterKey").prop('disabled', true);
+        $("#btnRegisterKey").text('Waiting for Security Key...');
+
+        const pkCredentialCreateOptions = transformCredentialCreateOptions(
+          JSON.parse('{{credential_create_options|tojson|safe}}')
+        )
+
+        let credential
+        try {
+          credential = await navigator.credentials.create({
+            publicKey: pkCredentialCreateOptions
+          });
+        } catch (err) {
+          toastr.error("An error occurred when we trying to register your key.");
+          $("#btnRegisterKey").prop('disabled', false);
+          $("#btnRegisterKey").text('Register Key');
+          return console.error("Error when trying to create credential:", err);
+        }
+
+        const skAssertion = transformNewAssertionForServer(credential);
+
+        $('#sk_assertion').val(JSON.stringify(skAssertion));
+        $('#formRegisterKey').submit();
+      }
+
+      $("#btnRegisterKey").click(registerKey);
+      $('document').ready(registerKey());
+    </script>
+
+  </div>
+{% endblock %}

+ 18 - 4
app/dashboard/templates/dashboard/setting.html

@@ -85,18 +85,32 @@
     </div>
     <!-- END change name & profile picture -->
 
+    <div class="card">
+      <div class="card-body">
+        <div class="card-title">Security Key (WebAuthn)</div>
+        <div class="mb-3">
+          You can secure your account by linking either your FIDO-supported physical key such as Yubikey, Google Titan,
+          or a device with appropriate hardware to your account.
+        </div>
+        {% if current_user.fido_uuid is none %}
+          <a href="{{ url_for('dashboard.fido_setup') }}" class="btn btn-outline-primary">Setup WebAuthn</a>
+        {% else %}
+          <a href="{{ url_for('dashboard.fido_cancel') }}" class="btn btn-outline-danger">Disable WebAuthn</a>
+        {% endif %}
+      </div>
+    </div>
 
     <div class="card">
       <div class="card-body">
-        <div class="card-title">Multi-Factor Authentication (MFA)</div>
+        <div class="card-title">One-Time Password (TOTP)</div>
         <div class="mb-3">
-          Secure your account with Multi-Factor Authentication. <br>
+          Secure your account with One-Time Password. <br>
           This requires having applications like Google Authenticator, Authy, MyDigiPassword, etc.
         </div>
         {% if not current_user.enable_otp %}
-          <a href="{{ url_for('dashboard.mfa_setup') }}" class="btn btn-outline-primary">Enable</a>
+          <a href="{{ url_for('dashboard.mfa_setup') }}" class="btn btn-outline-primary">Setup TOTP</a>
         {% else %}
-          <a href="{{ url_for('dashboard.mfa_cancel') }}" class="btn btn-outline-danger">Cancel MFA</a>
+          <a href="{{ url_for('dashboard.mfa_cancel') }}" class="btn btn-outline-danger">Disable TOTP</a>
         {% endif %}
       </div>
     </div>

+ 39 - 0
app/dashboard/views/fido_cancel.py

@@ -0,0 +1,39 @@
+from flask import render_template, flash, redirect, url_for
+from flask_login import login_required, current_user
+from flask_wtf import FlaskForm
+from wtforms import PasswordField, validators
+
+from app.dashboard.base import dashboard_bp
+from app.extensions import db
+
+
+class LoginForm(FlaskForm):
+    password = PasswordField("Password", validators=[validators.DataRequired()])
+
+
+@dashboard_bp.route("/fido_cancel", methods=["GET", "POST"])
+@login_required
+def fido_cancel():
+    if not current_user.fido_enabled():
+        flash("You haven't registed a security key", "warning")
+        return redirect(url_for("dashboard.index"))
+
+    password_check_form = LoginForm()
+
+    if password_check_form.validate_on_submit():
+        password = password_check_form.password.data
+
+        if current_user.check_password(password):
+            current_user.fido_pk = None
+            current_user.fido_uuid = None
+            current_user.fido_sign_count = None
+            current_user.fido_credential_id = None
+            db.session.commit()
+            flash("We've unlinked your security key.", "success")
+            return redirect(url_for("dashboard.index"))
+        else:
+            flash("Incorrect password", "warning")
+
+    return render_template(
+        "dashboard/fido_cancel.html", password_check_form=password_check_form
+    )

+ 95 - 0
app/dashboard/views/fido_setup.py

@@ -0,0 +1,95 @@
+import uuid
+import json
+import secrets
+import webauthn
+from app.config import RP_ID, URL
+from urllib.parse import urlparse
+
+from flask import render_template, flash, redirect, url_for, session
+from flask_login import login_required, current_user
+from flask_wtf import FlaskForm
+from wtforms import HiddenField, validators
+
+from app.dashboard.base import dashboard_bp
+from app.extensions import db
+from app.log import LOG
+
+
+class FidoTokenForm(FlaskForm):
+    sk_assertion = HiddenField("sk_assertion", validators=[validators.DataRequired()])
+
+
+@dashboard_bp.route("/fido_setup", methods=["GET", "POST"])
+@login_required
+def fido_setup():
+    if current_user.fido_enabled():
+        flash("You have already registered your security key", "warning")
+        return redirect(url_for("dashboard.index"))
+
+    fido_token_form = FidoTokenForm()
+
+    # Handling POST requests
+    if fido_token_form.validate_on_submit():
+        try:
+            sk_assertion = json.loads(fido_token_form.sk_assertion.data)
+        except Exception as e:
+            flash("Key registration failed. Error: Invalid Payload", "warning")
+            return redirect(url_for("dashboard.index"))
+
+        fido_uuid = session["fido_uuid"]
+        challenge = session["fido_challenge"]
+
+        fido_reg_response = webauthn.WebAuthnRegistrationResponse(
+            RP_ID,
+            URL,
+            sk_assertion,
+            challenge,
+            trusted_attestation_cert_required=False,
+            none_attestation_permitted=True,
+        )
+
+        try:
+            fido_credential = fido_reg_response.verify()
+        except Exception as e:
+            LOG.error(f"An error occurred in WebAuthn registration process: {e}")
+            flash("Key registration failed.", "warning")
+            return redirect(url_for("dashboard.index"))
+
+        current_user.fido_pk = str(fido_credential.public_key, "utf-8")
+        current_user.fido_uuid = fido_uuid
+        current_user.fido_sign_count = fido_credential.sign_count
+        current_user.fido_credential_id = str(fido_credential.credential_id, "utf-8")
+        db.session.commit()
+
+        flash("Security key has been activated", "success")
+
+        return redirect(url_for("dashboard.index"))
+
+    # Prepare information for key registration process
+    fido_uuid = str(uuid.uuid4())
+    challenge = secrets.token_urlsafe(32)
+
+    credential_create_options = webauthn.WebAuthnMakeCredentialOptions(
+        challenge,
+        "SimpleLogin",
+        RP_ID,
+        fido_uuid,
+        current_user.email,
+        current_user.name,
+        False,
+        attestation="none",
+    )
+
+    # Don't think this one should be used, but it's not configurable by arguments
+    # https://www.w3.org/TR/webauthn/#sctn-location-extension
+    registration_dict = credential_create_options.registration_dict
+    del registration_dict["extensions"]["webauthn.loc"]
+
+    session["fido_uuid"] = fido_uuid
+    session["fido_challenge"] = challenge.rstrip("=")
+
+    return render_template(
+        "dashboard/fido_setup.html",
+        fido_token_form=fido_token_form,
+        credential_create_options=registration_dict,
+    )

+ 11 - 0
app/models.py

@@ -134,6 +134,17 @@ class User(db.Model, ModelMixin, UserMixin):
         db.Boolean, nullable=False, default=False, server_default="0"
     )
 
+    # Fields for WebAuthn
+    fido_uuid = db.Column(db.String(), nullable=True, unique=True)
+    fido_credential_id = db.Column(db.String(), nullable=True, unique=True)
+    fido_pk = db.Column(db.String(), nullable=True, unique=True)
+    fido_sign_count = db.Column(db.Integer(), nullable=True)
+
+    def fido_enabled(self) -> bool:
+        if self.fido_uuid is not None:
+            return True
+        return False
+
     # some users could have lifetime premium
     lifetime = db.Column(db.Boolean, default=False, nullable=False, server_default="0")
 

+ 2 - 1
requirements.in

@@ -37,4 +37,5 @@ flask_profiler
 facebook-sdk
 google-api-python-client
 google-auth-httplib2
-python-gnupg
+python-gnupg
+webauthn

+ 1 - 0
requirements.txt

@@ -80,6 +80,7 @@ pycparser==2.19           # via cffi
 pycryptodome==3.9.4       # via -r requirements.in
 pygments==2.4.2           # via ipython
 pyopenssl==19.0.0         # via -r requirements.in
+webauthn==0.4.7           # via manually
 pyotp==2.3.0              # via -r requirements.in
 pyparsing==2.4.0          # via packaging
 pytest==4.6.3             # via -r requirements.in

+ 122 - 0
static/assets/js/vendors/base64.js

@@ -0,0 +1,122 @@
+// Copyright (c) 2017 Duo Security, Inc. All rights reserved.
+// Under BSD 3-Clause "New" or "Revised" License
+// https://github.com/duo-labs/py_webauthn/
+
+var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+
+;(function (exports) {
+  'use strict'
+
+  var Arr = (typeof Uint8Array !== 'undefined')
+    ? Uint8Array
+    : Array
+
+  var PLUS = '+'.charCodeAt(0)
+  var SLASH = '/'.charCodeAt(0)
+  var NUMBER = '0'.charCodeAt(0)
+  var LOWER = 'a'.charCodeAt(0)
+  var UPPER = 'A'.charCodeAt(0)
+  var PLUS_URL_SAFE = '-'.charCodeAt(0)
+  var SLASH_URL_SAFE = '_'.charCodeAt(0)
+
+  function decode (elt) {
+    var code = elt.charCodeAt(0)
+    if (code === PLUS || code === PLUS_URL_SAFE) return 62 // '+'
+    if (code === SLASH || code === SLASH_URL_SAFE) return 63 // '/'
+    if (code < NUMBER) return -1 // no match
+    if (code < NUMBER + 10) return code - NUMBER + 26 + 26
+    if (code < UPPER + 26) return code - UPPER
+    if (code < LOWER + 26) return code - LOWER + 26
+  }
+
+  function b64ToByteArray (b64) {
+    var i, j, l, tmp, placeHolders, arr
+
+    if (b64.length % 4 > 0) {
+      throw new Error('Invalid string. Length must be a multiple of 4')
+    }
+
+    // the number of equal signs (place holders)
+    // if there are two placeholders, than the two characters before it
+    // represent one byte
+    // if there is only one, then the three characters before it represent 2 bytes
+    // this is just a cheap hack to not do indexOf twice
+    var len = b64.length
+    placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0
+
+    // base64 is 4/3 + up to two characters of the original data
+    arr = new Arr(b64.length * 3 / 4 - placeHolders)
+
+    // if there are placeholders, only get up to the last complete 4 chars
+    l = placeHolders > 0 ? b64.length - 4 : b64.length
+
+    var L = 0
+
+    function push (v) {
+      arr[L++] = v
+    }
+
+    for (i = 0, j = 0; i < l; i += 4, j += 3) {
+      tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
+      push((tmp & 0xFF0000) >> 16)
+      push((tmp & 0xFF00) >> 8)
+      push(tmp & 0xFF)
+    }
+
+    if (placeHolders === 2) {
+      tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
+      push(tmp & 0xFF)
+    } else if (placeHolders === 1) {
+      tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
+      push((tmp >> 8) & 0xFF)
+      push(tmp & 0xFF)
+    }
+
+    return arr
+  }
+
+  function uint8ToBase64 (uint8) {
+    var i
+    var extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
+    var output = ''
+    var temp, length
+
+    function encode (num) {
+      return lookup.charAt(num)
+    }
+
+    function tripletToBase64 (num) {
+      return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
+    }
+
+    // go through the array every three bytes, we'll deal with trailing stuff later
+    for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
+      temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
+      output += tripletToBase64(temp)
+    }
+
+    // pad the end with zeros, but make sure to not forget the extra bytes
+    switch (extraBytes) {
+      case 1:
+        temp = uint8[uint8.length - 1]
+        output += encode(temp >> 2)
+        output += encode((temp << 4) & 0x3F)
+        output += '=='
+        break
+      case 2:
+        temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
+        output += encode(temp >> 10)
+        output += encode((temp >> 4) & 0x3F)
+        output += encode((temp << 2) & 0x3F)
+        output += '='
+        break
+      default:
+        break
+    }
+
+    return output
+  }
+
+  exports.toByteArray = b64ToByteArray
+  exports.fromByteArray = uint8ToBase64
+}(typeof exports === 'undefined' ? (this.base64js = {}) : exports))

+ 131 - 0
static/assets/js/vendors/webauthn.js

@@ -0,0 +1,131 @@
+// Copyright (c) 2017 Duo Security, Inc. All rights reserved.
+// Under BSD 3-Clause "New" or "Revised" License
+// https://github.com/duo-labs/py_webauthn/
+
+function b64enc(buf) {
+  return base64js
+    .fromByteArray(buf)
+    .replace(/\+/g, "-")
+    .replace(/\//g, "_")
+    .replace(/=/g, "");
+}
+
+function b64RawEnc(buf) {
+  return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
+}
+
+function hexEncode(buf) {
+  return Array.from(buf)
+    .map(function (x) {
+      return ("0" + x.toString(16)).substr(-2);
+    })
+    .join("");
+}
+
+const transformCredentialRequestOptions = (
+  credentialRequestOptionsFromServer
+) => {
+  let { challenge, allowCredentials } = credentialRequestOptionsFromServer;
+
+  challenge = Uint8Array.from(
+    atob(challenge.replace(/\_/g, "/").replace(/\-/g, "+")),
+    (c) => c.charCodeAt(0)
+  );
+
+  allowCredentials = allowCredentials.map((credentialDescriptor) => {
+    let { id } = credentialDescriptor;
+    id = id.replace(/\_/g, "/").replace(/\-/g, "+");
+    id = Uint8Array.from(atob(id), (c) => c.charCodeAt(0));
+    return Object.assign({}, credentialDescriptor, { id });
+  });
+
+  const transformedCredentialRequestOptions = Object.assign(
+    {},
+    credentialRequestOptionsFromServer,
+    { challenge, allowCredentials }
+  );
+
+  return transformedCredentialRequestOptions;
+};
+
+/**
+ * Transforms items in the credentialCreateOptions generated on the server
+ * into byte arrays expected by the navigator.credentials.create() call
+ * @param {Object} credentialCreateOptionsFromServer
+ */
+const transformCredentialCreateOptions = (
+  credentialCreateOptionsFromServer
+) => {
+  let { challenge, user } = credentialCreateOptionsFromServer;
+  user.id = Uint8Array.from(
+    atob(
+      credentialCreateOptionsFromServer.user.id
+        .replace(/\_/g, "/")
+        .replace(/\-/g, "+")
+    ),
+    (c) => c.charCodeAt(0)
+  );
+
+  challenge = Uint8Array.from(
+    atob(
+      credentialCreateOptionsFromServer.challenge
+        .replace(/\_/g, "/")
+        .replace(/\-/g, "+")
+    ),
+    (c) => c.charCodeAt(0)
+  );
+
+  const transformedCredentialCreateOptions = Object.assign(
+    {},
+    credentialCreateOptionsFromServer,
+    { challenge, user }
+  );
+
+  return transformedCredentialCreateOptions;
+};
+
+
+/**
+ * Transforms the binary data in the credential into base64 strings
+ * for posting to the server.
+ * @param {PublicKeyCredential} newAssertion
+ */
+const transformNewAssertionForServer = (newAssertion) => {
+  const attObj = new Uint8Array(newAssertion.response.attestationObject);
+  const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
+  const rawId = new Uint8Array(newAssertion.rawId);
+
+  const registrationClientExtensions = newAssertion.getClientExtensionResults();
+
+  return {
+    id: newAssertion.id,
+    rawId: b64enc(rawId),
+    type: newAssertion.type,
+    attObj: b64enc(attObj),
+    clientData: b64enc(clientDataJSON),
+    registrationClientExtensions: JSON.stringify(registrationClientExtensions),
+  };
+};
+
+
+/**
+ * Encodes the binary data in the assertion into strings for posting to the server.
+ * @param {PublicKeyCredential} newAssertion
+ */
+const transformAssertionForServer = (newAssertion) => {
+  const authData = new Uint8Array(newAssertion.response.authenticatorData);
+  const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
+  const rawId = new Uint8Array(newAssertion.rawId);
+  const sig = new Uint8Array(newAssertion.response.signature);
+  const assertionClientExtensions = newAssertion.getClientExtensionResults();
+
+  return {
+    id: newAssertion.id,
+    rawId: b64enc(rawId),
+    type: newAssertion.type,
+    authData: b64RawEnc(authData),
+    clientData: b64RawEnc(clientDataJSON),
+    signature: hexEncode(sig),
+    assertionClientExtensions: JSON.stringify(assertionClientExtensions),
+  };
+};