summaryrefslogtreecommitdiffstats
path: root/java/vaadin-u2f/src/main
diff options
context:
space:
mode:
authorJérémy Zurcher <jeremy@asynk.ch>2017-10-27 15:07:39 +0200
committerJérémy Zurcher <jeremy@asynk.ch>2017-10-27 15:07:39 +0200
commit78e9a723e1a9afd42c84c57b2d1c9ff1b3de78f4 (patch)
treefb46efea5f5b908a7f57c5eea3f3c7232d6147f6 /java/vaadin-u2f/src/main
parent6799fa1e2922aab7802a09c2f147eca419e9947e (diff)
downloadshare-78e9a723e1a9afd42c84c57b2d1c9ff1b3de78f4.zip
share-78e9a723e1a9afd42c84c57b2d1c9ff1b3de78f4.tar.gz
add vaadin-u2f
Diffstat (limited to 'java/vaadin-u2f/src/main')
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/help.pngbin0 -> 1582 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/home.pngbin0 -> 1783 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_down.pngbin0 -> 3326 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_up.pngbin0 -> 3226 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-128.pngbin0 -> 2505 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-24.pngbin0 -> 732 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-32.pngbin0 -> 901 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-64.pngbin0 -> 1882 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-lock.pngbin0 -> 2614 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo-48.pngbin0 -> 1791 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo.pngbin0 -> 10109 bytes
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/layouts/About.html10
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/mytheme.scss17
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/styles.scss4
-rw-r--r--java/vaadin-u2f/src/main/WebContent/VAADIN/widgetsets/mywidgetset.gwt.xml3
-rw-r--r--java/vaadin-u2f/src/main/WebContent/WEB-INF/web.xml31
-rw-r--r--java/vaadin-u2f/src/main/java/ch/asynk/Daddy.java25
-rw-r--r--java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldServlet.java38
-rw-r--r--java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldUI.java135
-rw-r--r--java/vaadin-u2f/src/main/java/ch/asynk/security/U2fConnector.java164
-rw-r--r--java/vaadin-u2f/src/main/java/ch/asynk/security/U2fService.java12
-rw-r--r--java/vaadin-u2f/src/main/java/ch/asynk/security/U2fServiceImpl.java43
-rw-r--r--java/vaadin-u2f/src/main/java/ch/asynk/security/u2f-api.js748
-rw-r--r--java/vaadin-u2f/src/main/java/ch/asynk/security/u2f_connector.js30
-rw-r--r--java/vaadin-u2f/src/main/java/ch/asynk/ui/Icons.java18
-rw-r--r--java/vaadin-u2f/src/main/java/ch/asynk/ui/ViewMain.java176
-rw-r--r--java/vaadin-u2f/src/main/resources/Application.properties5
-rw-r--r--java/vaadin-u2f/src/main/resources/logback-test.xml41
28 files changed, 1500 insertions, 0 deletions
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/help.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/help.png
new file mode 100644
index 0000000..e91d95a
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/help.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/home.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/home.png
new file mode 100644
index 0000000..7362322
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/home.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_down.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_down.png
new file mode 100644
index 0000000..7e1709f
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_down.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_up.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_up.png
new file mode 100644
index 0000000..cc442d5
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/thumb_up.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-128.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-128.png
new file mode 100644
index 0000000..52d0761
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-128.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-24.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-24.png
new file mode 100644
index 0000000..bd5594a
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-24.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-32.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-32.png
new file mode 100644
index 0000000..78a15c4
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-32.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-64.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-64.png
new file mode 100644
index 0000000..c89c931
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-64.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-lock.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-lock.png
new file mode 100644
index 0000000..7823ff2
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-lock.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo-48.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo-48.png
new file mode 100644
index 0000000..3641de1
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo-48.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo.png b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo.png
new file mode 100644
index 0000000..a18234b
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/icons/u2f-logo.png
Binary files differ
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/layouts/About.html b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/layouts/About.html
new file mode 100644
index 0000000..7020e1d
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/layouts/About.html
@@ -0,0 +1,10 @@
+<div style='margin:10px'>
+<h1>HelloWorld 0.0.0</h1>
+
+<p>powered by <ul>
+<li><a target="_blank" href="https://vaadin.com">vaadin</a> java framework for building modern web applications</li>
+<li><a target="_blank" href="http://debian.org">debian</a> a free operating system (OS) for your computer.</li>
+</ul></p>
+<p>written by <a target="_blank" href="mailto:jeremy@asynk.ch">Jérémy Zurcher</a>
+as a part of <a target="_blank" href="https://github.com/jeremyz/share">share</a></p>.
+</div>
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/mytheme.scss b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/mytheme.scss
new file mode 100644
index 0000000..ad29293
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/mytheme.scss
@@ -0,0 +1,17 @@
+
+@import "../valo/valo.scss";
+
+@mixin mytheme {
+ @include valo;
+ .v-app { background-color: #e6e6e9; }
+ .mybg { background-color: #e6e6e6; }
+ .v-verticallayout-margin-top { padding-top: $v-layout-margin-top; }
+ .v-verticallayout-margin-right { padding-right: $v-layout-margin-top; }
+ .v-menubar {
+ .v-menubar-menuitem-menuRight {
+ position: absolute;
+ right: 10px;
+ top: 0;
+ }
+ }
+}
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/styles.scss b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/styles.scss
new file mode 100644
index 0000000..447ce98
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/themes/mytheme/styles.scss
@@ -0,0 +1,4 @@
+@import "mytheme.scss";
+.mytheme {
+ @include mytheme;
+}
diff --git a/java/vaadin-u2f/src/main/WebContent/VAADIN/widgetsets/mywidgetset.gwt.xml b/java/vaadin-u2f/src/main/WebContent/VAADIN/widgetsets/mywidgetset.gwt.xml
new file mode 100644
index 0000000..3c11cb9
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/VAADIN/widgetsets/mywidgetset.gwt.xml
@@ -0,0 +1,3 @@
+<module>
+ <inherits name="com.vaadin.DefaultWidgetSet" />
+</module>
diff --git a/java/vaadin-u2f/src/main/WebContent/WEB-INF/web.xml b/java/vaadin-u2f/src/main/WebContent/WEB-INF/web.xml
new file mode 100644
index 0000000..6e03c3d
--- /dev/null
+++ b/java/vaadin-u2f/src/main/WebContent/WEB-INF/web.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app id="HelloWorld" version="3.0"
+ xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
+
+ <display-name>Hello World Vaadin</display-name>
+ <description>
+ Hello World in Vaadin
+ </description>
+
+ <context-param>
+ <description>Vaadin production mode</description>
+ <param-name>productionMode</param-name>
+ <param-value>false</param-value>
+ </context-param>
+
+ <session-config>
+ <session-timeout>60</session-timeout>
+ <!-- <tracking-mode>COOKIE</tracking-mode> -->
+ </session-config>
+
+
+ <!-- <welcome-file-list> -->
+ <!-- <welcome-file>index.html</welcome-file> -->
+ <!-- <welcome-file>index.htm</welcome-file> -->
+ <!-- <welcome-file>index.jsp</welcome-file> -->
+ <!-- <welcome-file>default.html</welcome-file> -->
+ <!-- <welcome-file>default.htm</welcome-file> -->
+ <!-- <welcome-file>default.jsp</welcome-file> -->
+ <!-- </welcome-file-list> -->
+</web-app>
diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/Daddy.java b/java/vaadin-u2f/src/main/java/ch/asynk/Daddy.java
new file mode 100644
index 0000000..3f279fd
--- /dev/null
+++ b/java/vaadin-u2f/src/main/java/ch/asynk/Daddy.java
@@ -0,0 +1,25 @@
+package ch.asynk;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Daddy
+{
+ private static final long serialVersionUID = 1L;
+
+ public static final String SESSION_STATUS = "SESSION_STATUS";
+
+ private static final Logger logger;
+
+ static {
+ logger = LoggerFactory.getLogger(Daddy.class);
+ warn("beware Daddy is up");
+ }
+
+ public static void trace(String msg) { logger.trace(msg); }
+ public static void debug(String msg) { logger.debug(msg); }
+ public static void warn(String msg) { logger.warn(msg); }
+ public static void info(String msg) { logger.info(msg); }
+ public static void error(String msg) { logger.error(msg); }
+ public static void error(String msg, Exception e) { logger.error(String.format("%s : %s", msg, e.getMessage())); }
+}
diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldServlet.java b/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldServlet.java
new file mode 100644
index 0000000..d0e8374
--- /dev/null
+++ b/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldServlet.java
@@ -0,0 +1,38 @@
+package ch.asynk;
+
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.ServletException;
+
+import com.vaadin.annotations.VaadinServletConfiguration;
+import com.vaadin.server.ServiceException;
+import com.vaadin.server.SessionInitEvent;
+import com.vaadin.server.SessionInitListener;
+import com.vaadin.server.SessionDestroyEvent;
+import com.vaadin.server.SessionDestroyListener;
+import com.vaadin.server.VaadinServlet;
+
+@WebServlet(value = "/*", asyncSupported = true)
+@VaadinServletConfiguration(productionMode = false, ui = ch.asynk.HelloWorldUI.class, closeIdleSessions = true)
+public class HelloWorldServlet extends VaadinServlet implements SessionInitListener, SessionDestroyListener
+{
+ private static final long serialVersionUID = 511085337415583793L;
+ @Override
+ protected void servletInitialized() throws ServletException {
+ super.servletInitialized();
+ getService().addSessionInitListener(this);
+ getService().addSessionDestroyListener(this);
+ }
+
+ @Override
+ public void sessionInit(SessionInitEvent event) throws ServiceException
+ {
+ event.getSession().setLocale(new java.util.Locale("fr", "CH"));
+ event.getSession().setAttribute(Daddy.SESSION_STATUS, "unknown");
+ System.err.println("sessionInit");
+ }
+
+ @Override
+ public void sessionDestroy(SessionDestroyEvent event) {
+ System.err.println("sessionDestroy");
+ }
+}
diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldUI.java b/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldUI.java
new file mode 100644
index 0000000..1f293df
--- /dev/null
+++ b/java/vaadin-u2f/src/main/java/ch/asynk/HelloWorldUI.java
@@ -0,0 +1,135 @@
+package ch.asynk;
+
+import com.vaadin.annotations.PreserveOnRefresh;
+import com.vaadin.annotations.Theme;
+import com.vaadin.annotations.Title;
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.navigator.Navigator;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.CustomLayout;
+import com.vaadin.ui.HorizontalSplitPanel;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.MenuBar;
+import com.vaadin.ui.MenuBar.Command;
+import com.vaadin.ui.MenuBar.MenuItem;
+import com.vaadin.ui.Notification;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.ui.Window;
+
+import ch.asynk.ui.Icons;
+import ch.asynk.ui.ViewMain;
+
+@PreserveOnRefresh
+@Theme("mytheme")
+@Title("Hello!!")
+@Widgetset("mywidgetset")
+public class HelloWorldUI extends UI
+{
+ private static final long serialVersionUID = 1L;
+
+ private static final String HOME = "Home";
+ private static final String ABOUT = "About";
+
+ private Navigator navigator;
+
+ @Override
+ protected void init(VaadinRequest request)
+ {
+ System.err.println("UI init()");
+ final VerticalLayout vl = new VerticalLayout();
+ vl.setSizeFull();
+ vl.setMargin(true);
+ setContent(vl);
+
+ final MenuBar menu = createMenuBar();
+
+ final HorizontalSplitPanel hsplit = new HorizontalSplitPanel();
+ hsplit.setSplitPosition(80f);
+ hsplit.addComponent(createLeftPanel());
+ hsplit.addComponent(createRightPanel());
+
+ vl.addComponents(menu, hsplit);
+ vl.setExpandRatio(menu, 0);
+ vl.setExpandRatio(hsplit, 1);
+
+ navigateToHome();
+ }
+
+ private Component createLeftPanel()
+ {
+ final VerticalLayout vl = new VerticalLayout();
+ vl.addStyleName("margin-top");
+ vl.addStyleName("margin-right");
+ navigator = new Navigator(this, vl);
+ navigator.addView(HOME, ViewMain.class);
+ return vl;
+ }
+
+ private Component createRightPanel()
+ {
+ final VerticalLayout vl = new VerticalLayout();
+ vl.setMargin(true);
+ vl.addStyleName("mybg");
+ // vl.addStyleName("margin-top");
+ // vl.addStyleName("margin-right");
+ vl.addComponent(new Label("Hello World using mytheme"));
+ Button btn = new Button("Push Me!", Icons.home);
+ btn.addClickListener(new Button.ClickListener() {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public void buttonClick(Button.ClickEvent event) {
+ Notification.show("Pushed!");
+ Daddy.trace("trace");
+ Daddy.debug("debug");
+ Daddy.info("info");
+ Daddy.warn("warn");
+ Daddy.error("error");
+ }
+ });
+ vl.addComponent(btn);
+ return vl;
+ }
+
+ private MenuBar createMenuBar()
+ {
+ Command menuCommand = new Command() {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public void menuSelected(MenuItem selectedItem) {
+ String itemText = selectedItem.getText();
+ if (itemText.equals(HOME)) {
+ navigateToHome();
+ } else if (itemText.equals(ABOUT)) {
+ showAbout();
+ } else {
+ Daddy.error("unhandeled MenuItem : " + itemText);
+ }
+ }
+ };
+
+ final MenuBar menu = new MenuBar();
+ menu.addItem(HOME, Icons.home, menuCommand);
+ menu.addItem(ABOUT, Icons.help, menuCommand);
+ MenuBar.MenuItem login = menu.addItem((String) UI.getCurrent().getSession().getAttribute(Daddy.SESSION_STATUS), menuCommand);
+ login.setStyleName("menuRight");
+ menu.setWidth("100%");
+
+ return menu;
+ }
+
+ public void navigateToHome() { navigator.navigateTo(HOME); }
+
+ private void showAbout()
+ {
+ final Window about = new Window(ABOUT);
+ about.setContent( new CustomLayout(ABOUT) );
+ about.setHeight("50%");
+ about.setWidth("600px");
+ about.center();
+ about.setModal(true);
+ UI.getCurrent().addWindow(about);
+ }
+}
diff --git a/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fConnector.java b/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fConnector.java
new file mode 100644
index 0000000..24b406d
--- /dev/null
+++ b/java/vaadin-u2f/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-u2f/src/main/java/ch/asynk/security/U2fService.java b/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fService.java
new file mode 100644
index 0000000..9bce94e
--- /dev/null
+++ b/java/vaadin-u2f/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-u2f/src/main/java/ch/asynk/security/U2fServiceImpl.java b/java/vaadin-u2f/src/main/java/ch/asynk/security/U2fServiceImpl.java
new file mode 100644
index 0000000..f8e785f
--- /dev/null
+++ b/java/vaadin-u2f/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-u2f/src/main/java/ch/asynk/security/u2f-api.js b/java/vaadin-u2f/src/main/java/ch/asynk/security/u2f-api.js
new file mode 100644
index 0000000..9244d14
--- /dev/null
+++ b/java/vaadin-u2f/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-u2f/src/main/java/ch/asynk/security/u2f_connector.js b/java/vaadin-u2f/src/main/java/ch/asynk/security/u2f_connector.js
new file mode 100644
index 0000000..8723cb3
--- /dev/null
+++ b/java/vaadin-u2f/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-u2f/src/main/java/ch/asynk/ui/Icons.java b/java/vaadin-u2f/src/main/java/ch/asynk/ui/Icons.java
new file mode 100644
index 0000000..3cc96bc
--- /dev/null
+++ b/java/vaadin-u2f/src/main/java/ch/asynk/ui/Icons.java
@@ -0,0 +1,18 @@
+package ch.asynk.ui;
+
+import com.vaadin.server.ThemeResource;
+
+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-u2f/src/main/java/ch/asynk/ui/ViewMain.java b/java/vaadin-u2f/src/main/java/ch/asynk/ui/ViewMain.java
new file mode 100644
index 0000000..8fae5f0
--- /dev/null
+++ b/java/vaadin-u2f/src/main/java/ch/asynk/ui/ViewMain.java
@@ -0,0 +1,176 @@
+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.data.Binder;
+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;
+
+import ch.asynk.security.U2fConnector;
+
+class Login
+{
+ private String login;
+ public String getLogin() { return login; }
+ public void setLogin(final String login) { this.login = login; }
+}
+
+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(u2fComponent("register", Action.U2F_REGISTER), "Register", Icons.u2flogo48);
+ addTab(u2fComponent("authenticate", Action.U2F_AUTHENTICATE), "Authenticate", Icons.u2f32);
+ setSelectedTab(2);
+ }
+
+ @Override
+ public void enter(ViewChangeEvent event) {
+ System.err.println(this.getClass().getName()+"::enter");
+ }
+
+ private VerticalLayout content(String s)
+ {
+ VerticalLayout ly = new VerticalLayout();
+ ly.addComponent(new Label(" Hi, this is " + s));
+ return ly;
+ }
+
+ private Component u2fComponent(final String title, Action action)
+ {
+ final Login login = new Login();
+ final TextField tf = new TextField();
+ final Button bt = new Button(title.toLowerCase());
+ final Binder<Login> binder = new Binder<>();
+ binder.addValueChangeListener(evt -> bt.setEnabled(binder.isValid()) );
+ binder.forField(tf)
+ .asRequired("Login required")
+ .withValidator(s -> s.length() >= 6, "at least 6 characters")
+ .withValidator(s -> s.length() <= 20, "at most 20 characters")
+ .withValidator(new RegexpValidator("invalid username", "^(?=.{6,20}$)[a-zA-Z][a-zA-Z0-9#@.]+[a-zA-Z]$"))
+ .bind(Login::getLogin, Login::setLogin);
+
+ bt.setEnabled(false);
+ bt.addClickListener(e -> {
+ if (binder.writeBeanIfValid(login)) {
+ tf.setValue("");
+ tryAction(action, login);
+ }
+ });
+
+ 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;
+ }
+
+ private void tryAction(Action action, Login login)
+ {
+ final String userId = login.getLogin();
+ login.setLogin(null);
+ try {
+ if (action == Action.U2F_REGISTER)
+ u2fConnector.sendRegisterRequest(userId, this);
+ else if (action == Action.U2F_AUTHENTICATE)
+ u2fConnector.startAuthentication(userId, this);
+ } catch (com.yubico.u2f.exceptions.U2fBadConfigurationException ex) {
+ Notification.show(U2F_TITLE, ex.getMessage(), Notification.Type.ERROR_MESSAGE);
+ }
+ }
+
+ 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-u2f/src/main/resources/Application.properties b/java/vaadin-u2f/src/main/resources/Application.properties
new file mode 100644
index 0000000..512362b
--- /dev/null
+++ b/java/vaadin-u2f/src/main/resources/Application.properties
@@ -0,0 +1,5 @@
+app.name=VaadinHelloWorld
+app.version=0.0.1
+app.path=/hello
+app.env=DEV
+app.logfile=/var/log/tomcat7/HelloWorld.log
diff --git a/java/vaadin-u2f/src/main/resources/logback-test.xml b/java/vaadin-u2f/src/main/resources/logback-test.xml
new file mode 100644
index 0000000..a8fcf4b
--- /dev/null
+++ b/java/vaadin-u2f/src/main/resources/logback-test.xml
@@ -0,0 +1,41 @@
+<configuration debug="true">
+
+ <contextName>HelloWorld</contextName>
+ <property resource="Application.properties" />
+ <property name="LOG_LEVEL" value="INFO" />
+ <property name="LOG_REF" value="FILE" />
+ <property name="LOG_FILE" value="${app.logfile}" />
+ <property name="LOG_PATTERN" value="%contextName %date{ISO8601} [%-5level %thread] [%file:%line] - %msg%n" />
+
+ <if condition='property("app.env").contains("DEV")'>
+ <then>
+ <property name="LOG_LEVEL" value="DEBUG" />
+ </then>
+ </if>
+
+ <if condition='isDefined("TEST")'>
+ <then>
+ <property name="LOG_FILE" value="/tmp/${app.name}.log" />
+ <property name="LOG_LEVEL" value="DEBUG" />
+ <property name="LOG_REF" value="STDOUT" />
+ </then>
+ </if>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>${LOG_PATTERN}</pattern>
+ </encoder>
+ </appender>
+
+ <appender name="FILE" class="ch.qos.logback.core.FileAppender">
+ <file>${LOG_FILE}</file>
+ <encoder>
+ <pattern>${LOG_PATTERN}</pattern>
+ </encoder>
+ </appender>
+
+ <root level="${LOG_LEVEL}">
+ <appender-ref ref="${LOG_REF}" />
+ </root>
+
+</configuration>