diff --git a/class-two-factor-core.php b/class-two-factor-core.php index 6f0d2a3d..05ade1b5 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -116,7 +116,6 @@ public static function get_providers() { $providers = array( 'Two_Factor_Email' => TWO_FACTOR_DIR . 'providers/class-two-factor-email.php', 'Two_Factor_Totp' => TWO_FACTOR_DIR . 'providers/class-two-factor-totp.php', - 'Two_Factor_FIDO_U2F' => TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f.php', 'Two_Factor_Backup_Codes' => TWO_FACTOR_DIR . 'providers/class-two-factor-backup-codes.php', 'Two_Factor_Dummy' => TWO_FACTOR_DIR . 'providers/class-two-factor-dummy.php', ); @@ -132,18 +131,6 @@ public static function get_providers() { */ $providers = apply_filters( 'two_factor_providers', $providers ); - // FIDO U2F is PHP 5.3+ only. - if ( isset( $providers['Two_Factor_FIDO_U2F'] ) && version_compare( PHP_VERSION, '5.3.0', '<' ) ) { - unset( $providers['Two_Factor_FIDO_U2F'] ); - trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - sprintf( - /* translators: %s: version number */ - __( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - PHP_VERSION - ) - ); - } - /** * For each filtered provider, */ diff --git a/includes/Google/u2f-api.js b/includes/Google/u2f-api.js deleted file mode 100644 index 1036b920..00000000 --- a/includes/Google/u2f-api.js +++ /dev/null @@ -1,748 +0,0 @@ -//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 messages 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.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} 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} signRequests - * @param {Array} 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} - * @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. - * 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} 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} 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} registerRequests - * @param {Array} 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} registerRequests - * @param {Array} 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/includes/Yubico/U2F.php b/includes/Yubico/U2F.php deleted file mode 100644 index e819bbc1..00000000 --- a/includes/Yubico/U2F.php +++ /dev/null @@ -1,507 +0,0 @@ -appId = $appId; - $this->attestDir = $attestDir; - } - - /** - * Called to get a registration request to send to a user. - * Returns an array of one registration request and a array of sign requests. - * - * @param array $registrations List of current registrations for this - * user, to prevent the user from registering the same authenticator several - * times. - * @return array An array of two elements, the first containing a - * RegisterRequest the second being an array of SignRequest - * @throws Error - */ - public function getRegisterData(array $registrations = array()) - { - $challenge = $this->createChallenge(); - $request = new RegisterRequest($challenge, $this->appId); - $signs = $this->getAuthenticateData($registrations); - return array($request, $signs); - } - - /** - * Called to verify and unpack a registration message. - * - * @param RegisterRequest $request this is a reply to - * @param object $response response from a user - * @param bool $includeCert set to true if the attestation certificate should be - * included in the returned Registration object - * @return Registration - * @throws Error - */ - public function doRegister($request, $response, $includeCert = true) - { - if( !is_object( $request ) ) { - throw new \InvalidArgumentException('$request of doRegister() method only accepts object.'); - } - - if( !is_object( $response ) ) { - throw new \InvalidArgumentException('$response of doRegister() method only accepts object.'); - } - - if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { - throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING ); - } - - if( !is_bool( $includeCert ) ) { - throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.'); - } - - $rawReg = $this->base64u_decode($response->registrationData); - $regData = array_values(unpack('C*', $rawReg)); - $clientData = $this->base64u_decode($response->clientData); - $cli = json_decode($clientData); - - if($cli->challenge !== $request->challenge) { - throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE ); - } - - $registration = new Registration(); - $offs = 1; - $pubKey = substr($rawReg, $offs, PUBKEY_LEN); - $offs += PUBKEY_LEN; - // Decode the pubKey to make sure it's good. - $tmpKey = $this->pubkey_to_pem($pubKey); - if($tmpKey === null) { - throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); - } - $registration->publicKey = base64_encode($pubKey); - $khLen = $regData[$offs++]; - $kh = substr($rawReg, $offs, $khLen); - $offs += $khLen; - $registration->keyHandle = $this->base64u_encode($kh); - - // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes). - $certLen = 4; - $certLen += ($regData[$offs + 2] << 8); - $certLen += $regData[$offs + 3]; - - $rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen)); - $offs += $certLen; - $pemCert = "-----BEGIN CERTIFICATE-----\r\n"; - $pemCert .= chunk_split(base64_encode($rawCert), 64); - $pemCert .= "-----END CERTIFICATE-----"; - if($includeCert) { - $registration->certificate = base64_encode($rawCert); - } - if($this->attestDir) { - if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) { - throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION ); - } - } - - if(!openssl_pkey_get_public($pemCert)) { - throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); - } - $signature = substr($rawReg, $offs); - - $dataToVerify = chr(0); - $dataToVerify .= hash('sha256', $request->appId, true); - $dataToVerify .= hash('sha256', $clientData, true); - $dataToVerify .= $kh; - $dataToVerify .= $pubKey; - - if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) { - return $registration; - } else { - throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE ); - } - } - - /** - * Called to get an authentication request. - * - * @param array $registrations An array of the registrations to create authentication requests for. - * @return array An array of SignRequest - * @throws Error - */ - public function getAuthenticateData(array $registrations) - { - $sigs = array(); - $challenge = $this->createChallenge(); - foreach ($registrations as $reg) { - if( !is_object( $reg ) ) { - throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.'); - } - - $sig = new SignRequest(); - $sig->appId = $this->appId; - $sig->keyHandle = $reg->keyHandle; - $sig->challenge = $challenge; - $sigs[] = $sig; - } - return $sigs; - } - - /** - * Called to verify an authentication response - * - * @param array $requests An array of outstanding authentication requests - * @param array $registrations An array of current registrations - * @param object $response A response from the authenticator - * @return Registration - * @throws Error - * - * The Registration object returned on success contains an updated counter - * that should be saved for future authentications. - * If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of - * token cloning or similar and appropriate action should be taken. - */ - public function doAuthenticate(array $requests, array $registrations, $response) - { - if( !is_object( $response ) ) { - throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.'); - } - - if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { - throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING ); - } - - /** @var object|null $req */ - $req = null; - - /** @var object|null $reg */ - $reg = null; - - $clientData = $this->base64u_decode($response->clientData); - $decodedClient = json_decode($clientData); - foreach ($requests as $req) { - if( !is_object( $req ) ) { - throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.'); - } - - if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) { - break; - } - - $req = null; - } - if($req === null) { - throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST ); - } - foreach ($registrations as $reg) { - if( !is_object( $reg ) ) { - throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.'); - } - - if($reg->keyHandle === $response->keyHandle) { - break; - } - $reg = null; - } - if($reg === null) { - throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION ); - } - $pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey)); - if($pemKey === null) { - throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); - } - - $signData = $this->base64u_decode($response->signatureData); - $dataToVerify = hash('sha256', $req->appId, true); - $dataToVerify .= substr($signData, 0, 5); - $dataToVerify .= hash('sha256', $clientData, true); - $signature = substr($signData, 5); - - if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) { - $ctr = unpack("Nctr", substr($signData, 1, 4)); - $counter = $ctr['ctr']; - /* TODO: wrap-around should be handled somehow.. */ - if($counter > $reg->counter) { - $reg->counter = $counter; - return $reg; - } else { - throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW ); - } - } else { - throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE ); - } - } - - /** - * @return array - */ - private function get_certs() - { - $files = array(); - $dir = $this->attestDir; - if($dir && $handle = opendir($dir)) { - while(false !== ($entry = readdir($handle))) { - if(is_file("$dir/$entry")) { - $files[] = "$dir/$entry"; - } - } - closedir($handle); - } - return $files; - } - - /** - * @param string $data - * @return string - */ - private function base64u_encode($data) - { - return trim(strtr(base64_encode($data), '+/', '-_'), '='); - } - - /** - * @param string $data - * @return string - */ - private function base64u_decode($data) - { - return base64_decode(strtr($data, '-_', '+/')); - } - - /** - * @param string $key - * @return null|string - */ - private function pubkey_to_pem($key) - { - if(strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") { - return null; - } - - /* - * Convert the public key to binary DER format first - * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480 - * - * SEQUENCE(2 elem) 30 59 - * SEQUENCE(2 elem) 30 13 - * OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01 - * OID1.2.840.10045.3.1.7 (secp256r1) 06 08 2a 86 48 ce 3d 03 01 07 - * BIT STRING(520 bit) 03 42 ..key.. - */ - $der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01"; - $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42"; - $der .= "\0".$key; - - $pem = "-----BEGIN PUBLIC KEY-----\r\n"; - $pem .= chunk_split(base64_encode($der), 64); - $pem .= "-----END PUBLIC KEY-----"; - - return $pem; - } - - /** - * @return string - * @throws Error - */ - private function createChallenge() - { - $challenge = openssl_random_pseudo_bytes(32, $crypto_strong ); - if( $crypto_strong !== true ) { - throw new Error('Unable to obtain a good source of randomness', ERR_BAD_RANDOM); - } - - $challenge = $this->base64u_encode( $challenge ); - - return $challenge; - } - - /** - * Fixes a certificate where the signature contains unused bits. - * - * @param string $cert - * @return mixed - */ - private function fixSignatureUnusedBits($cert) - { - if(in_array(hash('sha256', $cert), $this->FIXCERTS)) { - $cert[strlen($cert) - 257] = "\0"; - } - return $cert; - } -} - -/** - * Class for building a registration request - * - * @package u2flib_server - */ -class RegisterRequest -{ - /** Protocol version */ - public $version = U2F_VERSION; - - /** Registration challenge */ - public $challenge; - - /** Application id */ - public $appId; - - /** - * @param string $challenge - * @param string $appId - * @internal - */ - public function __construct($challenge, $appId) - { - $this->challenge = $challenge; - $this->appId = $appId; - } -} - -/** - * Class for building up an authentication request - * - * @package u2flib_server - */ -class SignRequest -{ - /** Protocol version */ - public $version = U2F_VERSION; - - /** Authentication challenge */ - public $challenge; - - /** Key handle of a registered authenticator */ - public $keyHandle; - - /** Application id */ - public $appId; -} - -/** - * Class returned for successful registrations - * - * @package u2flib_server - */ -class Registration -{ - /** The key handle of the registered authenticator */ - public $keyHandle; - - /** The public key of the registered authenticator */ - public $publicKey; - - /** The attestation certificate of the registered authenticator */ - public $certificate; - - /** The counter associated with this registration */ - public $counter = -1; -} - -/** - * Error class, returned on errors - * - * @package u2flib_server - */ -class Error extends \Exception -{ - /** - * Override constructor and make message and code mandatory - * @param string $message - * @param int $code - * @param \Exception|null $previous - */ - public function __construct($message, $code, \Exception $previous = null) { - parent::__construct($message, $code, $previous); - } -} diff --git a/package.json b/package.json index c1e7d400..1d40adb7 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "build": "grunt build", "lint": "npm-run-all lint:*", "lint:php": "composer lint", - "lint:css": "wp-scripts lint-style ./user-edit.css ./providers/css/", - "lint:js": "wp-scripts lint-js ./Gruntfile.js ./providers/js/", + "lint:css": "wp-scripts lint-style ./user-edit.css", + "lint:js": "wp-scripts lint-js ./Gruntfile.js", "format": "npm-run-all format:*", "format:php": "composer format", "format:js": "npm run lint:js -- --fix", diff --git a/providers/class-two-factor-fido-u2f-admin-list-table.php b/providers/class-two-factor-fido-u2f-admin-list-table.php deleted file mode 100644 index deb220cf..00000000 --- a/providers/class-two-factor-fido-u2f-admin-list-table.php +++ /dev/null @@ -1,160 +0,0 @@ - wp_strip_all_tags( __( 'Name', 'two-factor' ) ), - 'added' => wp_strip_all_tags( __( 'Added', 'two-factor' ) ), - 'last_used' => wp_strip_all_tags( __( 'Last Used', 'two-factor' ) ), - ); - } - - /** - * Prepares the list of items for displaying. - * - * @since 0.1-dev - */ - public function prepare_items() { - $columns = $this->get_columns(); - $hidden = array(); - $sortable = array(); - $primary = 'name'; - $this->_column_headers = array( $columns, $hidden, $sortable, $primary ); - } - - /** - * Generates content for a single row of the table - * - * @since 0.1-dev - * @access protected - * - * @param object $item The current item. - * @param string $column_name The current column name. - * @return string - */ - protected function column_default( $item, $column_name ) { - switch ( $column_name ) { - case 'name': - $out = ''; - - $actions = array( - 'rename hide-if-no-js' => Two_Factor_FIDO_U2F_Admin::rename_link( $item ), - 'delete' => Two_Factor_FIDO_U2F_Admin::delete_link( $item ), - ); - - return esc_html( $item->name ) . $out . self::row_actions( $actions ); - case 'added': - return gmdate( get_option( 'date_format', 'r' ), $item->added ); - case 'last_used': - return gmdate( get_option( 'date_format', 'r' ), $item->last_used ); - default: - return 'WTF^^?'; - } - } - - /** - * Generates custom table navigation to prevent conflicting nonces. - * - * @since 0.1-dev - * @access protected - * - * @param string $which The location of the bulk actions: 'top' or 'bottom'. - */ - protected function display_tablenav( $which ) { - // Not used for the Security key list. - } - - /** - * Generates content for a single row of the table - * - * @since 0.1-dev - * @access public - * - * @param object $item The current item. - */ - public function single_row( $item ) { - ?> - - single_row_columns( $item ); ?> - - - - - - - - -
- getRegisterData( $security_keys ); - list( $req,$sigs ) = $data; - - update_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, $req ); - } catch ( Exception $e ) { - return false; - } - - wp_enqueue_style( - 'fido-u2f-admin', - plugins_url( 'css/fido-u2f-admin.css', __FILE__ ), - null, - self::asset_version() - ); - - wp_enqueue_script( - 'fido-u2f-admin', - plugins_url( 'js/fido-u2f-admin.js', __FILE__ ), - array( 'jquery', 'fido-u2f-api' ), - self::asset_version(), - true - ); - - /** - * Pass a U2F challenge and user data to our scripts - */ - - $translation_array = array( - 'user_id' => $user_id, - 'register' => array( - 'request' => $req, - 'sigs' => $sigs, - ), - 'text' => array( - 'insert' => esc_html__( 'Now insert (and tap) your Security Key.', 'two-factor' ), - 'error' => esc_html__( 'U2F request failed.', 'two-factor' ), - 'error_codes' => array( - // Map u2f.ErrorCodes to error messages. - 0 => esc_html__( 'Request OK.', 'two-factor' ), - 1 => esc_html__( 'Other U2F error.', 'two-factor' ), - 2 => esc_html__( 'Bad U2F request.', 'two-factor' ), - 3 => esc_html__( 'Unsupported U2F configuration.', 'two-factor' ), - 4 => esc_html__( 'U2F device ineligible.', 'two-factor' ), - 5 => esc_html__( 'U2F request timeout reached.', 'two-factor' ), - ), - 'u2f_not_supported' => esc_html__( 'FIDO U2F appears to be not supported by your web browser. Try using Google Chrome or Firefox.', 'two-factor' ), - ), - ); - - wp_localize_script( - 'fido-u2f-admin', - 'u2fL10n', - $translation_array - ); - - /** - * Script for admin UI - */ - - wp_enqueue_script( - 'inline-edit-key', - plugins_url( 'js/fido-u2f-admin-inline-edit.js', __FILE__ ), - array( 'jquery' ), - self::asset_version(), - true - ); - - wp_localize_script( - 'inline-edit-key', - 'inlineEditL10n', - array( - 'error' => esc_html__( 'Error while saving the changes.', 'two-factor' ), - ) - ); - } - - /** - * Return the current asset version number. - * - * Added as own helper to allow swapping the implementation once we inject - * it as a dependency. - * - * @return string - */ - protected static function asset_version() { - return Two_Factor_FIDO_U2F::asset_version(); - } - - /** - * Display the security key section in a users profile. - * - * This executes during the `show_user_security_settings` action. - * - * @since 0.1-dev - * - * @access public - * @static - * - * @param WP_User $user WP_User object of the logged-in user. - */ - public static function show_user_profile( $user ) { - wp_nonce_field( "user_security_keys-{$user->ID}", '_nonce_user_security_keys' ); - $new_key = false; - - $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user->ID ); - if ( $security_keys ) { - foreach ( $security_keys as &$security_key ) { - if ( property_exists( $security_key, 'new' ) ) { - $new_key = true; - unset( $security_key->new ); - - // If we've got a new one, update the db record to not save it there any longer. - Two_Factor_FIDO_U2F::update_security_key( $user->ID, $security_key ); - } - } - unset( $security_key ); - } - - ?> -
-

- - -

- -

- - -
- - - - - -
- - -
-

-
- - -

- - items = $security_keys; - $u2f_list_table->prepare_items(); - $u2f_list_table->display(); - $u2f_list_table->inline_edit(); - ?> -
- doRegister( get_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, true ), $response ); - $reg->new = true; - - Two_Factor_FIDO_U2F::add_security_key( $user_id, $reg ); - } catch ( Exception $e ) { - return false; - } - - delete_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY ); - - wp_safe_redirect( - add_query_arg( - array( - 'new_app_pass' => 1, - ), - wp_get_referer() - ) . '#security-keys-section' - ); - exit; - } - } - - /** - * Catch the delete security key request. - * - * This executes during the `load-profile.php` & `load-user-edit.php` actions. - * - * @since 0.1-dev - * - * @access public - * @static - */ - public static function catch_delete_security_key() { - $user_id = Two_Factor_Core::current_user_being_edited(); - - if ( ! empty( $user_id ) && ! empty( $_REQUEST['delete_security_key'] ) ) { - $slug = $_REQUEST['delete_security_key']; - - check_admin_referer( "delete_security_key-{$slug}", '_nonce_delete_security_key' ); - - Two_Factor_FIDO_U2F::delete_security_key( $user_id, $slug ); - - wp_safe_redirect( remove_query_arg( 'new_app_pass', wp_get_referer() ) . '#security-keys-section' ); - exit; - } - } - - /** - * Generate a link to rename a specified security key. - * - * @since 0.1-dev - * - * @access public - * @static - * - * @param array $item The current item. - * @return string - */ - public static function rename_link( $item ) { - return sprintf( '%s', esc_html__( 'Rename', 'two-factor' ) ); - } - - /** - * Generate a link to delete a specified security key. - * - * @since 0.1-dev - * - * @access public - * @static - * - * @param array $item The current item. - * @return string - */ - public static function delete_link( $item ) { - $delete_link = add_query_arg( 'delete_security_key', $item->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $delete_link = wp_nonce_url( $delete_link, "delete_security_key-{$item->keyHandle}", '_nonce_delete_security_key' ); - return sprintf( '%2$s', esc_url( $delete_link ), esc_html__( 'Delete', 'two-factor' ) ); - } - - /** - * Ajax handler for quick edit saving for a security key. - * - * @since 0.1-dev - * - * @access public - * @static - */ - public static function wp_ajax_inline_save() { - check_ajax_referer( 'keyinlineeditnonce', '_inline_edit' ); - - require TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin-list-table.php'; - $wp_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table(); - - if ( ! isset( $_POST['keyHandle'] ) ) { - wp_die(); - } - - $user_id = Two_Factor_Core::current_user_being_edited(); - $security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id ); - if ( ! $security_keys ) { - wp_die(); - } - - foreach ( $security_keys as &$key ) { - if ( $key->keyHandle === $_POST['keyHandle'] ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - break; - } - } - - $key->name = $_POST['name']; - - $updated = Two_Factor_FIDO_U2F::update_security_key( $user_id, $key ); - if ( ! $updated ) { - wp_die( esc_html__( 'Item not updated.', 'two-factor' ) ); - } - $wp_list_table->single_row( $key ); - wp_die(); - } -} diff --git a/providers/class-two-factor-fido-u2f.php b/providers/class-two-factor-fido-u2f.php deleted file mode 100644 index 330451c2..00000000 --- a/providers/class-two-factor-fido-u2f.php +++ /dev/null @@ -1,397 +0,0 @@ - -

- ID ); - $data = self::$u2f->getAuthenticateData( $keys ); - update_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, $data ); - } catch ( Exception $e ) { - ?> -

- $data, - ) - ); - - wp_enqueue_script( 'fido-u2f-login' ); - - ?> -

- - ID, self::AUTH_DATA_USER_META_KEY, true ); - - $response = json_decode( stripslashes( $_REQUEST['u2f_response'] ) ); - - $keys = self::get_security_keys( $user->ID ); - - try { - $reg = self::$u2f->doAuthenticate( $requests, $keys, $response ); - - $reg->last_used = time(); - - self::update_security_key( $user->ID, $reg ); - - return true; - } catch ( Exception $e ) { - return false; - } - } - - /** - * Whether this Two Factor provider is configured and available for the user specified. - * - * @since 0.1-dev - * - * @param WP_User $user WP_User object of the logged-in user. - * @return boolean - */ - public function is_available_for_user( $user ) { - return (bool) self::get_security_keys( $user->ID ); - } - - /** - * Inserts markup at the end of the user profile field for this provider. - * - * @since 0.1-dev - * - * @param WP_User $user WP_User object of the logged-in user. - */ - public function user_options( $user ) { - ?> -

- -

- keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - || ! property_exists( $register, 'publicKey' ) || empty( $register->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - || ! property_exists( $register, 'certificate' ) || empty( $register->certificate ) - || ! property_exists( $register, 'counter' ) || ( -1 > $register->counter ) - ) { - return false; - } - - $register = array( - 'keyHandle' => $register->keyHandle, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - 'publicKey' => $register->publicKey, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - 'certificate' => $register->certificate, - 'counter' => $register->counter, - ); - - $register['name'] = __( 'New Security Key', 'two-factor' ); - $register['added'] = time(); - $register['last_used'] = $register['added']; - - return add_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, $register ); - } - - /** - * Retrieve registered security keys for a user. - * - * @since 0.1-dev - * - * @param int $user_id User ID. - * @return array|bool Array of keys on success, false on failure. - */ - public static function get_security_keys( $user_id ) { - if ( ! is_numeric( $user_id ) ) { - return false; - } - - $keys = get_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY ); - if ( $keys ) { - foreach ( $keys as &$key ) { - $key = (object) $key; - } - unset( $key ); - } - - return $keys; - } - - /** - * Update registered security key. - * - * Use the $prev_value parameter to differentiate between meta fields with the - * same key and user ID. - * - * If the meta field for the user does not exist, it will be added. - * - * @since 0.1-dev - * - * @param int $user_id User ID. - * @param object $data The data of registered security key. - * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure. - */ - public static function update_security_key( $user_id, $data ) { - if ( ! is_numeric( $user_id ) ) { - return false; - } - - if ( - ! is_object( $data ) - || ! property_exists( $data, 'keyHandle' ) || empty( $data->keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - || ! property_exists( $data, 'publicKey' ) || empty( $data->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - || ! property_exists( $data, 'certificate' ) || empty( $data->certificate ) - || ! property_exists( $data, 'counter' ) || ( -1 > $data->counter ) - ) { - return false; - } - - $keys = self::get_security_keys( $user_id ); - if ( $keys ) { - foreach ( $keys as $key ) { - if ( $key->keyHandle === $data->keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - return update_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, (array) $data, (array) $key ); - } - } - } - - return self::add_security_key( $user_id, $data ); - } - - /** - * Remove registered security key matching criteria from a user. - * - * @since 0.1-dev - * - * @param int $user_id User ID. - * @param string $keyHandle Optional. Key handle. - * @return bool True on success, false on failure. - */ - public static function delete_security_key( $user_id, $keyHandle = null ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase - global $wpdb; - - if ( ! is_numeric( $user_id ) ) { - return false; - } - - $user_id = absint( $user_id ); - if ( ! $user_id ) { - return false; - } - - $keyHandle = wp_unslash( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase - $keyHandle = maybe_serialize( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase - - $query = $wpdb->prepare( "SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id = %d", self::REGISTERED_KEY_USER_META_KEY, $user_id ); - - if ( $keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase - $key_handle_lookup = sprintf( ':"%s";s:', $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase - - $query .= $wpdb->prepare( - ' AND meta_value LIKE %s', - '%' . $wpdb->esc_like( $key_handle_lookup ) . '%' - ); - } - - $meta_ids = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - if ( ! count( $meta_ids ) ) { - return false; - } - - foreach ( $meta_ids as $meta_id ) { - delete_metadata_by_mid( 'user', $meta_id ); - } - - return true; - } -} diff --git a/providers/css/fido-u2f-admin.css b/providers/css/fido-u2f-admin.css deleted file mode 100644 index 96ca78aa..00000000 --- a/providers/css/fido-u2f-admin.css +++ /dev/null @@ -1,12 +0,0 @@ -#security-keys-section .wp-list-table { - margin-bottom: 2em; -} - -#security-keys-section .register-security-key .spinner { - float: none; -} - -#security-keys-section .security-key-status { - vertical-align: middle; - font-style: italic; -} diff --git a/providers/js/fido-u2f-admin-inline-edit.js b/providers/js/fido-u2f-admin-inline-edit.js deleted file mode 100644 index b7b0123e..00000000 --- a/providers/js/fido-u2f-admin-inline-edit.js +++ /dev/null @@ -1,150 +0,0 @@ -/* global window, document, jQuery, inlineEditL10n, ajaxurl */ -var inlineEditKey; - -( function( $ ) { - inlineEditKey = { - - init: function() { - var t = this, - row = $( '#security-keys-section #inline-edit' ); - - t.what = '#key-'; - - $( '#security-keys-section #the-list' ).on( 'click', 'a.editinline', function() { - inlineEditKey.edit( this ); - return false; - } ); - - // Prepare the edit row. - row.keyup( function( event ) { - if ( 27 === event.which ) { - return inlineEditKey.revert(); - } - } ); - - $( 'a.cancel', row ).click( function() { - return inlineEditKey.revert(); - } ); - - $( 'a.save', row ).click( function() { - return inlineEditKey.save( this ); - } ); - - $( 'input, select', row ).keydown( function( event ) { - if ( 13 === event.which ) { - return inlineEditKey.save( this ); - } - } ); - }, - - toggle: function( el ) { - var t = this; - - if ( 'none' === $( t.what + t.getId( el ) ).css( 'display' ) ) { - t.revert(); - } else { - t.edit( el ); - } - }, - - edit: function( id ) { - var editRow, rowData, val, - t = this; - t.revert(); - - if ( 'object' === typeof id ) { - id = t.getId( id ); - } - - editRow = $( '#inline-edit' ).clone( true ); - rowData = $( '#inline_' + id ); - - $( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '#security-keys-section .widefat thead' ).length ); - - $( t.what + id ).hide().after( editRow ).after( '' ); - - val = $( '.name', rowData ); - val.find( 'img' ).replaceWith( function() { - return this.alt; - } ); - val = val.text(); - $( ':input[name="name"]', editRow ).val( val ); - - $( editRow ).attr( 'id', 'edit-' + id ).addClass( 'inline-editor' ).show(); - $( '.ptitle', editRow ).eq( 0 ).focus(); - - return false; - }, - - save: function( id ) { - var params, fields; - - if ( 'object' === typeof id ) { - id = this.getId( id ); - } - - $( '#security-keys-section table.widefat .spinner' ).addClass( 'is-active' ); - - params = { - action: 'inline-save-key', - keyHandle: id, - user_id: window.u2fL10n.user_id - }; - - fields = $( '#edit-' + id ).find( ':input' ).serialize(); - params = fields + '&' + $.param( params ); - - // Make ajax request. - $.post( ajaxurl, params, - function( r ) { - var row, newID; - $( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' ); - - if ( r ) { - if ( -1 !== r.indexOf( '' )[0].submit.call( $( '#your-profile' )[0] ); - } ); - } ); -}( jQuery ) ); diff --git a/providers/js/fido-u2f-login.js b/providers/js/fido-u2f-login.js deleted file mode 100644 index 28295307..00000000 --- a/providers/js/fido-u2f-login.js +++ /dev/null @@ -1,16 +0,0 @@ -/* global window, u2f, u2fL10n, jQuery */ -( function( $ ) { - if ( ! window.u2fL10n ) { - window.console.error( 'u2fL10n is not defined' ); - return; - } - - u2f.sign( u2fL10n.request[0].appId, u2fL10n.request[0].challenge, u2fL10n.request, function( data ) { - if ( data.errorCode ) { - window.console.error( 'Registration Failed', data.errorCode ); - } else { - $( '#u2f_response' ).val( JSON.stringify( data ) ); - $( '#loginform' ).submit(); - } - } ); -}( jQuery ) ); diff --git a/readme.txt b/readme.txt index 9bd7f6ef..099cfa0f 100644 --- a/readme.txt +++ b/readme.txt @@ -1,12 +1,12 @@ === Two-Factor === Contributors: georgestephanis, valendesigns, stevenkword, extendwings, sgrant, aaroncampbell, johnbillion, stevegrunwell, netweb, kasparsd, alihusnainarshad, passoniate -Tags: two factor, two step, authentication, login, totp, fido u2f, u2f, email, backup codes, 2fa, yubikey +Tags: two factor, two step, authentication, login, totp, email, backup codes, 2fa, yubikey Requires at least: 4.3 Tested up to: 5.9 Requires PHP: 5.6 Stable tag: trunk -Enable Two-Factor Authentication using time-based one-time passwords (OTP, Google Authenticator), Universal 2nd Factor (FIDO U2F, YubiKey), email and backup verification codes. +Enable Two-Factor Authentication using time-based one-time passwords (OTP, Google Authenticator), email and backup verification codes. == Description == @@ -14,7 +14,6 @@ Use the "Two-Factor Options" section under "Users" → "Your Profile" to enable - Email codes - Time Based One-Time Passwords (TOTP) -- FIDO Universal 2nd Factor (U2F) - Backup Codes - Dummy Method (only for testing purposes) @@ -32,7 +31,6 @@ Here is a list of action and filter hooks provided by the plugin: == Screenshots == 1. Two-factor options under User Profile. -2. U2F Security Keys section under User Profile. 3. Email Code Authentication during WordPress Login. == Get Involved == diff --git a/tests/providers/class-two-factor-fido-u2f.php b/tests/providers/class-two-factor-fido-u2f.php deleted file mode 100644 index 833e2e83..00000000 --- a/tests/providers/class-two-factor-fido-u2f.php +++ /dev/null @@ -1,221 +0,0 @@ -u2f = new u2flib_server\U2F( 'http://demo.example.com' ); - - $this->provider = Two_Factor_FIDO_U2F::get_instance(); - } catch ( Exception $e ) { - $this->markTestSkipped( 'Could not create U2F provider!' ); - } - } - - /** - * Verify the label value. - */ - public function test_get_label() { - $this->assertContains( 'FIDO U2F Security Keys', $this->provider->get_label() ); - } - - /** - * Verify adding a security key. - */ - public function test_add_security_key() { - $req = json_decode( '{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}' ); - $resp = json_decode( '{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }' ); - $reg = $this->u2f->doRegister( $req, $resp ); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible( true ); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible( true ); - - $user_id = $this->factory->user->create(); - $this->assertInternalType( 'int', $add_method->invoke( $this->provider, $user_id, $reg ) ); - - $delete_method->invoke( $this->provider, $user_id ); - } - - /** - * Verify adding a second security key. - */ - public function test_add_security_key2() { - $reg = json_decode( '{"keyHandle":"j_k-SThUkpALxdWPS8PjBzxaNUMj3WQIGz1rHIDMRnZCMr26C7xWItTRgiRqeqPVMPgSC6WvBJcKgqNNcReDAw","publicKey":"BBLL5S5hWjGRZzpw4kZnDMbAVTdWtlBFaCt5Fsn7sPWAXdInZpVdk\/bGNVfm43+uUmB2w6u90lf57PXQghhbF4I=","certificate":"MIICLjCCARigAwIBAgIECmML\/zALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCkxJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDE3NDI2MzI5NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKQjZF26iyPtbNnl5IuTKs\/fRWTHVzHxz1IHRRBrSbqWD60PCqUJPe4zkIRFqBa4NnzdhVcS80nlZuY3ANQm0J+jJjAkMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4yMAsGCSqGSIb3DQEBCwOCAQEAZTmwMqHPxEjSB64Umwq2tGDKplAcEzrwmg6kgS8KPkJKXKSu9T1H6XBM9+LAE9cN48oUirFFmDIlTbZRXU2Vm2qO9OdrSVFY+qdbF9oti8CKAmPHuJZSW6ii7qNE59dHKUaP4lDYpnhRDqttWSUalh2LPDJQUpO9bsJPkgNZAhBUQMYZXL\/MQZLRYkX+ld7llTNOX5u7n\/4Y5EMr+lqOyVVC9lQ6JP6xoa9q6Zp9+Y9ZmLCecrrcuH6+pLDgAzPcc8qxhC2OR1B0ZSpI9RBgcT0KqnVE0tq1KEDeokPqF3MgmDRkJ++\/a2pV0wAYfPC3tC57BtBdH\/UXEB8xZVFhtA==","counter":-1}' ); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible( true ); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible( true ); - - $user_id = $this->factory->user->create(); - $this->assertInternalType( 'int', $add_method->invoke( $this->provider, $user_id, $reg ) ); - - $delete_method->invoke( $this->provider, $user_id ); - } - - /** - * Verify retrieving security keys. - */ - public function test_get_security_keys() { - $reg = json_decode( '{"keyHandle":"j_k-SThUkpALxdWPS8PjBzxaNUMj3WQIGz1rHIDMRnZCMr26C7xWItTRgiRqeqPVMPgSC6WvBJcKgqNNcReDAw","publicKey":"BBLL5S5hWjGRZzpw4kZnDMbAVTdWtlBFaCt5Fsn7sPWAXdInZpVdk\/bGNVfm43+uUmB2w6u90lf57PXQghhbF4I=","certificate":"MIICLjCCARigAwIBAgIECmML\/zALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCkxJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDE3NDI2MzI5NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKQjZF26iyPtbNnl5IuTKs\/fRWTHVzHxz1IHRRBrSbqWD60PCqUJPe4zkIRFqBa4NnzdhVcS80nlZuY3ANQm0J+jJjAkMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4yMAsGCSqGSIb3DQEBCwOCAQEAZTmwMqHPxEjSB64Umwq2tGDKplAcEzrwmg6kgS8KPkJKXKSu9T1H6XBM9+LAE9cN48oUirFFmDIlTbZRXU2Vm2qO9OdrSVFY+qdbF9oti8CKAmPHuJZSW6ii7qNE59dHKUaP4lDYpnhRDqttWSUalh2LPDJQUpO9bsJPkgNZAhBUQMYZXL\/MQZLRYkX+ld7llTNOX5u7n\/4Y5EMr+lqOyVVC9lQ6JP6xoa9q6Zp9+Y9ZmLCecrrcuH6+pLDgAzPcc8qxhC2OR1B0ZSpI9RBgcT0KqnVE0tq1KEDeokPqF3MgmDRkJ++\/a2pV0wAYfPC3tC57BtBdH\/UXEB8xZVFhtA==","counter":-1}' ); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible( true ); - $get_method = new ReflectionMethod( $this->provider, 'get_security_keys' ); - $get_method->setAccessible( true ); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible( true ); - - $user_id = $this->factory->user->create(); - - $add_method->invoke( $this->provider, $user_id, $reg ); - $actual = $get_method->invoke( $this->provider, $user_id ); - - $this->assertEquals( $reg->keyHandle, $actual[0]->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $this->assertEquals( $reg->publicKey, $actual[0]->publicKey ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $this->assertEquals( $reg->certificate, $actual[0]->certificate ); - $this->assertEquals( $reg->counter, $actual[0]->counter ); - - $delete_method->invoke( $this->provider, $user_id ); - } - - /** - * Verify updating a security key. - */ - public function test_update_security_key() { - $reqs = array( json_decode( '{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}' ) ); - $regs = array( json_decode( '{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}' ) ); - $resp = json_decode( '{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }' ); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible( true ); - $get_method = new ReflectionMethod( $this->provider, 'get_security_keys' ); - $get_method->setAccessible( true ); - $update_method = new ReflectionMethod( $this->provider, 'update_security_key' ); - $update_method->setAccessible( true ); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible( true ); - - $user_id = $this->factory->user->create(); - $add_method->invoke( $this->provider, $user_id, $regs[0] ); - - $data = $this->u2f->doAuthenticate( $reqs, $regs, $resp ); - - $this->assertEquals( true, $update_method->invoke( $this->provider, $user_id, $data ) ); - - $meta = $get_method->invoke( $this->provider, $user_id ); - $this->assertEquals( $data->keyHandle, $meta[0]->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $this->assertEquals( $data->publicKey, $meta[0]->publicKey ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $this->assertEquals( $data->certificate, $meta[0]->certificate ); - $this->assertEquals( $data->counter, $meta[0]->counter ); - $this->assertEquals( 4, $meta[0]->counter ); - - $delete_method->invoke( $this->provider, $user_id ); - } - - /** - * Verify updating a second security key. - */ - public function test_update_security_key2() { - $reqs = array( json_decode( '{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}' ) ); - $regs = array( json_decode( '{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}' ) ); - $resp = json_decode( '{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }' ); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible( true ); - $get_method = new ReflectionMethod( $this->provider, 'get_security_keys' ); - $get_method->setAccessible( true ); - $update_method = new ReflectionMethod( $this->provider, 'update_security_key' ); - $update_method->setAccessible( true ); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible( true ); - - $user_id = $this->factory->user->create(); - - $data = $this->u2f->doAuthenticate( $reqs, $regs, $resp ); - - $this->assertInternalType( 'int', $update_method->invoke( $this->provider, $user_id, $data ) ); - - $meta = $get_method->invoke( $this->provider, $user_id ); - $this->assertEquals( $data->keyHandle, $meta[0]->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $this->assertEquals( $data->publicKey, $meta[0]->publicKey ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $this->assertEquals( $data->certificate, $meta[0]->certificate ); - $this->assertEquals( $data->counter, $meta[0]->counter ); - $this->assertEquals( 4, $meta[0]->counter ); - - $delete_method->invoke( $this->provider, $user_id ); - } - - /** - * Verify removing a security key. - */ - public function test_delete_security_key() { - $get_method = new ReflectionMethod( $this->provider, 'get_security_keys' ); - $get_method->setAccessible( true ); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible( true ); - - $user_id = $this->factory->user->create(); - $delete_method->invoke( $this->provider, $user_id ); - - $this->assertEquals( array(), $get_method->invoke( $this->provider, $user_id ) ); - } - - /** - * Verify removing a second security key. - */ - public function test_delete_security_key2() { - $req = json_decode( '{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}' ); - $resp = json_decode( '{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }' ); - $reg = $this->u2f->doRegister( $req, $resp ); - - $add_method = new ReflectionMethod( $this->provider, 'add_security_key' ); - $add_method->setAccessible( true ); - $get_method = new ReflectionMethod( $this->provider, 'get_security_keys' ); - $get_method->setAccessible( true ); - $delete_method = new ReflectionMethod( $this->provider, 'delete_security_key' ); - $delete_method->setAccessible( true ); - - $user_id = $this->factory->user->create(); - $add_method->invoke( $this->provider, $user_id, $reg ); - $delete_method->invoke( $this->provider, $user_id ); - - $this->assertEquals( array(), $get_method->invoke( $this->provider, $user_id ) ); - } -} diff --git a/two-factor.php b/two-factor.php index 365b12a9..dfbefc5a 100644 --- a/two-factor.php +++ b/two-factor.php @@ -10,7 +10,7 @@ * @wordpress-plugin * Plugin Name: Two Factor * Plugin URI: https://wordpress.org/plugins/two-factor/ - * Description: Two-Factor Authentication using time-based one-time passwords, Universal 2nd Factor (FIDO U2F), email and backup verification codes. + * Description: Two-Factor Authentication using time-based one-time passwords, email, and backup verification codes. * Author: Plugin Contributors * Version: 0.7.1 * Author URI: https://github.com/wordpress/two-factor/graphs/contributors