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.png Binary files differnew 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.png Binary files differnew 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.png Binary files differnew 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.png Binary files differnew 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.png Binary files differnew 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.png Binary files differnew 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.png Binary files differnew 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.png Binary files differnew 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.png Binary files differnew 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(); } } |