diff options
| author | Jérémy Zurcher <jeremy@asynk.ch> | 2017-03-30 09:16:28 +0200 | 
|---|---|---|
| committer | Jérémy Zurcher <jeremy@asynk.ch> | 2017-03-30 09:16:28 +0200 | 
| commit | ac2a06489202c6f1de3bc43078059e608ce87e20 (patch) | |
| tree | 432272cac5783c62715d83b928f4e22e9afe0990 | |
| parent | b611f66803dee20970dc2f386783f507a411465a (diff) | |
| download | share-ac2a06489202c6f1de3bc43078059e608ce87e20.zip share-ac2a06489202c6f1de3bc43078059e608ce87e20.tar.gz | |
java : vaadin : add U2F support ;)
19 files changed, 1150 insertions, 9 deletions
| diff --git a/java/vaadin/build.xml b/java/vaadin/build.xml index 1090e68..712034f 100644 --- a/java/vaadin/build.xml +++ b/java/vaadin/build.xml @@ -2,7 +2,7 @@  <project xmlns:ivy="antlib:org.apache.ivy.ant"      name="My Vaadin Hello World"      basedir="." -    default="war"> +    default="compile.all">      <target name="configure">          <property file="${basedir}/src/main/resources/Application.properties"/> @@ -114,6 +114,11 @@              <compilerarg value="-Xmaxerrs"/>              <compilerarg value="10"/>          </javac> +        <copy todir="${classes.dir}"> +            <fileset dir="${src.dir}"> +                <include name="**/*.js"/> +            </fileset> +        </copy>      </target>      <target name="compile.all" description="compile everything" depends="compile.app,compile.themes"/> diff --git a/java/vaadin/ivy.xml b/java/vaadin/ivy.xml index b1a4b3b..81ca70d 100644 --- a/java/vaadin/ivy.xml +++ b/java/vaadin/ivy.xml @@ -22,5 +22,6 @@          <dependency org="org.codehaus.janino" name="janino" rev="3.0.6"/>          <dependency org="com.yahoo.platform.yui" name="yuicompressor" rev="2.4.8" conf="themes->default"/>          <dependency org="org.eclipse.jetty" name="jetty-webapp" rev="9.4.2.v20170220" conf="run->default"/> +        <dependency org="com.yubico" name="u2flib-server-core" rev="0.16.0"/>      </dependencies>  </ivy-module> diff --git a/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_down.png b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_down.pngBinary files differ new file mode 100644 index 0000000..7e1709f --- /dev/null +++ b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_down.png diff --git a/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_up.png b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_up.pngBinary files differ new file mode 100644 index 0000000..cc442d5 --- /dev/null +++ b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_up.png diff --git a/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-128.png b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-128.pngBinary files differ new file mode 100644 index 0000000..52d0761 --- /dev/null +++ b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-128.png diff --git a/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-24.png b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-24.pngBinary files differ new file mode 100644 index 0000000..bd5594a --- /dev/null +++ b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-24.png diff --git a/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-32.png b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-32.pngBinary files differ new file mode 100644 index 0000000..78a15c4 --- /dev/null +++ b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-32.png diff --git a/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-64.png b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-64.pngBinary files differ new file mode 100644 index 0000000..c89c931 --- /dev/null +++ b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-64.png diff --git a/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-lock.png b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-lock.pngBinary files differ new file mode 100644 index 0000000..7823ff2 --- /dev/null +++ b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-lock.png diff --git a/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo-48.png b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo-48.pngBinary files differ new file mode 100644 index 0000000..3641de1 --- /dev/null +++ b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo-48.png diff --git a/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo.png b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo.pngBinary files differ new file mode 100644 index 0000000..a18234b --- /dev/null +++ b/java/vaadin/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo.png diff --git a/java/vaadin/src/main/java/ch/asynk/security/U2fConnector.java b/java/vaadin/src/main/java/ch/asynk/security/U2fConnector.java new file mode 100644 index 0000000..24b406d --- /dev/null +++ b/java/vaadin/src/main/java/ch/asynk/security/U2fConnector.java @@ -0,0 +1,164 @@ +package ch.asynk.security; + +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.annotations.JavaScript; +import com.vaadin.server.AbstractJavaScriptExtension; +import com.vaadin.ui.UI; +import com.vaadin.ui.JavaScriptFunction; +import com.yubico.u2f.U2F; +import com.yubico.u2f.data.DeviceRegistration; +import com.yubico.u2f.data.messages.AuthenticateRequestData; +import com.yubico.u2f.data.messages.AuthenticateResponse; +import com.yubico.u2f.data.messages.RegisterRequestData; +import com.yubico.u2f.data.messages.RegisterResponse; +import com.yubico.u2f.exceptions.DeviceCompromisedException; +import com.yubico.u2f.exceptions.NoEligibleDevicesException; +import elemental.json.JsonArray; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@JavaScript({"u2f-api.js", "u2f_connector.js"}) +public class U2fConnector extends AbstractJavaScriptExtension +{ +    private static final long serialVersionUID = 1L; + +    private static final Logger logger = LoggerFactory.getLogger(U2fConnector.class); + +    // appId MUST match the URL  - https://developers.yubico.com/U2F/App_ID.html +    private final String appId = "https://localhost:8666"; + +    private final U2F u2f = new U2F(); +    // private final U2F u2f = U2F.withoutAppIdValidation(); + +    // https://developers.yubico.com/U2F/Libraries/Client_error_codes.html +    private static String U2F_ERRORS[] = {"","OTHER_ERROR","BAD_REQUEST","CONFIGURATION_UNSUPPORTED","DEVICE_INELIGIBLE","TIMEOUT","DEVICE_COMPROMISED"}; +    public String getErrorMsg(int i) { return U2F_ERRORS[i]; } + +    private final U2fService u2fService = new U2fServiceImpl(); + +    class Request +    { +        String userId; +        String data; +        U2fListener listener; +        public Request(String userId, String data, U2fListener listener) +        { +            this.userId = userId; +            this.data = data; +            this.listener = listener; +        } +    } + +    private final Map<String, Request> requests = new HashMap<>(); + +    public enum U2fAction { +        REGISTRATION_PENDING, +        REGISTRATION_SUCCESS, +        REGISTRATION_FAILURE, +        AUTHENTICATION_PENDING, +        AUTHENTICATION_FAILURE, +        AUTHENTICATION_SUCCESS +    }; + +    public interface U2fListener +    { +        public void u2fCallback(U2fAction action, String msg); +    } + +    public U2fConnector() { +        extend(UI.getCurrent()); +        addFunction("onRegisterResponse", new JavaScriptFunction() { +            private static final long serialVersionUID = 1L; +            @Override +            public void call(final JsonArray arguments) { +                onRegisterResponse(arguments); +            } +        }); +        addFunction("onAuthenticateResponse", new JavaScriptFunction() { +            private static final long serialVersionUID = 1L; +            @Override +            public void call(final JsonArray arguments) { +                onAuthenticateResponse(arguments); +            } +        }); +    } + +    public void sendRegisterRequest(final String userId, final U2fListener listener) +    { +        final RegisterRequestData registerRequestData = u2f.startRegistration(appId, u2fService.getDeviceRegistrations(userId)); +        final String registerRequestDataJson = registerRequestData.toJson(); +        final String registerRequestDataId = registerRequestData.getRequestId(); +        requests.put(registerRequestDataId, new Request(userId, registerRequestDataJson, listener)); +        callFunction("register", registerRequestDataJson); +        listener.u2fCallback(U2fAction.REGISTRATION_PENDING, null); +        logger.debug(String.format("register[%s] : %s", userId, registerRequestDataJson)); +    } + +    public void onRegisterResponse(JsonArray arguments) +    { +        if (arguments.get(0) instanceof elemental.json.impl.JreJsonNumber) { +            int errorCode = (int) arguments.getNumber(0); +            // FIXME why does it not work ?? +            // final RegisterRequestData registerRequestData = RegisterRequestData.fromJson(arguments.getString(1)); +            final String registerRequestDataJson = arguments.getObject(1).toString(); +            final RegisterRequestData registerRequestData = RegisterRequestData.fromJson(registerRequestDataJson); +            final Request request = requests.remove(registerRequestData.getRequestId()); +            request.listener.u2fCallback(U2fAction.REGISTRATION_FAILURE, getErrorMsg(errorCode)); +            logger.debug(String.format("failure[%d] : %s", errorCode, registerRequestDataJson)); +        } else { +            final String registerResponseJson = arguments.getString(0); +            final RegisterResponse registerResponse = RegisterResponse.fromJson(registerResponseJson); +            final Request request = requests.remove(registerResponse.getRequestId()); +            final RegisterRequestData registerRequestData = RegisterRequestData.fromJson(request.data); +            u2fService.addDeviceRegistration(request.userId, u2f.finishRegistration(registerRequestData, registerResponse)); +            request.listener.u2fCallback(U2fAction.REGISTRATION_SUCCESS, null); +            logger.debug(String.format("success : %s", registerResponseJson)); +        } +    } + +    public void startAuthentication(final String userId, final U2fListener listener) +    { +        try { +            final AuthenticateRequestData authenticateRequestData = u2f.startAuthentication(appId, u2fService.getDeviceRegistrations(userId)); +            final String authenticateRequestDataJson = authenticateRequestData.toJson(); +            final String authenticateRequestDataId = authenticateRequestData.getRequestId(); +            requests.put(authenticateRequestDataId, new Request(userId, authenticateRequestDataJson, listener)); +            callFunction("authenticate", authenticateRequestDataJson, userId); +            listener.u2fCallback(U2fAction.AUTHENTICATION_PENDING, null); +            logger.debug(String.format("authenticate[%s] : %s", userId, authenticateRequestDataJson)); +        } catch (NoEligibleDevicesException e) { +            listener.u2fCallback(U2fAction.AUTHENTICATION_FAILURE, getErrorMsg(4)); +        } +    } + +    public void onAuthenticateResponse(JsonArray arguments) +    { +        if (arguments.get(0) instanceof elemental.json.impl.JreJsonNumber) { +            int errorCode = (int) arguments.getNumber(0); +            // FIXME why does it not work ?? +            // final AuthenticateRequestData authenticateRequestData = AuthenticateRequestData.fromJson(arguments.getString(1)); +            final String authenticateRequestDataJson = arguments.getObject(1).toString(); +            final AuthenticateRequestData authenticateRequestData = AuthenticateRequestData.fromJson(authenticateRequestDataJson); +            final Request request = requests.remove(authenticateRequestData.getRequestId()); +            request.listener.u2fCallback(U2fAction.AUTHENTICATION_FAILURE, getErrorMsg(errorCode)); +            logger.debug(String.format("failure[%d] : %s", errorCode, authenticateRequestDataJson)); +        } else { +            final String authenticateResponseJson = arguments.getString(0); +            final AuthenticateResponse authenticateResponse = AuthenticateResponse.fromJson(authenticateResponseJson); +            final Request request = requests.remove(authenticateResponse.getRequestId()); +            final AuthenticateRequestData authenticateRequestData = AuthenticateRequestData.fromJson(request.data); +            DeviceRegistration registration = null; +            try { +                registration = u2f.finishAuthentication(authenticateRequestData, authenticateResponse, u2fService.getDeviceRegistrations(request.userId)); +            } catch (final DeviceCompromisedException e) { +                request.listener.u2fCallback(U2fAction.AUTHENTICATION_FAILURE, getErrorMsg(6)); +                logger.error(String.format("device compromised: %s", request.data)); +            } +            request.listener.u2fCallback(U2fAction.AUTHENTICATION_SUCCESS, null); +            logger.debug(String.format("success : %s", authenticateResponseJson)); +        } +    } +} diff --git a/java/vaadin/src/main/java/ch/asynk/security/U2fService.java b/java/vaadin/src/main/java/ch/asynk/security/U2fService.java new file mode 100644 index 0000000..9bce94e --- /dev/null +++ b/java/vaadin/src/main/java/ch/asynk/security/U2fService.java @@ -0,0 +1,12 @@ +package ch.asynk.security; + +import java.util.List; + +import com.yubico.u2f.data.DeviceRegistration; + +public interface U2fService +{ +    public boolean hasDeviceRegistrations(final String userId); +    public List<DeviceRegistration> getDeviceRegistrations(final String userId); +    public void addDeviceRegistration(final String userId, final DeviceRegistration deviceRegistration); +} diff --git a/java/vaadin/src/main/java/ch/asynk/security/U2fServiceImpl.java b/java/vaadin/src/main/java/ch/asynk/security/U2fServiceImpl.java new file mode 100644 index 0000000..f8e785f --- /dev/null +++ b/java/vaadin/src/main/java/ch/asynk/security/U2fServiceImpl.java @@ -0,0 +1,43 @@ +package ch.asynk.security; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.yubico.u2f.data.DeviceRegistration; + +public class U2fServiceImpl implements U2fService +{ +    private HashMap<String, List<DeviceRegistration>> store; + +    public U2fServiceImpl() +    { +        store = new HashMap<String, List<DeviceRegistration>>(); +    } + +    public boolean hasDeviceRegistrations(final String userId) +    { +        return store.containsKey(userId); +    } + +    public List<DeviceRegistration> getDeviceRegistrations(final String userId) +    { +        return get(userId); +    } + +    public void addDeviceRegistration(final String userId, final DeviceRegistration deviceRegistration) +    { +        List<DeviceRegistration> registrations = get(userId); +        registrations.add(deviceRegistration); +        store.put(userId, registrations); +    } + +    private List<DeviceRegistration> get(final String userId) +    { +        List<DeviceRegistration> registrations = store.get(userId); +        if (registrations == null) +            registrations = new ArrayList<DeviceRegistration>(); +        return registrations; +    } +} diff --git a/java/vaadin/src/main/java/ch/asynk/security/u2f-api.js b/java/vaadin/src/main/java/ch/asynk/security/u2f-api.js new file mode 100644 index 0000000..9244d14 --- /dev/null +++ b/java/vaadin/src/main/java/ch/asynk/security/u2f-api.js @@ -0,0 +1,748 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. + u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { +    'U2F_REGISTER_REQUEST': 'u2f_register_request', +    'U2F_REGISTER_RESPONSE': 'u2f_register_response', +    'U2F_SIGN_REQUEST': 'u2f_sign_request', +    'U2F_SIGN_RESPONSE': 'u2f_sign_response', +    'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', +    'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { +    'OK': 0, +    'OTHER_ERROR': 1, +    'BAD_REQUEST': 2, +    'CONFIGURATION_UNSUPPORTED': 3, +    'DEVICE_INELIGIBLE': 4, +    'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + *   type: u2f.MessageTypes, + *   appId: ?string, + *   timeoutSeconds: ?number, + *   requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + *   type: u2f.MessageTypes, + *   responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + *   requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + *   errorCode: u2f.ErrorCodes, + *   errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array<u2f.Transport>} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + *   version: string, + *   challenge: string, + *   keyHandle: string, + *   appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + *   keyHandle: string, + *   signatureData: string, + *   clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + *   version: string, + *   challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + *   version: string, + *   keyHandle: string, + *   transports: Transports, + *   appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + *   version: string, + *   keyHandle: string, + *   transports: ?Transports, + *   appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + *   js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { +  if (typeof chrome != 'undefined' && chrome.runtime) { +    // The actual message here does not matter, but we need to get a reply +    // for the callback to run. Thus, send an empty signature request +    // in order to get a failure response. +    var msg = { +        type: u2f.MessageTypes.U2F_SIGN_REQUEST, +        signRequests: [] +    }; +    chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { +      if (!chrome.runtime.lastError) { +        // We are on a whitelisted origin and can talk directly +        // with the extension. +        u2f.getChromeRuntimePort_(callback); +      } else { +        // chrome.runtime was available, but we couldn't message +        // the extension directly, use iframe +        u2f.getIframePort_(callback); +      } +    }); +  } else if (u2f.isAndroidChrome_()) { +    u2f.getAuthenticatorPort_(callback); +  } else if (u2f.isIosChrome_()) { +    u2f.getIosPort_(callback); +  } else { +    // chrome.runtime was not available at all, which is normal +    // when this origin doesn't have access to any extensions. +    u2f.getIframePort_(callback); +  } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { +  var userAgent = navigator.userAgent; +  return userAgent.indexOf('Chrome') != -1 && +  userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { +  return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { +  var port = chrome.runtime.connect(u2f.EXTENSION_ID, +      {'includeTlsChannelId': true}); +  setTimeout(function() { +    callback(new u2f.WrappedChromeRuntimePort_(port)); +  }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { +  setTimeout(function() { +    callback(new u2f.WrappedAuthenticatorPort_()); +  }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { +  setTimeout(function() { +    callback(new u2f.WrappedIosPort_()); +  }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { +  this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array<u2f.SignRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = +  function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { +  if (js_api_version === undefined || js_api_version < 1.1) { +    // Adapt request to the 1.0 JS API +    var signRequests = []; +    for (var i = 0; i < registeredKeys.length; i++) { +      signRequests[i] = { +          version: registeredKeys[i].version, +          challenge: challenge, +          keyHandle: registeredKeys[i].keyHandle, +          appId: appId +      }; +    } +    return { +      type: u2f.MessageTypes.U2F_SIGN_REQUEST, +      signRequests: signRequests, +      timeoutSeconds: timeoutSeconds, +      requestId: reqId +    }; +  } +  // JS 1.1 API +  return { +    type: u2f.MessageTypes.U2F_SIGN_REQUEST, +    appId: appId, +    challenge: challenge, +    registeredKeys: registeredKeys, +    timeoutSeconds: timeoutSeconds, +    requestId: reqId +  }; +}; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array<u2f.SignRequest>} signRequests + * @param {Array<u2f.RegisterRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = +  function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { +  if (js_api_version === undefined || js_api_version < 1.1) { +    // Adapt request to the 1.0 JS API +    for (var i = 0; i < registerRequests.length; i++) { +      registerRequests[i].appId = appId; +    } +    var signRequests = []; +    for (var i = 0; i < registeredKeys.length; i++) { +      signRequests[i] = { +          version: registeredKeys[i].version, +          challenge: registerRequests[0], +          keyHandle: registeredKeys[i].keyHandle, +          appId: appId +      }; +    } +    return { +      type: u2f.MessageTypes.U2F_REGISTER_REQUEST, +      signRequests: signRequests, +      registerRequests: registerRequests, +      timeoutSeconds: timeoutSeconds, +      requestId: reqId +    }; +  } +  // JS 1.1 API +  return { +    type: u2f.MessageTypes.U2F_REGISTER_REQUEST, +    appId: appId, +    registerRequests: registerRequests, +    registeredKeys: registeredKeys, +    timeoutSeconds: timeoutSeconds, +    requestId: reqId +  }; +}; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { +  this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = +    function(eventName, handler) { +  var name = eventName.toLowerCase(); +  if (name == 'message' || name == 'onmessage') { +    this.port_.onMessage.addListener(function(message) { +      // Emulate a minimal MessageEvent object +      handler({'data': message}); +    }); +  } else { +    console.error('WrappedChromeRuntimePort only supports onMessage'); +  } +}; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { +  this.requestId_ = -1; +  this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { +  var intentUrl = +    u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + +    ';S.request=' + encodeURIComponent(JSON.stringify(message)) + +    ';end'; +  document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { +  return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { +  var name = eventName.toLowerCase(); +  if (name == 'message') { +    var self = this; +    /* Register a callback to that executes when +     * chrome injects the response. */ +    window.addEventListener( +        'message', self.onRequestUpdate_.bind(self, handler), false); +  } else { +    console.error('WrappedAuthenticatorPort only supports message'); +  } +}; + +/** + * Callback invoked  when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = +    function(callback, message) { +  var messageObject = JSON.parse(message.data); +  var intentUrl = messageObject['intentURL']; + +  var errorCode = messageObject['errorCode']; +  var responseObject = null; +  if (messageObject.hasOwnProperty('data')) { +    responseObject = /** @type {Object} */ ( +        JSON.parse(messageObject['data'])); +  } + +  callback({'data': responseObject}); +}; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = +  'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { +  var str = JSON.stringify(message); +  var url = "u2f://auth?" + encodeURI(str); +  location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { +  return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { +  var name = eventName.toLowerCase(); +  if (name !== 'message') { +    console.error('WrappedIosPort only supports message'); +  } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { +  // Create the iframe +  var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; +  var iframe = document.createElement('iframe'); +  iframe.src = iframeOrigin + '/u2f-comms.html'; +  iframe.setAttribute('style', 'display:none'); +  document.body.appendChild(iframe); + +  var channel = new MessageChannel(); +  var ready = function(message) { +    if (message.data == 'ready') { +      channel.port1.removeEventListener('message', ready); +      callback(channel.port1); +    } else { +      console.error('First event on iframe port was not "ready"'); +    } +  }; +  channel.port1.addEventListener('message', ready); +  channel.port1.start(); + +  iframe.addEventListener('load', function() { +    // Deliver the port to the iframe and initialize +    iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); +  }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse)) + *                       |function((u2f.Error|u2f.SignResponse)))>} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { +  if (u2f.port_) { +    callback(u2f.port_); +  } else { +    if (u2f.waitingForPort_.length == 0) { +      u2f.getMessagePort(function(port) { +        u2f.port_ = port; +        u2f.port_.addEventListener('message', +            /** @type {function(Event)} */ (u2f.responseHandler_)); + +        // Careful, here be async callbacks. Maybe. +        while (u2f.waitingForPort_.length) +          u2f.waitingForPort_.shift()(u2f.port_); +      }); +    } +    u2f.waitingForPort_.push(callback); +  } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.<u2f.Response>} message + * @private + */ +u2f.responseHandler_ = function(message) { +  var response = message.data; +  var reqId = response['requestId']; +  if (!reqId || !u2f.callbackMap_[reqId]) { +    console.error('Unknown or missing requestId in response.'); +    return; +  } +  var cb = u2f.callbackMap_[reqId]; +  delete u2f.callbackMap_[reqId]; +  cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { +  if (js_api_version === undefined) { +    // Send a message to get the extension to JS API version, then send the actual sign request. +    u2f.getApiVersion( +        function (response) { +          js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; +          console.log("Extension JS API Version: ", js_api_version); +          u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); +        }); +  } else { +    // We know the JS API version. Send the actual sign request in the supported API version. +    u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); +  } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { +  u2f.getPortSingleton_(function(port) { +    var reqId = ++u2f.reqCounter_; +    u2f.callbackMap_[reqId] = callback; +    var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? +        opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); +    var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); +    port.postMessage(req); +  }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { +  if (js_api_version === undefined) { +    // Send a message to get the extension to JS API version, then send the actual register request. +    u2f.getApiVersion( +        function (response) { +          js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; +          console.log("Extension JS API Version: ", js_api_version); +          u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, +              callback, opt_timeoutSeconds); +        }); +  } else { +    // We know the JS API version. Send the actual register request in the supported API version. +    u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, +        callback, opt_timeoutSeconds); +  } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { +  u2f.getPortSingleton_(function(port) { +    var reqId = ++u2f.reqCounter_; +    u2f.callbackMap_[reqId] = callback; +    var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? +        opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); +    var req = u2f.formatRegisterRequest_( +        appId, registeredKeys, registerRequests, timeoutSeconds, reqId); +    port.postMessage(req); +  }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { +   // If we are using Android Google Authenticator or iOS client app, +   // do not fire an intent to ask which JS API version to use. +   if (port.getPortType) { +     var apiVersion; +     switch (port.getPortType()) { +       case 'WrappedIosPort_': +       case 'WrappedAuthenticatorPort_': +         apiVersion = 1.1; +         break; + +       default: +         apiVersion = 0; +         break; +     } +     callback({ 'js_api_version': apiVersion }); +     return; +   } +    var reqId = ++u2f.reqCounter_; +    u2f.callbackMap_[reqId] = callback; +    var req = { +      type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, +      timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? +          opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), +      requestId: reqId +    }; +    port.postMessage(req); +  }); +}; diff --git a/java/vaadin/src/main/java/ch/asynk/security/u2f_connector.js b/java/vaadin/src/main/java/ch/asynk/security/u2f_connector.js new file mode 100644 index 0000000..8723cb3 --- /dev/null +++ b/java/vaadin/src/main/java/ch/asynk/security/u2f_connector.js @@ -0,0 +1,30 @@ +window.ch_asynk_security_U2fConnector = function() { + +    var connector = this; + +    this.register = function(requestJson) { +        var request = JSON.parse(requestJson); +        // alert(JSON.stringify(request)); +        setTimeout(function() { +            u2f.register(request.registerRequests, request.authenticateRequests, function(data) { +                if(data.errorCode) +                    connector.onRegisterResponse(data.errorCode, request); +                else +                    connector.onRegisterResponse(JSON.stringify(data)); +            }); +        }, 500); +    } + +    this.authenticate = function(requestJson) { +        var request = JSON.parse(requestJson); +        //alert(JSON.stringify(request)); +        setTimeout(function() { +            u2f.sign(request.authenticateRequests, function(data) { +                if(data.errorCode) +                    connector.onAuthenticateResponse(data.errorCode, request); +                else +                    connector.onAuthenticateResponse(JSON.stringify(data)); +            }); +        }, 500); +    } +} diff --git a/java/vaadin/src/main/java/ch/asynk/ui/Icons.java b/java/vaadin/src/main/java/ch/asynk/ui/Icons.java index 98bfb5f..3cc96bc 100644 --- a/java/vaadin/src/main/java/ch/asynk/ui/Icons.java +++ b/java/vaadin/src/main/java/ch/asynk/ui/Icons.java @@ -6,4 +6,13 @@ public class Icons  {      public static final ThemeResource help = new ThemeResource("icons/help.png");      public static final ThemeResource home = new ThemeResource("icons/home.png"); +    public static final ThemeResource u2f128 = new ThemeResource("icons/u2f-128.png"); +    public static final ThemeResource u2f64 = new ThemeResource("icons/u2f-64.png"); +    public static final ThemeResource u2f32 = new ThemeResource("icons/u2f-32.png"); +    public static final ThemeResource u2f24 = new ThemeResource("icons/u2f-24.png"); +    public static final ThemeResource u2flogo = new ThemeResource("icons/u2f-logo.png"); +    public static final ThemeResource u2flogo48 = new ThemeResource("icons/u2f-logo-48.png"); +    public static final ThemeResource u2flock = new ThemeResource("icons/u2f-lock.png"); +    public static final ThemeResource thumbUp = new ThemeResource("icons/thumb_up.png"); +    public static final ThemeResource thumbDown = new ThemeResource("icons/thumb_down.png");  } diff --git a/java/vaadin/src/main/java/ch/asynk/ui/ViewMain.java b/java/vaadin/src/main/java/ch/asynk/ui/ViewMain.java index 489e328..66ef64f 100644 --- a/java/vaadin/src/main/java/ch/asynk/ui/ViewMain.java +++ b/java/vaadin/src/main/java/ch/asynk/ui/ViewMain.java @@ -1,20 +1,43 @@  package ch.asynk.ui; +import com.vaadin.data.validator.RegexpValidator;  import com.vaadin.navigator.View;  import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; +import com.vaadin.server.Page; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.Component; +import com.vaadin.ui.Image;  import com.vaadin.ui.Label; +import com.vaadin.ui.Notification; +import com.vaadin.ui.Panel;  import com.vaadin.ui.TabSheet; +import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; +import com.vaadin.ui.GridLayout;  import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; -public class ViewMain extends TabSheet implements View +import ch.asynk.security.U2fConnector; + +public class ViewMain extends TabSheet implements View, U2fConnector.U2fListener  {      private static final long serialVersionUID = 1L; +    private static final String U2F_TITLE = "FIDO U2F (Universal 2nd factor)"; + +    private final Window u2fWindow; +    private final U2fConnector u2fConnector; + +    private enum Action { U2F_REGISTER, U2F_AUTHENTICATE } +      public ViewMain()      { +        u2fWindow = u2fWindow(); +        u2fConnector = new U2fConnector();          addStyleName("framed"); -        addTab(content("0"), "Tab 0", Icons.home, 0); -        addTab(content("1"), "Tab 1"); +        addTab(u2fComponent("register", Action.U2F_REGISTER), "Register", Icons.u2flogo48); +        addTab(u2fComponent("authenticate", Action.U2F_AUTHENTICATE), "Authenticate", Icons.u2f32);          setSelectedTab(2);      } @@ -29,4 +52,110 @@ public class ViewMain extends TabSheet implements View          ly.addComponent(new Label(" Hi, this is " + s));          return ly;      } + +    private Component u2fComponent(final String title, Action action) +    { +        final TextField tf = new TextField(); +        tf.setRequired(true); +        tf.setInputPrompt("username"); +        tf.addValidator(new RegexpValidator("^(?=.{6,20}$)[a-zA-Z][a-zA-Z0-9#@.]+[a-zA-Z]$", "invalid username")); + +        final ViewMain instance = this; +        final Button bt = new Button(title.toLowerCase()); +        bt.addClickListener(new Button.ClickListener() { +            private static final long serialVersionUID = 1L; +            @Override +            public void buttonClick(Button.ClickEvent event) { +                try { +                    tf.validate(); +                    String userId = tf.getValue(); +                    tf.setValue(""); +                    if (action == Action.U2F_REGISTER) +                        u2fConnector.sendRegisterRequest(userId, instance); +                    else if (action == Action.U2F_AUTHENTICATE) +                        u2fConnector.startAuthentication(userId, instance); +                } catch (com.yubico.u2f.exceptions.U2fBadConfigurationException e) { +                    Notification.show(U2F_TITLE, e.getMessage(), Notification.Type.ERROR_MESSAGE); +                } catch (Exception e) { +                    Notification.show("invalid user name"); +                } +            } +        }); + +        Panel panel = new Panel(title); +        panel.setWidth("450px"); +        final GridLayout ly = new GridLayout(2,2); +        ly.setMargin(true); +        ly.setSpacing(true); +        panel.setContent(ly); +        ly.addComponent(new Image(null, Icons.u2flock), 0, 0, 0, 1); +        ly.addComponent(tf, 1, 0); +        ly.addComponent(bt,1,1); +        ly.setComponentAlignment(bt, Alignment.MIDDLE_CENTER); + +        VerticalLayout vl = new VerticalLayout(); +        vl.setMargin(true); +        vl.setSizeFull(); +        vl.addComponent(panel); +        vl.setComponentAlignment(panel, Alignment.MIDDLE_CENTER); +        return vl; +    } + +    public void u2fCallback(U2fConnector.U2fAction action, String msg) +    { +        Notification n = null; +        switch (action) { +            case REGISTRATION_PENDING: +            case AUTHENTICATION_PENDING: +                UI.getCurrent().addWindow(u2fWindow); +                break; +            case REGISTRATION_SUCCESS: +                msg = "Your token has been successfully registred."; +                break; +            case REGISTRATION_FAILURE: +                msg = "Registration failed with error : " + msg; +                break; +            case AUTHENTICATION_SUCCESS: +                msg = "You have been successfully authenticated."; +                break; +            case AUTHENTICATION_FAILURE: +                msg = "Authentication failed with error : " + msg; +                break; +        } +        switch (action) { +            case REGISTRATION_SUCCESS: +            case AUTHENTICATION_SUCCESS: +                n = new Notification(U2F_TITLE, msg, Notification.Type.HUMANIZED_MESSAGE); +                n.setIcon(Icons.thumbUp); +                break; +            case REGISTRATION_FAILURE: +            case AUTHENTICATION_FAILURE: +                n = new Notification(U2F_TITLE, msg, Notification.Type.ERROR_MESSAGE); +                n.setIcon(Icons.thumbDown); +                break; +        } +        if (n != null) { +            n.setDelayMsec(5000); +            u2fWindow.close(); +            n.show(Page.getCurrent()); +        } +    } + +    private Window u2fWindow() +    { +        Window w = new Window(U2F_TITLE); +        final VerticalLayout vy = new VerticalLayout(); +        vy.setMargin(true); +        vy.setSpacing(true); +        vy.addComponent(new Label("Insert your u2f token and click on it.")); +        vy.addComponent(new Image(null, Icons.u2flock)); +        w.setModal(true); +        w.setClosable(false); +        w.setContent(vy); +        w.setResizable(false); +        w.setWidth("280px"); +        w.setHeight("220px"); +        w.center(); +        return w; +    }  } diff --git a/java/vaadin/src/run/java/ch/asynk/Main.java b/java/vaadin/src/run/java/ch/asynk/Main.java index 222fccb..005ac9c 100644 --- a/java/vaadin/src/run/java/ch/asynk/Main.java +++ b/java/vaadin/src/run/java/ch/asynk/Main.java @@ -84,7 +84,7 @@ class HttpVaadinJettyServer extends VaadinJettyServer      @Override      protected void configure(int port)      { -        System.out.println("http://127.0.0.1:" + port + "/hello"); +        System.out.println("http://localhost:" + port + "/hello");          addConnector(httpConnector(port));      }  } @@ -96,7 +96,7 @@ class HttpsVaadinJettyServer extends VaadinJettyServer      @Override      protected void configure(int port)      { -        System.out.println("https://127.0.0.1:" + port + "/hello"); +        System.out.println("https://localhost:" + port + "/hello");          addConnector(httpsConnector(port));      }  } @@ -107,8 +107,8 @@ class HttpHttpsVaadinJettyServer extends HttpsVaadinJettyServer      @Override      protected void configure(int port)      { -        System.out.println("http://127.0.0.1:" + port + "/hello"); -        System.out.println("https://127.0.0.1:" + (port + 1) + "/hello"); +        System.out.println("http://localhost:" + port + "/hello"); +        System.out.println("https://localhost:" + (port + 1) + "/hello");          this.setConnectors(new Connector[] { httpConnector(port), httpsConnector(port + 1) });      }  } @@ -121,6 +121,6 @@ public class Main          String webRoot = System.getProperty("WEBROOT");          if (webRoot == null) webRoot = "./src/main/WebContent"; -        new HttpVaadinJettyServer(port, webRoot).start(); +        new HttpsVaadinJettyServer(port, webRoot).start();      }  } | 
