diff --git a/.gitignore b/.gitignore
index 14bc68c..4656a53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
-/nbproject/private/
\ No newline at end of file
+/nbproject/private/
+
+/vendor/
diff --git a/appinfo/app.php b/appinfo/app.php
new file mode 100644
index 0000000..baa7afb
--- /dev/null
+++ b/appinfo/app.php
@@ -0,0 +1,14 @@
+
+ * @copyright Christoph Wurst 2016
+ */
+
+OC_App::registerPersonal('twofactor_u2f', 'settings/personal');
diff --git a/appinfo/autoload.php b/appinfo/autoload.php
index e454192..336be9e 100644
--- a/appinfo/autoload.php
+++ b/appinfo/autoload.php
@@ -1,4 +1,5 @@
* @copyright Christoph Wurst 2016
*/
-
-namespace OCA\TwoFactorUf\AppInfo;
-
-use OCP\AppFramework\App;
-
-/**
- * Additional autoloader registration, e.g. registering composer autoloaders
- */
-// require_once __DIR__ . '/../vendor/autoload.php';
\ No newline at end of file
+require_once __DIR__ . '/../vendor/autoload.php';
diff --git a/appinfo/routes.php b/appinfo/routes.php
new file mode 100644
index 0000000..df25953
--- /dev/null
+++ b/appinfo/routes.php
@@ -0,0 +1,34 @@
+
+ *
+ * Two-factor TOTP
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+return [
+ 'routes' => [
+ [
+ 'name' => 'settings#startRegister',
+ 'url' => '/settings/startregister',
+ 'verb' => 'POST'
+ ],
+ [
+ 'name' => 'settings#finishRegister',
+ 'url' => '/settings/finishregister',
+ 'verb' => 'POST'
+ ],
+ ]
+];
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..368f3a6
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,15 @@
+{
+ "name": "christophwurst/twofactor_u2f",
+ "description": "A two factor provider for U2F devices for Nextcloud",
+ "type": "project",
+ "license": "agplv3",
+ "authors": [
+ {
+ "name": "Christoph Wurst",
+ "email": "christoph@winzerhof-wurst.at"
+ }
+ ],
+ "require": {
+ "yubico/u2flib-server": "^1.0"
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..941461e
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,50 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+ "This file is @generated automatically"
+ ],
+ "hash": "901e1b2c0ead1ebd56edbe3b05f81ad1",
+ "content-hash": "869de9edf8a8cc8838b095308dd980ce",
+ "packages": [
+ {
+ "name": "yubico/u2flib-server",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Yubico/php-u2flib-server.git",
+ "reference": "407eb21da24150aad30bcd8cc0ee72963eac5e9d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Yubico/php-u2flib-server/zipball/407eb21da24150aad30bcd8cc0ee72963eac5e9d",
+ "reference": "407eb21da24150aad30bcd8cc0ee72963eac5e9d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "description": "Library for U2F implementation",
+ "homepage": "https://developers.yubico.com/php-u2flib-server",
+ "time": "2016-02-19 09:47:51"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": []
+}
diff --git a/js/settings.js b/js/settings.js
new file mode 100644
index 0000000..88f888b
--- /dev/null
+++ b/js/settings.js
@@ -0,0 +1,16 @@
+/* global OC */
+
+(function (OC) {
+ 'use strict';
+
+ OC.Settings = OC.Settings || {};
+ OC.Settings.TwoFactorU2F = OC.Settings.TwoFactorU2F || {};
+
+ $(function () {
+ var view = new OC.Settings.TwoFactorU2F.View({
+ el: $('#twofactor-u2f-settings')
+ });
+ view.render();
+ });
+})(OC);
+
diff --git a/js/settingsview.js b/js/settingsview.js
new file mode 100644
index 0000000..f54a42f
--- /dev/null
+++ b/js/settingsview.js
@@ -0,0 +1,59 @@
+/* global Backbone, Handlebars, OC, u2f */
+
+(function (OC, Backbone, Handlebars, $, u2f) {
+ 'use strict';
+
+ OC.Settings = OC.Settings || {};
+ OC.Settings.TwoFactorU2F = OC.Settings.TwoFactorU2F || {};
+
+ var TEMPLATE = '
'
+ + ' '
+ + '
';
+
+ var View = Backbone.View.extend({
+ template: Handlebars.compile(TEMPLATE),
+ events: {
+ 'click #u2f-register': 'onRegister'
+ },
+ initialize: function () {
+
+ },
+ render: function (data) {
+ this.$el.html(this.template(data));
+ },
+ onRegister: function () {
+ console.log('start register…');
+ var url = OC.generateUrl('apps/twofactor_u2f/settings/startregister');
+ $.ajax(url, {
+ method: 'POST'
+ }).done(function (data) {
+ this.doRegister(data.req, data.sigs);
+ }.bind(this));
+ },
+ doRegister: function (req, sigs) {
+ console.log('doRegister', req, sigs);
+ u2f.register([req], sigs, function (data) {
+ console.log(data.errorCode);
+ if (data.errorCode && data.errorCode !== 0) {
+ alert("registration failed with errror: " + data.errorCode);
+ return;
+ }
+ this.finishRegister(data);
+ }.bind(this));
+ },
+ finishRegister: function (data) {
+ console.log('finish register…', data);
+ var url = OC.generateUrl('apps/twofactor_u2f/settings/finishregister');
+ $.ajax(url, {
+ method: 'POST',
+ data: data
+ }).then(function() {
+ OC.Notification.showTemporary('U2F device successfully registered');
+ console.log('registration finished');
+ });
+ }
+ });
+
+ OC.Settings.TwoFactorU2F.View = View;
+
+})(OC, Backbone, Handlebars, $, u2f);
\ No newline at end of file
diff --git a/js/vendor/u2f-api.js b/js/vendor/u2f-api.js
new file mode 100644
index 0000000..0f06f50
--- /dev/null
+++ b/js/vendor/u2f-api.js
@@ -0,0 +1,651 @@
+// 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 || {};
+
+/**
+ * The U2F extension id
+ * @type {string}
+ * @const
+ */
+u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
+
+/**
+ * Message types for messsages to/from the extension
+ * @const
+ * @enum {string}
+ */
+u2f.MessageTypes = {
+ 'U2F_REGISTER_REQUEST': 'u2f_register_request',
+ 'U2F_SIGN_REQUEST': 'u2f_sign_request',
+ 'U2F_REGISTER_RESPONSE': 'u2f_register_response',
+ 'U2F_SIGN_RESPONSE': 'u2f_sign_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 type for registration requests
+ * @typedef {{
+ * type: u2f.MessageTypes,
+ * signRequests: Array,
+ * registerRequests: ?Array,
+ * timeoutSeconds: ?number,
+ * requestId: ?number
+ * }}
+ */
+u2f.Request;
+
+/**
+ * A message for registration responses
+ * @typedef {{
+ * type: u2f.MessageTypes,
+ * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
+ * requestId: ?number
+ * }}
+ */
+u2f.Response;
+
+/**
+ * An error object for responses
+ * @typedef {{
+ * errorCode: u2f.ErrorCodes,
+ * errorMessage: ?string
+ * }}
+ */
+u2f.Error;
+
+/**
+ * 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,
+ * appId: string
+ * }}
+ */
+u2f.RegisterRequest;
+
+/**
+ * Data object for a registration response.
+ * @typedef {{
+ * registrationData: string,
+ * clientData: string
+ * }}
+ */
+u2f.RegisterResponse;
+
+
+// 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 {
+ // 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;
+};
+
+/**
+ * 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);
+};
+
+/**
+ * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
+ * @param {Port} port
+ * @constructor
+ * @private
+ */
+u2f.WrappedChromeRuntimePort_ = function(port) {
+ this.port_ = port;
+};
+
+/**
+ * Format a return a sign request.
+ * @param {Array} signRequests
+ * @param {number} timeoutSeconds
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ =
+ function(signRequests, timeoutSeconds, reqId) {
+ return {
+ type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+ signRequests: signRequests,
+ timeoutSeconds: timeoutSeconds,
+ requestId: reqId
+ };
+ };
+
+/**
+ * Format a return a register request.
+ * @param {Array} signRequests
+ * @param {Array} signRequests
+ * @param {number} timeoutSeconds
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ =
+ function(signRequests, registerRequests, timeoutSeconds, reqId) {
+ return {
+ type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
+ signRequests: signRequests,
+ registerRequests: registerRequests,
+ 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 intentLocation = /** @type {string} */ (message);
+ document.location = intentLocation;
+};
+
+/**
+ * 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']));
+ responseObject['requestId'] = this.requestId_;
+ }
+
+ /* Sign responses from the authenticator do not conform to U2F,
+ * convert to U2F here. */
+ responseObject = this.doResponseFixups_(responseObject);
+ callback({'data': responseObject});
+ };
+
+/**
+ * Fixup the response provided by the Authenticator to conform with
+ * the U2F spec.
+ * @param {Object} responseData
+ * @return {Object} the U2F compliant response object
+ */
+u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ =
+ function(responseObject) {
+ if (responseObject.hasOwnProperty('responseData')) {
+ return responseObject;
+ } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) {
+ // Only sign responses require fixups. If this is not a response
+ // to a sign request, then an internal error has occurred.
+ return {
+ 'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE,
+ 'responseData': {
+ 'errorCode': u2f.ErrorCodes.OTHER_ERROR,
+ 'errorMessage': 'Internal error: invalid response from Authenticator'
+ }
+ };
+ }
+
+ /* Non-conformant sign response, do fixups. */
+ var encodedChallengeObject = responseObject['challenge'];
+ if (typeof encodedChallengeObject !== 'undefined') {
+ var challengeObject = JSON.parse(atob(encodedChallengeObject));
+ var serverChallenge = challengeObject['challenge'];
+ var challengesList = this.requestObject_['signData'];
+ var requestChallengeObject = null;
+ for (var i = 0; i < challengesList.length; i++) {
+ var challengeObject = challengesList[i];
+ if (challengeObject['keyHandle'] == responseObject['keyHandle']) {
+ requestChallengeObject = challengeObject;
+ break;
+ }
+ }
+ }
+ var responseData = {
+ 'errorCode': responseObject['resultCode'],
+ 'keyHandle': responseObject['keyHandle'],
+ 'signatureData': responseObject['signature'],
+ 'clientData': encodedChallengeObject
+ };
+ return {
+ 'type': u2f.MessageTypes.U2F_SIGN_RESPONSE,
+ 'responseData': responseData,
+ 'requestId': responseObject['requestId']
+ }
+ };
+
+/**
+ * Base URL for intents to Authenticator.
+ * @const
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
+ 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
+
+/**
+ * Format a return a sign request.
+ * @param {Array} signRequests
+ * @param {number} timeoutSeconds (ignored for now)
+ * @param {number} reqId
+ * @return {string}
+ */
+u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ =
+ function(signRequests, timeoutSeconds, reqId) {
+ if (!signRequests || signRequests.length == 0) {
+ return null;
+ }
+ /* TODO(fixme): stash away requestId, as the authenticator app does
+ * not return it for sign responses. */
+ this.requestId_ = reqId;
+ /* TODO(fixme): stash away the signRequests, to deal with the legacy
+ * response format returned by the Authenticator app. */
+ this.requestObject_ = {
+ 'type': u2f.MessageTypes.U2F_SIGN_REQUEST,
+ 'signData': signRequests,
+ 'requestId': reqId,
+ 'timeout': timeoutSeconds
+ };
+
+ var appId = signRequests[0]['appId'];
+ var intentUrl =
+ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
+ ';S.appId=' + encodeURIComponent(appId) +
+ ';S.eventId=' + reqId +
+ ';S.challenges=' +
+ encodeURIComponent(
+ JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end';
+ return intentUrl;
+ };
+
+/**
+ * Get the browser data objects from the challenge list
+ * @param {Array} challenges list of challenges
+ * @return {Array} list of browser data objects
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_
+ .prototype.getBrowserDataList_ = function(challenges) {
+ return challenges
+ .map(function(challenge) {
+ var browserData = {
+ 'typ': 'navigator.id.getAssertion',
+ 'challenge': challenge['challenge']
+ };
+ var challengeObject = {
+ 'challenge' : browserData,
+ 'keyHandle' : challenge['keyHandle']
+ };
+ return challengeObject;
+ });
+};
+
+/**
+ * Format a return a register request.
+ * @param {Array} signRequests
+ * @param {Array} enrollChallenges
+ * @param {number} timeoutSeconds (ignored for now)
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ =
+ function(signRequests, enrollChallenges, timeoutSeconds, reqId) {
+ if (!enrollChallenges || enrollChallenges.length == 0) {
+ return null;
+ }
+ // Assume the appId is the same for all enroll challenges.
+ var appId = enrollChallenges[0]['appId'];
+ var registerRequests = [];
+ for (var i = 0; i < enrollChallenges.length; i++) {
+ var registerRequest = {
+ 'challenge': enrollChallenges[i]['challenge'],
+ 'version': enrollChallenges[i]['version']
+ };
+ if (enrollChallenges[i]['appId'] != appId) {
+ // Only include the appId when it differs from the first appId.
+ registerRequest['appId'] = enrollChallenges[i]['appId'];
+ }
+ registerRequests.push(registerRequest);
+ }
+ var registeredKeys = [];
+ if (signRequests) {
+ for (i = 0; i < signRequests.length; i++) {
+ var key = {
+ 'keyHandle': signRequests[i]['keyHandle'],
+ 'version': signRequests[i]['version']
+ };
+ // Only include the appId when it differs from the appId that's
+ // being registered now.
+ if (signRequests[i]['appId'] != appId) {
+ key['appId'] = signRequests[i]['appId'];
+ }
+ registeredKeys.push(key);
+ }
+ }
+ var request = {
+ 'type': u2f.MessageTypes.U2F_REGISTER_REQUEST,
+ 'appId': appId,
+ 'registerRequests': registerRequests,
+ 'registeredKeys': registeredKeys,
+ 'requestId': reqId,
+ 'timeoutSeconds': timeoutSeconds
+ };
+ var intentUrl =
+ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
+ ';S.request=' + encodeURIComponent(JSON.stringify(request)) +
+ ';end';
+ /* TODO(fixme): stash away requestId, this is is not necessary for
+ * register requests, but here to keep parity with sign.
+ */
+ this.requestId_ = reqId;
+ return intentUrl;
+ };
+
+
+/**
+ * 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}
+ * @private
+ */
+u2f.waitingForPort_ = [];
+
+/**
+ * A counter for requestIds.
+ * @type {number}
+ * @private
+ */
+u2f.reqCounter_ = 0;
+
+/**
+ * A map from requestIds to client callbacks
+ * @type {Object.}
+ * @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.} 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.
+ * @param {Array} signRequests
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.sign = function(signRequests, 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 = port.formatSignRequest_(signRequests, timeoutSeconds, reqId);
+ port.postMessage(req);
+ });
+};
+
+/**
+ * Dispatches register requests to available U2F tokens. An array of sign
+ * requests identifies already registered tokens.
+ * @param {Array} registerRequests
+ * @param {Array} signRequests
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.register = function(registerRequests, signRequests,
+ 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 = port.formatRegisterRequest_(
+ signRequests, registerRequests, timeoutSeconds, reqId);
+ port.postMessage(req);
+ });
+};
\ No newline at end of file
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
new file mode 100644
index 0000000..a7727cd
--- /dev/null
+++ b/lib/Controller/SettingsController.php
@@ -0,0 +1,97 @@
+
+ * @copyright Christoph Wurst 2016
+ */
+
+namespace OCA\TwoFactor_U2F\Controller;
+
+require_once(__DIR__ . '/../../vendor/yubico/u2flib-server/src/u2flib_server/U2F.php');
+
+use OC;
+use OCP\AppFramework\Controller;
+use OCP\ILogger;
+use OCP\IRequest;
+use OCP\ISession;
+use u2flib_server\U2F;
+
+class SettingsController extends Controller {
+
+ /** @var ISession */
+ private $session;
+
+ /** @var ILogger */
+ private $logger;
+
+ public function __construct($appName, IRequest $request, ISession $session, ILogger $logger) {
+ parent::__construct($appName, $request);
+ $this->session = $session;
+ $this->logger = $logger;
+ }
+
+ private function getU2f() {
+ return new U2F(OC::$server->getURLGenerator()->getAbsoluteURL('/'));
+ }
+
+ private function getRegs() {
+ if (!file_exists('/tmp/yubi')) {
+ return [];
+ }
+ return [json_decode(file_get_contents('/tmp/yubi'))];
+ }
+
+ private function setReg($data) {
+ file_put_contents('/tmp/yubi', json_encode($data));
+ }
+
+ /**
+ * @NoAdminRequired
+ * @UseSession
+ */
+ public function startRegister() {
+ $u2f = $this->getU2f();
+ $data = $u2f->getRegisterData($this->getRegs());
+ list($req, $sigs) = $data;
+
+ $this->logger->debug(json_encode($req));
+ $this->logger->debug(json_encode($sigs));
+
+ $this->session->set('twofactor_u2f_regReq', json_encode($req));
+
+ return [
+ 'req' => $req,
+ 'sigs' => $sigs,
+ 'username' => 'user', // TODO
+ ];
+ }
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param string $registrationData
+ * @param string $clientData
+ */
+ public function finishRegister($registrationData, $clientData) {
+ $this->logger->debug($registrationData);
+ $this->logger->debug($clientData);
+
+ $u2f = $this->getU2f();
+ $regReq = json_decode($this->session->get('twofactor_u2f_regReq'));
+ $regResp = [
+ 'registrationData' => $registrationData,
+ 'clientData' => $clientData,
+ ];
+ $reg = $u2f->doRegister($regReq, (object) $regResp);
+
+ $this->setReg($reg);
+
+ $this->logger->debug(json_encode($reg));
+ }
+
+}
diff --git a/settings/personal.php b/settings/personal.php
new file mode 100644
index 0000000..4ccf86b
--- /dev/null
+++ b/settings/personal.php
@@ -0,0 +1,5 @@
+fetchPage();
diff --git a/templates/personal.php b/templates/personal.php
new file mode 100644
index 0000000..a227cfb
--- /dev/null
+++ b/templates/personal.php
@@ -0,0 +1,12 @@
+
+
+
+
t('U2F Second-factor Auth')); ?>
+
+