installed plugin `Two Factor` version 0.7.3

This commit is contained in:
KawaiiPunk 2022-10-27 11:23:01 +00:00 committed by Gitium
parent a626bfa106
commit e48f0b2253
30 changed files with 5784 additions and 0 deletions

View File

@ -0,0 +1,280 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">
<g fill="none" fill-rule="evenodd">
<path fill="#CCC" d="M98 150a60 60 0 1 1 60 0v60a8 8 0 0 1-8 8h-44a8 8 0 0 1-8-8v-60z"/>
<path fill="#0073AA" d="M116 132a36 36 0 1 1 24 0v64.7a4 4 0 0 1-4 4h-16a4 4 0 0 1-4-4v-64-.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,55 @@
<?php
/**
* A compatibility layer for some of the most popular plugins.
*
* @package Two_Factor
*/
/**
* A compatibility layer for some of the most popular plugins.
*
* Should be used with care because ideally we wouldn't need
* any integration specific code for this plugin. Everything should
* be handled through clever use of hooks and best practices.
*/
class Two_Factor_Compat {
/**
* Initialize all the custom hooks as necessary.
*
* @return void
*/
public function init() {
/**
* Jetpack
*
* @see https://wordpress.org/plugins/jetpack/
*/
add_filter( 'two_factor_rememberme', array( $this, 'jetpack_rememberme' ) );
}
/**
* Jetpack single sign-on wants long-lived sessions for users.
*
* @param boolean $rememberme Current state of the "remember me" toggle.
*
* @return boolean
*/
public function jetpack_rememberme( $rememberme ) {
$action = filter_input( INPUT_GET, 'action', FILTER_CALLBACK, array( 'options' => 'sanitize_key' ) );
if ( 'jetpack-sso' === $action && $this->jetpack_is_sso_active() ) {
return true;
}
return $rememberme;
}
/**
* Helper to detect the presence of the active SSO module.
*
* @return boolean
*/
public function jetpack_is_sso_active() {
return ( method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'sso' ) );
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,748 @@
//Copyright 2014-2015 Google Inc. All rights reserved.
//Use of this source code is governed by a BSD-style
//license that can be found in the LICENSE file or at
//https://developers.google.com/open-source/licenses/bsd
/**
* @fileoverview The U2F api.
*/
'use strict';
/**
* Namespace for the U2F api.
* @type {Object}
*/
var u2f = u2f || {};
/**
* FIDO U2F Javascript API Version
* @number
*/
var js_api_version;
/**
* The U2F extension id
* @const {string}
*/
// The Chrome packaged app extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the package Chrome app and does not require installing the U2F Chrome extension.
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
// The U2F Chrome extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the U2F Chrome extension to authenticate.
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
/**
* Message types for 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.Transport>}
*/
u2f.Transports;
/**
* Data object for a single sign request.
* @typedef {{
* version: string,
* challenge: string,
* keyHandle: string,
* appId: string
* }}
*/
u2f.SignRequest;
/**
* Data object for a sign response.
* @typedef {{
* keyHandle: string,
* signatureData: string,
* clientData: string
* }}
*/
u2f.SignResponse;
/**
* Data object for a registration request.
* @typedef {{
* version: string,
* challenge: string
* }}
*/
u2f.RegisterRequest;
/**
* Data object for a registration response.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: Transports,
* appId: string
* }}
*/
u2f.RegisterResponse;
/**
* Data object for a registered key.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: ?Transports,
* appId: ?string
* }}
*/
u2f.RegisteredKey;
/**
* Data object for a get API register response.
* @typedef {{
* js_api_version: number
* }}
*/
u2f.GetJsApiVersionResponse;
//Low level MessagePort API support
/**
* Sets up a MessagePort to the U2F extension using the
* available mechanisms.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
*/
u2f.getMessagePort = function(callback) {
if (typeof chrome != 'undefined' && chrome.runtime) {
// The actual message here does not matter, but we need to get a reply
// for the callback to run. Thus, send an empty signature request
// in order to get a failure response.
var msg = {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: []
};
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
if (!chrome.runtime.lastError) {
// We are on a whitelisted origin and can talk directly
// with the extension.
u2f.getChromeRuntimePort_(callback);
} else {
// chrome.runtime was available, but we couldn't message
// the extension directly, use iframe
u2f.getIframePort_(callback);
}
});
} else if (u2f.isAndroidChrome_()) {
u2f.getAuthenticatorPort_(callback);
} else if (u2f.isIosChrome_()) {
u2f.getIosPort_(callback);
} else {
// chrome.runtime was not available at all, which is normal
// when this origin doesn't have access to any extensions.
u2f.getIframePort_(callback);
}
};
/**
* Detect chrome running on android based on the browser's useragent.
* @private
*/
u2f.isAndroidChrome_ = function() {
var userAgent = navigator.userAgent;
return userAgent.indexOf('Chrome') != -1 &&
userAgent.indexOf('Android') != -1;
};
/**
* Detect chrome running on iOS based on the browser's platform.
* @private
*/
u2f.isIosChrome_ = function() {
return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1;
};
/**
* Connects directly to the extension via chrome.runtime.connect.
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
* @private
*/
u2f.getChromeRuntimePort_ = function(callback) {
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
{'includeTlsChannelId': true});
setTimeout(function() {
callback(new u2f.WrappedChromeRuntimePort_(port));
}, 0);
};
/**
* Return a 'port' abstraction to the Authenticator app.
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
* @private
*/
u2f.getAuthenticatorPort_ = function(callback) {
setTimeout(function() {
callback(new u2f.WrappedAuthenticatorPort_());
}, 0);
};
/**
* Return a 'port' abstraction to the iOS client app.
* @param {function(u2f.WrappedIosPort_)} callback
* @private
*/
u2f.getIosPort_ = function(callback) {
setTimeout(function() {
callback(new u2f.WrappedIosPort_());
}, 0);
};
/**
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
* @param {Port} port
* @constructor
* @private
*/
u2f.WrappedChromeRuntimePort_ = function(port) {
this.port_ = port;
};
/**
* Format and return a sign request compliant with the JS API version supported by the extension.
* @param {Array<u2f.SignRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatSignRequest_ =
function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API.
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: challenge,
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: signRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API.
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
appId: appId,
challenge: challenge,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Format and return a register request compliant with the JS API version supported by the extension..
* @param {Array<u2f.SignRequest>} signRequests
* @param {Array<u2f.RegisterRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatRegisterRequest_ =
function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API.
for (var i = 0; i < registerRequests.length; i++) {
registerRequests[i].appId = appId;
}
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: registerRequests[0],
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
signRequests: signRequests,
registerRequests: registerRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API.
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
appId: appId,
registerRequests: registerRequests,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Posts a message on the underlying channel.
* @param {Object} message
*/
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
this.port_.postMessage(message);
};
/**
* Emulates the HTML 5 addEventListener interface. Works only for the
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
function(eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message' || name == 'onmessage') {
this.port_.onMessage.addListener(function(message) {
// Emulate a minimal MessageEvent object.
handler({'data': message});
});
} else {
console.error('WrappedChromeRuntimePort only supports onMessage');
}
};
/**
* Wrap the Authenticator app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedAuthenticatorPort_ = function() {
this.requestId_ = -1;
this.requestObject_ = null;
}
/**
* Launch the Authenticator intent.
* @param {Object} message
*/
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
var intentUrl =
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
';end';
document.location = intentUrl;
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
return "WrappedAuthenticatorPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message') {
var self = this;
/* Register a callback to that executes when
* chrome injects the response. */
window.addEventListener(
'message', self.onRequestUpdate_.bind(self, handler), false);
} else {
console.error('WrappedAuthenticatorPort only supports message');
}
};
/**
* Callback invoked when a response is received from the Authenticator.
* @param function({data: Object}) callback
* @param {Object} message message Object
*/
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
function(callback, message) {
var messageObject = JSON.parse(message.data);
var intentUrl = messageObject['intentURL'];
var errorCode = messageObject['errorCode'];
var responseObject = null;
if (messageObject.hasOwnProperty('data')) {
responseObject = /** @type {Object} */ (
JSON.parse(messageObject['data']));
}
callback({'data': responseObject});
};
/**
* Base URL for intents to Authenticator.
* @const
* @private
*/
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
/**
* Wrap the iOS client app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedIosPort_ = function() {};
/**
* Launch the iOS client app request
* @param {Object} message
*/
u2f.WrappedIosPort_.prototype.postMessage = function(message) {
var str = JSON.stringify(message);
var url = "u2f://auth?" + encodeURI(str);
location.replace(url);
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedIosPort_.prototype.getPortType = function() {
return "WrappedIosPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
var name = eventName.toLowerCase();
if (name !== 'message') {
console.error('WrappedIosPort only supports message');
}
};
/**
* Sets up an embedded trampoline iframe, sourced from the extension.
* @param {function(MessagePort)} callback
* @private
*/
u2f.getIframePort_ = function(callback) {
// Create the iframe
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
var iframe = document.createElement('iframe');
iframe.src = iframeOrigin + '/u2f-comms.html';
iframe.setAttribute('style', 'display:none');
document.body.appendChild(iframe);
var channel = new MessageChannel();
var ready = function(message) {
if (message.data == 'ready') {
channel.port1.removeEventListener('message', ready);
callback(channel.port1);
} else {
console.error('First event on iframe port was not "ready"');
}
};
channel.port1.addEventListener('message', ready);
channel.port1.start();
iframe.addEventListener('load', function() {
// Deliver the port to the iframe and initialize
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
});
};
//High-level JS API
/**
* Default extension response timeout in seconds.
* @const
*/
u2f.EXTENSION_TIMEOUT_SEC = 30;
/**
* A singleton instance for a MessagePort to the extension.
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
* @private
*/
u2f.port_ = null;
/**
* Callbacks waiting for a port
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
* @private
*/
u2f.waitingForPort_ = [];
/**
* A counter for requestIds.
* @type {number}
* @private
*/
u2f.reqCounter_ = 0;
/**
* A map from requestIds to client callbacks
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
* |function((u2f.Error|u2f.SignResponse)))>}
* @private
*/
u2f.callbackMap_ = {};
/**
* Creates or retrieves the MessagePort singleton to use.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
* @private
*/
u2f.getPortSingleton_ = function(callback) {
if (u2f.port_) {
callback(u2f.port_);
} else {
if (u2f.waitingForPort_.length == 0) {
u2f.getMessagePort(function(port) {
u2f.port_ = port;
u2f.port_.addEventListener('message',
/** @type {function(Event)} */ (u2f.responseHandler_));
// Careful, here be async callbacks. Maybe.
while (u2f.waitingForPort_.length)
u2f.waitingForPort_.shift()(u2f.port_);
});
}
u2f.waitingForPort_.push(callback);
}
};
/**
* Handles response messages from the extension.
* @param {MessageEvent.<u2f.Response>} message
* @private
*/
u2f.responseHandler_ = function(message) {
var response = message.data;
var reqId = response['requestId'];
if (!reqId || !u2f.callbackMap_[reqId]) {
console.error('Unknown or missing requestId in response.');
return;
}
var cb = u2f.callbackMap_[reqId];
delete u2f.callbackMap_[reqId];
cb(response['responseData']);
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the sign request.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual sign request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
console.log("Extension JS API Version: ", js_api_version);
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual sign request in the supported API version.
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
}
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the register request.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual register request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
console.log("Extension JS API Version: ", js_api_version);
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual register request in the supported API version.
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
}
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatRegisterRequest_(
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches a message to the extension to find out the supported
* JS API version.
* If the user is on a mobile phone and is thus using Google Authenticator instead
* of the Chrome extension, don't send the request and simply return 0.
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
// If we are using Android Google Authenticator or iOS client app,
// do not fire an intent to ask which JS API version to use.
if (port.getPortType) {
var apiVersion;
switch (port.getPortType()) {
case 'WrappedIosPort_':
case 'WrappedAuthenticatorPort_':
apiVersion = 1.1;
break;
default:
apiVersion = 0;
break;
}
callback({ 'js_api_version': apiVersion });
return;
}
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var req = {
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
requestId: reqId
};
port.postMessage(req);
});
};

View File

@ -0,0 +1,507 @@
<?php
/* Copyright (c) 2014 Yubico AB
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace u2flib_server;
/** Constant for the version of the u2f protocol */
const U2F_VERSION = "U2F_V2";
/** Error for the authentication message not matching any outstanding
* authentication request */
const ERR_NO_MATCHING_REQUEST = 1;
/** Error for the authentication message not matching any registration */
const ERR_NO_MATCHING_REGISTRATION = 2;
/** Error for the signature on the authentication message not verifying with
* the correct key */
const ERR_AUTHENTICATION_FAILURE = 3;
/** Error for the challenge in the registration message not matching the
* registration challenge */
const ERR_UNMATCHED_CHALLENGE = 4;
/** Error for the attestation signature on the registration message not
* verifying */
const ERR_ATTESTATION_SIGNATURE = 5;
/** Error for the attestation verification not verifying */
const ERR_ATTESTATION_VERIFICATION = 6;
/** Error for not getting good random from the system */
const ERR_BAD_RANDOM = 7;
/** Error when the counter is lower than expected */
const ERR_COUNTER_TOO_LOW = 8;
/** Error decoding public key */
const ERR_PUBKEY_DECODE = 9;
/** Error user-agent returned error */
const ERR_BAD_UA_RETURNING = 10;
/** Error old OpenSSL version */
const ERR_OLD_OPENSSL = 11;
/** @internal */
const PUBKEY_LEN = 65;
class U2F
{
/** @var string */
private $appId;
/** @var null|string */
private $attestDir;
/** @internal */
private $FIXCERTS = array(
'349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
'1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
'6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511'
);
/**
* @param string $appId Application id for the running application
* @param string|null $attestDir Directory where trusted attestation roots may be found
* @throws Error If OpenSSL older than 1.0.0 is used
*/
public function __construct($appId, $attestDir = null)
{
if(OPENSSL_VERSION_NUMBER < 0x10000000) {
throw new Error('OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT, ERR_OLD_OPENSSL);
}
$this->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);
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* Extracted from wp-login.php since that file also loads WP core which we already have.
*/
/**
* Outputs the footer for the login page.
*
* @since 3.1.0
*
* @global bool|string $interim_login Whether interim login modal is being displayed. String 'success'
* upon successful login.
*
* @param string $input_id Which input to auto-focus.
*/
function login_footer( $input_id = '' ) {
global $interim_login;
// Don't allow interim logins to navigate away from the page.
if ( ! $interim_login ) {
?>
<p id="backtoblog">
<?php
$html_link = sprintf(
'<a href="%s">%s</a>',
esc_url( home_url( '/' ) ),
sprintf(
/* translators: %s: Site title. */
_x( '&larr; Go to %s', 'site' ),
get_bloginfo( 'title', 'display' )
)
);
/**
* Filter the "Go to site" link displayed in the login page footer.
*
* @since 5.7.0
*
* @param string $link HTML link to the home URL of the current site.
*/
echo apply_filters( 'login_site_html_link', $html_link );
?>
</p>
<?php
the_privacy_policy_link( '<div class="privacy-policy-page-link">', '</div>' );
}
?>
</div><?php // End of <div id="login">. ?>
<?php
if ( ! empty( $input_id ) ) {
?>
<script type="text/javascript">
try{document.getElementById('<?php echo $input_id; ?>').focus();}catch(e){}
if(typeof wpOnload==='function')wpOnload();
</script>
<?php
}
/**
* Fires in the login page footer.
*
* @since 3.1.0
*/
do_action( 'login_footer' );
?>
<div class="clear"></div>
</body>
</html>
<?php
}
/**
* Outputs the JavaScript to handle the form shaking on the login page.
*
* @since 3.0.0
*/
function wp_shake_js() {
?>
<script type="text/javascript">
document.querySelector('form').classList.add('shake');
</script>
<?php
}

View File

@ -0,0 +1,259 @@
<?php
/**
* Extracted from wp-login.php since that file also loads WP core which we already have.
*/
/**
* Output the login page header.
*
* @since 2.1.0
*
* @global string $error Login error message set by deprecated pluggable wp_login() function
* or plugins replacing it.
* @global bool|string $interim_login Whether interim login modal is being displayed. String 'success'
* upon successful login.
* @global string $action The action that brought the visitor to the login page.
*
* @param string $title Optional. WordPress login Page title to display in the `<title>` element.
* Default 'Log In'.
* @param string $message Optional. Message to display in header. Default empty.
* @param WP_Error $wp_error Optional. The error to pass. Default is a WP_Error instance.
*/
function login_header( $title = 'Log In', $message = '', $wp_error = null ) {
global $error, $interim_login, $action;
// Don't index any of these forms.
add_filter( 'wp_robots', 'wp_robots_sensitive_page' );
add_action( 'login_head', 'wp_strict_cross_origin_referrer' );
add_action( 'login_head', 'wp_login_viewport_meta' );
if ( ! is_wp_error( $wp_error ) ) {
$wp_error = new WP_Error();
}
// Shake it!
$shake_error_codes = array( 'empty_password', 'empty_email', 'invalid_email', 'invalidcombo', 'empty_username', 'invalid_username', 'incorrect_password', 'retrieve_password_email_failure' );
/**
* Filters the error codes array for shaking the login form.
*
* @since 3.0.0
*
* @param array $shake_error_codes Error codes that shake the login form.
*/
$shake_error_codes = apply_filters( 'shake_error_codes', $shake_error_codes );
if ( $shake_error_codes && $wp_error->has_errors() && in_array( $wp_error->get_error_code(), $shake_error_codes, true ) ) {
add_action( 'login_footer', 'wp_shake_js', 12 );
}
$login_title = get_bloginfo( 'name', 'display' );
/* translators: Login screen title. 1: Login screen name, 2: Network or site name. */
$login_title = sprintf( __( '%1$s &lsaquo; %2$s &#8212; WordPress' ), $title, $login_title );
if ( wp_is_recovery_mode() ) {
/* translators: %s: Login screen title. */
$login_title = sprintf( __( 'Recovery Mode &#8212; %s' ), $login_title );
}
/**
* Filters the title tag content for login page.
*
* @since 4.9.0
*
* @param string $login_title The page title, with extra context added.
* @param string $title The original page title.
*/
$login_title = apply_filters( 'login_title', $login_title, $title );
?><!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php bloginfo( 'charset' ); ?>" />
<title><?php echo $login_title; ?></title>
<?php
wp_enqueue_style( 'login' );
/*
* Remove all stored post data on logging out.
* This could be added by add_action('login_head'...) like wp_shake_js(),
* but maybe better if it's not removable by plugins.
*/
if ( 'loggedout' === $wp_error->get_error_code() ) {
?>
<script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script>
<?php
}
/**
* Enqueue scripts and styles for the login page.
*
* @since 3.1.0
*/
do_action( 'login_enqueue_scripts' );
/**
* Fires in the login page header after scripts are enqueued.
*
* @since 2.1.0
*/
do_action( 'login_head' );
$login_header_url = __( 'https://wordpress.org/' );
/**
* Filters link URL of the header logo above login form.
*
* @since 2.1.0
*
* @param string $login_header_url Login header logo URL.
*/
$login_header_url = apply_filters( 'login_headerurl', $login_header_url );
$login_header_title = '';
/**
* Filters the title attribute of the header logo above login form.
*
* @since 2.1.0
* @deprecated 5.2.0 Use {@see 'login_headertext'} instead.
*
* @param string $login_header_title Login header logo title attribute.
*/
$login_header_title = apply_filters_deprecated(
'login_headertitle',
array( $login_header_title ),
'5.2.0',
'login_headertext',
__( 'Usage of the title attribute on the login logo is not recommended for accessibility reasons. Use the link text instead.' )
);
$login_header_text = empty( $login_header_title ) ? __( 'Powered by WordPress' ) : $login_header_title;
/**
* Filters the link text of the header logo above the login form.
*
* @since 5.2.0
*
* @param string $login_header_text The login header logo link text.
*/
$login_header_text = apply_filters( 'login_headertext', $login_header_text );
$classes = array( 'login-action-' . $action, 'wp-core-ui' );
if ( is_rtl() ) {
$classes[] = 'rtl';
}
if ( $interim_login ) {
$classes[] = 'interim-login';
?>
<style type="text/css">html{background-color: transparent;}</style>
<?php
if ( 'success' === $interim_login ) {
$classes[] = 'interim-login-success';
}
}
$classes[] = ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) );
/**
* Filters the login page body classes.
*
* @since 3.5.0
*
* @param array $classes An array of body classes.
* @param string $action The action that brought the visitor to the login page.
*/
$classes = apply_filters( 'login_body_class', $classes, $action );
?>
</head>
<body class="login no-js <?php echo esc_attr( implode( ' ', $classes ) ); ?>">
<script type="text/javascript">
document.body.className = document.body.className.replace('no-js','js');
</script>
<?php
/**
* Fires in the login page header after the body tag is opened.
*
* @since 4.6.0
*/
do_action( 'login_header' );
?>
<div id="login">
<h1><a href="<?php echo esc_url( $login_header_url ); ?>"><?php echo $login_header_text; ?></a></h1>
<?php
/**
* Filters the message to display above the login form.
*
* @since 2.1.0
*
* @param string $message Login message text.
*/
$message = apply_filters( 'login_message', $message );
if ( ! empty( $message ) ) {
echo $message . "\n";
}
// In case a plugin uses $error rather than the $wp_errors object.
if ( ! empty( $error ) ) {
$wp_error->add( 'error', $error );
unset( $error );
}
if ( $wp_error->has_errors() ) {
$errors = '';
$messages = '';
foreach ( $wp_error->get_error_codes() as $code ) {
$severity = $wp_error->get_error_data( $code );
foreach ( $wp_error->get_error_messages( $code ) as $error_message ) {
if ( 'message' === $severity ) {
$messages .= ' ' . $error_message . "<br />\n";
} else {
$errors .= ' ' . $error_message . "<br />\n";
}
}
}
if ( ! empty( $errors ) ) {
/**
* Filters the error messages displayed above the login form.
*
* @since 2.1.0
*
* @param string $errors Login error message.
*/
echo '<div id="login_error">' . apply_filters( 'login_errors', $errors ) . "</div>\n";
}
if ( ! empty( $messages ) ) {
/**
* Filters instructional messages displayed above the login form.
*
* @since 2.5.0
*
* @param string $messages Login messages.
*/
echo '<p class="message">' . apply_filters( 'login_messages', $messages ) . "</p>\n";
}
}
} // End of login_header().
/**
* Outputs the viewport meta tag for the login page.
*
* @since 3.7.0
*/
function wp_login_viewport_meta() {
?>
<meta name="viewport" content="width=device-width" />
<?php
}

View File

@ -0,0 +1,355 @@
<?php
/**
* Class for creating a backup codes provider.
*
* @package Two_Factor
*/
/**
* Class for creating a backup codes provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_Backup_Codes extends Two_Factor_Provider {
/**
* The user meta backup codes key.
*
* @type string
*/
const BACKUP_CODES_META_KEY = '_two_factor_backup_codes';
/**
* The number backup codes.
*
* @type int
*/
const NUMBER_OF_CODES = 10;
/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @since 0.1-dev
*/
public static function get_instance() {
static $instance;
$class = __CLASS__;
if ( ! is_a( $instance, $class ) ) {
$instance = new $class();
}
return $instance;
}
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
add_action( 'wp_ajax_two_factor_backup_codes_generate', array( $this, 'ajax_generate_json' ) );
return parent::__construct();
}
/**
* Displays an admin notice when backup codes have run out.
*
* @since 0.1-dev
*/
public function admin_notices() {
$user = wp_get_current_user();
// Return if the provider is not enabled.
if ( ! in_array( __CLASS__, Two_Factor_Core::get_enabled_providers_for_user( $user->ID ), true ) ) {
return;
}
// Return if we are not out of codes.
if ( $this->is_available_for_user( $user ) ) {
return;
}
?>
<div class="error">
<p>
<span>
<?php
echo wp_kses(
sprintf(
/* translators: %s: URL for code regeneration */
__( 'Two-Factor: You are out of backup codes and need to <a href="%s">regenerate!</a>', 'two-factor' ),
esc_url( get_edit_user_link( $user->ID ) . '#two-factor-backup-codes' )
),
array( 'a' => array( 'href' => true ) )
);
?>
<span>
</p>
</div>
<?php
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label() {
return _x( 'Backup Verification Codes (Single Use)', 'Provider Label', 'two-factor' );
}
/**
* Whether this Two Factor provider is configured and codes are 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 ) {
// Does this user have available codes?
if ( 0 < self::codes_remaining_for_user( $user ) ) {
return true;
}
return false;
}
/**
* 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 ) {
$ajax_nonce = wp_create_nonce( 'two-factor-backup-codes-generate-json-' . $user->ID );
$count = self::codes_remaining_for_user( $user );
?>
<p id="two-factor-backup-codes">
<button type="button" class="button button-two-factor-backup-codes-generate button-secondary hide-if-no-js">
<?php esc_html_e( 'Generate Verification Codes', 'two-factor' ); ?>
</button>
<span class="two-factor-backup-codes-count">
<?php
echo esc_html(
sprintf(
/* translators: %s: count */
_n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ),
$count
)
);
?>
</span>
</p>
<div class="two-factor-backup-codes-wrapper" style="display:none;">
<ol class="two-factor-backup-codes-unused-codes"></ol>
<p class="description"><?php esc_html_e( 'Write these down! Once you navigate away from this page, you will not be able to view these codes again.', 'two-factor' ); ?></p>
<p>
<a class="button button-two-factor-backup-codes-download button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-download-link" download="two-factor-backup-codes.txt"><?php esc_html_e( 'Download Codes', 'two-factor' ); ?></a>
<p>
</div>
<script type="text/javascript">
( function( $ ) {
$( '.button-two-factor-backup-codes-generate' ).click( function() {
$.ajax( {
method: 'POST',
url: ajaxurl,
data: {
action: 'two_factor_backup_codes_generate',
user_id: '<?php echo esc_js( $user->ID ); ?>',
nonce: '<?php echo esc_js( $ajax_nonce ); ?>'
},
dataType: 'JSON',
success: function( response ) {
var $codesList = $( '.two-factor-backup-codes-unused-codes' );
$( '.two-factor-backup-codes-wrapper' ).show();
$codesList.html( '' );
// Append the codes.
for ( i = 0; i < response.data.codes.length; i++ ) {
$codesList.append( '<li>' + response.data.codes[ i ] + '</li>' );
}
// Update counter.
$( '.two-factor-backup-codes-count' ).html( response.data.i18n.count );
// Build the download link.
var txt_data = 'data:application/text;charset=utf-8,' + '\n';
txt_data += response.data.i18n.title.replace( /%s/g, document.domain ) + '\n\n';
for ( i = 0; i < response.data.codes.length; i++ ) {
txt_data += i + 1 + '. ' + response.data.codes[ i ] + '\n';
}
$( '#two-factor-backup-codes-download-link' ).attr( 'href', encodeURI( txt_data ) );
}
} );
} );
} )( jQuery );
</script>
<?php
}
/**
* Generates backup codes & updates the user meta.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @param array $args Optional arguments for assigning new codes.
* @return array
*/
public function generate_codes( $user, $args = '' ) {
$codes = array();
$codes_hashed = array();
// Check for arguments.
if ( isset( $args['number'] ) ) {
$num_codes = (int) $args['number'];
} else {
$num_codes = self::NUMBER_OF_CODES;
}
// Append or replace (default).
if ( isset( $args['method'] ) && 'append' === $args['method'] ) {
$codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
}
for ( $i = 0; $i < $num_codes; $i++ ) {
$code = $this->get_code();
$codes_hashed[] = wp_hash_password( $code );
$codes[] = $code;
unset( $code );
}
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $codes_hashed );
// Unhashed.
return $codes;
}
/**
* Generates a JSON object of backup codes.
*
* @since 0.1-dev
*/
public function ajax_generate_json() {
$user = get_user_by( 'id', filter_input( INPUT_POST, 'user_id', FILTER_SANITIZE_NUMBER_INT ) );
check_ajax_referer( 'two-factor-backup-codes-generate-json-' . $user->ID, 'nonce' );
// Setup the return data.
$codes = $this->generate_codes( $user );
$count = self::codes_remaining_for_user( $user );
$i18n = array(
/* translators: %s: count */
'count' => esc_html( sprintf( _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ), $count ) ),
/* translators: %s: the site's domain */
'title' => esc_html__( 'Two-Factor Backup Codes for %s', 'two-factor' ),
);
// Send the response.
wp_send_json_success(
array(
'codes' => $codes,
'i18n' => $i18n,
)
);
}
/**
* Returns the number of unused codes for the specified user
*
* @param WP_User $user WP_User object of the logged-in user.
* @return int $int The number of unused codes remaining
*/
public static function codes_remaining_for_user( $user ) {
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
return count( $backup_codes );
}
return 0;
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<p><?php esc_html_e( 'Enter a backup verification code.', 'two-factor' ); ?></p><br/>
<p>
<label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label>
<input type="tel" name="two-factor-backup-code" id="authcode" class="input" value="" size="20" pattern="[0-9]*" />
</p>
<?php
submit_button( __( 'Submit', 'two-factor' ) );
}
/**
* Validates the users input token.
*
* In this class we just return true.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
$backup_code = isset( $_POST['two-factor-backup-code'] ) ? sanitize_text_field( wp_unslash( $_POST['two-factor-backup-code'] ) ) : '';
return $this->validate_code( $user, $backup_code );
}
/**
* Validates a backup code.
*
* Backup Codes are single use and are deleted upon a successful validation.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @param int $code The backup code.
* @return boolean
*/
public function validate_code( $user, $code ) {
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
foreach ( $backup_codes as $code_index => $code_hashed ) {
if ( wp_check_password( $code, $code_hashed, $user->ID ) ) {
$this->delete_code( $user, $code_hashed );
return true;
}
}
}
return false;
}
/**
* Deletes a backup code.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @param string $code_hashed The hashed the backup code.
*/
public function delete_code( $user, $code_hashed ) {
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
// Delete the current code from the list since it's been used.
$backup_codes = array_flip( $backup_codes );
unset( $backup_codes[ $code_hashed ] );
$backup_codes = array_values( array_flip( $backup_codes ) );
// Update the backup code master list.
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $backup_codes );
}
}

View File

@ -0,0 +1,99 @@
<?php
/**
* Class for creating a dummy provider.
*
* @package Two_Factor
*/
/**
* Class for creating a dummy provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_Dummy extends Two_Factor_Provider {
/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @since 0.1-dev
*/
public static function get_instance() {
static $instance;
$class = __CLASS__;
if ( ! is_a( $instance, $class ) ) {
$instance = new $class();
}
return $instance;
}
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
return parent::__construct();
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label() {
return _x( 'Dummy Method', 'Provider Label', 'two-factor' );
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<p><?php esc_html_e( 'Are you really you?', 'two-factor' ); ?></p>
<?php
submit_button( __( 'Yup.', 'two-factor' ) );
}
/**
* Validates the users input token.
*
* In this class we just return true.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
return true;
}
/**
* 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 true;
}
/**
* 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 ) {}
}

View File

@ -0,0 +1,361 @@
<?php
/**
* Class for creating an email provider.
*
* @package Two_Factor
*/
/**
* Class for creating an email provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_Email extends Two_Factor_Provider {
/**
* The user meta token key.
*
* @var string
*/
const TOKEN_META_KEY = '_two_factor_email_token';
/**
* Store the timestamp when the token was generated.
*
* @var string
*/
const TOKEN_META_KEY_TIMESTAMP = '_two_factor_email_token_timestamp';
/**
* Name of the input field used for code resend.
*
* @var string
*/
const INPUT_NAME_RESEND_CODE = 'two-factor-email-code-resend';
/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @since 0.1-dev
*/
public static function get_instance() {
static $instance;
$class = __CLASS__;
if ( ! is_a( $instance, $class ) ) {
$instance = new $class();
}
return $instance;
}
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
return parent::__construct();
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label() {
return _x( 'Email', 'Provider Label', 'two-factor' );
}
/**
* Generate the user token.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @return string
*/
public function generate_token( $user_id ) {
$token = $this->get_code();
update_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, time() );
update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );
return $token;
}
/**
* Check if user has a valid token already.
*
* @param int $user_id User ID.
* @return boolean If user has a valid email token.
*/
public function user_has_token( $user_id ) {
$hashed_token = $this->get_user_token( $user_id );
if ( ! empty( $hashed_token ) ) {
return true;
}
return false;
}
/**
* Has the user token validity timestamp expired.
*
* @param integer $user_id User ID.
*
* @return boolean
*/
public function user_token_has_expired( $user_id ) {
$token_lifetime = $this->user_token_lifetime( $user_id );
$token_ttl = $this->user_token_ttl( $user_id );
// Invalid token lifetime is considered an expired token.
if ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl ) {
return false;
}
return true;
}
/**
* Get the lifetime of a user token in seconds.
*
* @param integer $user_id User ID.
*
* @return integer|null Return `null` if the lifetime can't be measured.
*/
public function user_token_lifetime( $user_id ) {
$timestamp = intval( get_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, true ) );
if ( ! empty( $timestamp ) ) {
return time() - $timestamp;
}
return null;
}
/**
* Return the token time-to-live for a user.
*
* @param integer $user_id User ID.
*
* @return integer
*/
public function user_token_ttl( $user_id ) {
$token_ttl = 15 * MINUTE_IN_SECONDS;
/**
* Number of seconds the token is considered valid
* after the generation.
*
* @param integer $token_ttl Token time-to-live in seconds.
* @param integer $user_id User ID.
*/
return (int) apply_filters( 'two_factor_token_ttl', $token_ttl, $user_id );
}
/**
* Get the authentication token for the user.
*
* @param int $user_id User ID.
*
* @return string|boolean User token or `false` if no token found.
*/
public function get_user_token( $user_id ) {
$hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true );
if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
return $hashed_token;
}
return false;
}
/**
* Validate the user token.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param string $token User token.
* @return boolean
*/
public function validate_token( $user_id, $token ) {
$hashed_token = $this->get_user_token( $user_id );
// Bail if token is empty or it doesn't match.
if ( empty( $hashed_token ) || ! hash_equals( wp_hash( $token ), $hashed_token ) ) {
return false;
}
if ( $this->user_token_has_expired( $user_id ) ) {
return false;
}
// Ensure the token can be used only once.
$this->delete_token( $user_id );
return true;
}
/**
* Delete the user token.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
*/
public function delete_token( $user_id ) {
delete_user_meta( $user_id, self::TOKEN_META_KEY );
}
/**
* Generate and email the user token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return bool Whether the email contents were sent successfully.
*/
public function generate_and_email_token( $user ) {
$token = $this->generate_token( $user->ID );
/* translators: %s: site name */
$subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'two-factor' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) );
/* translators: %s: token */
$message = wp_strip_all_tags( sprintf( __( 'Enter %s to log in.', 'two-factor' ), $token ) );
/**
* Filter the token email subject.
*
* @param string $subject The email subject line.
* @param int $user_id The ID of the user.
*/
$subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID );
/**
* Filter the token email message.
*
* @param string $message The email message.
* @param string $token The token.
* @param int $user_id The ID of the user.
*/
$message = apply_filters( 'two_factor_token_email_message', $message, $token, $user->ID );
return wp_mail( $user->user_email, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public function authentication_page( $user ) {
if ( ! $user ) {
return;
}
if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) {
$this->generate_and_email_token( $user );
}
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<p><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'two-factor' ); ?></p>
<p>
<label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label>
<input type="tel" name="two-factor-email-code" id="authcode" class="input" value="" size="20" />
<?php submit_button( __( 'Log In', 'two-factor' ) ); ?>
</p>
<p class="two-factor-email-resend">
<input type="submit" class="button" name="<?php echo esc_attr( self::INPUT_NAME_RESEND_CODE ); ?>" value="<?php esc_attr_e( 'Resend Code', 'two-factor' ); ?>" />
</p>
<script type="text/javascript">
setTimeout( function(){
var d;
try{
d = document.getElementById('authcode');
d.value = '';
d.focus();
} catch(e){}
}, 200);
</script>
<?php
}
/**
* Send the email code if missing or requested. Stop the authentication
* validation if a new token has been generated and sent.
*
* @param WP_USer $user WP_User object of the logged-in user.
* @return boolean
*/
public function pre_process_authentication( $user ) {
if ( isset( $user->ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) {
$this->generate_and_email_token( $user );
return true;
}
return false;
}
/**
* Validates the users input token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
if ( ! isset( $user->ID ) || ! isset( $_REQUEST['two-factor-email-code'] ) ) {
return false;
}
// Ensure there are no spaces or line breaks around the code.
$code = trim( sanitize_text_field( $_REQUEST['two-factor-email-code'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, handled by the core method already.
return $this->validate_token( $user->ID, $code );
}
/**
* 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 true;
}
/**
* 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 ) {
$email = $user->user_email;
?>
<div>
<?php
echo esc_html(
sprintf(
/* translators: %s: email address */
__( 'Authentication codes will be sent to %s.', 'two-factor' ),
$email
)
);
?>
</div>
<?php
}
}

View File

@ -0,0 +1,160 @@
<?php
/**
* Class for displaying the list of security key items.
*
* @package Two_Factor
*/
// Load the parent class if it doesn't exist.
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* Class for displaying the list of security key items.
*
* @since 0.1-dev
* @access private
*
* @package Two_Factor
*/
class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table {
/**
* Get a list of columns.
*
* @since 0.1-dev
*
* @return array
*/
public function get_columns() {
return array(
'name' => 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 = '<div class="hidden" id="inline_' . esc_attr( $item->keyHandle ) . '">'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$out .= '<div class="name">' . esc_html( $item->name ) . '</div>';
$out .= '</div>';
$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 ) {
?>
<tr id="key-<?php echo esc_attr( $item->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ?>">
<?php $this->single_row_columns( $item ); ?>
</tr>
<?php
}
/**
* Outputs the hidden row displayed when inline editing
*
* @since 0.1-dev
*/
public function inline_edit() {
?>
<table style="display: none">
<tbody id="inlineedit">
<tr id="inline-edit" class="inline-edit-row" style="display: none">
<td colspan="<?php echo esc_attr( $this->get_column_count() ); ?>" class="colspanchange">
<fieldset>
<div class="inline-edit-col">
<label>
<span class="title"><?php esc_html_e( 'Name', 'two-factor' ); ?></span>
<span class="input-text-wrap"><input type="text" name="name" class="ptitle" value="" /></span>
</label>
</div>
</fieldset>
<?php
$core_columns = array(
'name' => true,
'added' => true,
'last_used' => true,
);
list( $columns ) = $this->get_column_info();
foreach ( $columns as $column_name => $column_display_name ) {
if ( isset( $core_columns[ $column_name ] ) ) {
continue;
}
/** This action is documented in wp-admin/includes/class-wp-posts-list-table.php */
do_action( 'quick_edit_custom_box', $column_name, 'edit-security-keys' );
}
?>
<p class="inline-edit-save submit">
<a href="#inline-edit" class="cancel button-secondary alignleft"><?php esc_html_e( 'Cancel', 'two-factor' ); ?></a>
<a href="#inline-edit" class="save button-primary alignright"><?php esc_html_e( 'Update', 'two-factor' ); ?></a>
<span class="spinner"></span>
<span class="error" style="display:none;"></span>
<?php wp_nonce_field( 'keyinlineeditnonce', '_inline_edit', false ); ?>
<br class="clear" />
</p>
</td>
</tr>
</tbody>
</table>
<?php
}
}

View File

@ -0,0 +1,359 @@
<?php
/**
* Class for registering & modifying FIDO U2F security keys.
*
* @package Two_Factor
*/
/**
* Class for registering & modifying FIDO U2F security keys.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_FIDO_U2F_Admin {
/**
* The user meta register data.
*
* @type string
*/
const REGISTER_DATA_USER_META_KEY = '_two_factor_fido_u2f_register_request';
/**
* Add various hooks.
*
* @since 0.1-dev
*
* @access public
* @static
*/
public static function add_hooks() {
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
add_action( 'show_user_security_settings', array( __CLASS__, 'show_user_profile' ) );
add_action( 'personal_options_update', array( __CLASS__, 'catch_submission' ), 0 );
add_action( 'edit_user_profile_update', array( __CLASS__, 'catch_submission' ), 0 );
add_action( 'load-profile.php', array( __CLASS__, 'catch_delete_security_key' ) );
add_action( 'load-user-edit.php', array( __CLASS__, 'catch_delete_security_key' ) );
add_action( 'wp_ajax_inline-save-key', array( __CLASS__, 'wp_ajax_inline_save' ) );
}
/**
* Enqueue assets.
*
* @since 0.1-dev
*
* @access public
* @static
*
* @param string $hook Current page.
*/
public static function enqueue_assets( $hook ) {
if ( ! in_array( $hook, array( 'user-edit.php', 'profile.php' ), true ) ) {
return;
}
$user_id = Two_Factor_Core::current_user_being_edited();
if ( ! $user_id ) {
return;
}
$security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id );
// @todo Ensure that scripts don't fail because of missing u2fL10n.
try {
$data = Two_Factor_FIDO_U2F::$u2f->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 );
}
?>
<div class="security-keys" id="security-keys-section">
<h3><?php esc_html_e( 'Security Keys', 'two-factor' ); ?></h3>
<?php if ( ! is_ssl() ) : ?>
<p class="u2f-error-https">
<em><?php esc_html_e( 'U2F requires an HTTPS connection. You won\'t be able to add new security keys over HTTP.', 'two-factor' ); ?></em>
</p>
<?php endif; ?>
<div class="register-security-key">
<input type="hidden" name="do_new_security_key" id="do_new_security_key" />
<input type="hidden" name="u2f_response" id="u2f_response" />
<button type="button" class="button button-secondary" id="register_security_key"><?php echo esc_html( _x( 'Register New Key', 'security key', 'two-factor' ) ); ?></button>
<span class="spinner"></span>
<span class="security-key-status"></span>
</div>
<?php if ( $new_key ) : ?>
<div class="notice notice-success is-dismissible">
<p class="new-security-key"><?php esc_html_e( 'Your new security key registered.', 'two-factor' ); ?></p>
</div>
<?php endif; ?>
<p><a href="https://support.google.com/accounts/answer/6103523"><?php esc_html_e( 'You can find FIDO U2F Security Key devices for sale from here.', 'two-factor' ); ?></a></p>
<?php
require TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin-list-table.php';
$u2f_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table();
$u2f_list_table->items = $security_keys;
$u2f_list_table->prepare_items();
$u2f_list_table->display();
$u2f_list_table->inline_edit();
?>
</div>
<?php
}
/**
* Catch the non-ajax submission from the new form.
*
* This executes during the `personal_options_update` & `edit_user_profile_update` actions.
*
* @since 0.1-dev
*
* @access public
* @static
*
* @param int $user_id User ID.
* @return false
*/
public static function catch_submission( $user_id ) {
if ( ! empty( $_REQUEST['do_new_security_key'] ) ) {
check_admin_referer( "user_security_keys-{$user_id}", '_nonce_user_security_keys' );
try {
$response = json_decode( stripslashes( $_POST['u2f_response'] ) );
$reg = Two_Factor_FIDO_U2F::$u2f->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( '<a href="#" class="editinline">%s</a>', 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( '<a href="%1$s">%2$s</a>', 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();
}
}

View File

@ -0,0 +1,397 @@
<?php
/**
* Class for creating a FIDO Universal 2nd Factor provider.
*
* @package Two_Factor
*/
/**
* Class for creating a FIDO Universal 2nd Factor provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
/**
* U2F Library
*
* @var u2flib_server\U2F
*/
public static $u2f;
/**
* The user meta registered key.
*
* @type string
*/
const REGISTERED_KEY_USER_META_KEY = '_two_factor_fido_u2f_registered_key';
/**
* The user meta authenticate data.
*
* @type string
*/
const AUTH_DATA_USER_META_KEY = '_two_factor_fido_u2f_login_request';
/**
* Version number for the bundled assets.
*
* @var string
*/
const U2F_ASSET_VERSION = '0.2.1';
/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @return \Two_Factor_FIDO_U2F
*/
public static function get_instance() {
static $instance;
if ( ! isset( $instance ) ) {
$instance = new self();
}
return $instance;
}
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
if ( version_compare( PHP_VERSION, '5.3.0', '<' ) ) {
return;
}
require_once TWO_FACTOR_DIR . 'includes/Yubico/U2F.php';
self::$u2f = new u2flib_server\U2F( self::get_u2f_app_id() );
require_once TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin.php';
Two_Factor_FIDO_U2F_Admin::add_hooks();
// Ensure the script dependencies have been registered before they're enqueued at a later priority.
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'login_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
return parent::__construct();
}
/**
* Get the asset version number.
*
* TODO: There should be a plugin-level helper for getting the current plugin version.
*
* @return string
*/
public static function asset_version() {
return self::U2F_ASSET_VERSION;
}
/**
* Return the U2F AppId. U2F requires the AppID to use HTTPS
* and a top-level domain.
*
* @return string AppID URI
*/
public static function get_u2f_app_id() {
$url_parts = wp_parse_url( home_url() );
if ( ! empty( $url_parts['port'] ) ) {
return sprintf( 'https://%s:%d', $url_parts['host'], $url_parts['port'] );
} else {
return sprintf( 'https://%s', $url_parts['host'] );
}
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label() {
return _x( 'FIDO U2F Security Keys', 'Provider Label', 'two-factor' );
}
/**
* Register script dependencies used during login and when
* registering keys in the WP admin.
*
* @return void
*/
public static function enqueue_scripts() {
wp_register_script(
'fido-u2f-api',
plugins_url( 'includes/Google/u2f-api.js', dirname( __FILE__ ) ),
null,
self::asset_version(),
true
);
wp_register_script(
'fido-u2f-login',
plugins_url( 'js/fido-u2f-login.js', __FILE__ ),
array( 'jquery', 'fido-u2f-api' ),
self::asset_version(),
true
);
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return null
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
// U2F doesn't work without HTTPS.
if ( ! is_ssl() ) {
?>
<p><?php esc_html_e( 'U2F requires an HTTPS connection. Please use an alternative 2nd factor method.', 'two-factor' ); ?></p>
<?php
return;
}
try {
$keys = self::get_security_keys( $user->ID );
$data = self::$u2f->getAuthenticateData( $keys );
update_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, $data );
} catch ( Exception $e ) {
?>
<p><?php esc_html_e( 'An error occurred while creating authentication data.', 'two-factor' ); ?></p>
<?php
return null;
}
wp_localize_script(
'fido-u2f-login',
'u2fL10n',
array(
'request' => $data,
)
);
wp_enqueue_script( 'fido-u2f-login' );
?>
<p><?php esc_html_e( 'Now insert (and tap) your Security Key.', 'two-factor' ); ?></p>
<input type="hidden" name="u2f_response" id="u2f_response" />
<?php
}
/**
* Validates the users input token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
$requests = get_user_meta( $user->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 ) {
?>
<p>
<?php esc_html_e( 'Requires an HTTPS connection. Configure your security keys in the "Security Keys" section below.', 'two-factor' ); ?>
</p>
<?php
}
/**
* Add registered security key to a user.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param object $register The data of registered security key.
* @return int|bool Meta ID on success, false on failure.
*/
public static function add_security_key( $user_id, $register ) {
if ( ! is_numeric( $user_id ) ) {
return false;
}
if (
! is_object( $register )
|| ! property_exists( $register, 'keyHandle' ) || empty( $register->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;
}
}

View File

@ -0,0 +1,102 @@
<?php
/**
* Abstract class for creating two factor authentication providers.
*
* @package Two_Factor
*/
/**
* Abstract class for creating two factor authentication providers.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
abstract class Two_Factor_Provider {
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
return $this;
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*
* @return string
*/
abstract public function get_label();
/**
* Prints the name of the provider.
*
* @since 0.1-dev
*/
public function print_label() {
echo esc_html( $this->get_label() );
}
/**
* Prints the form that prompts the user to authenticate.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
abstract public function authentication_page( $user );
/**
* Allow providers to do extra processing before the authentication.
* Return `true` to prevent the authentication and render the
* authentication page.
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function pre_process_authentication( $user ) {
return false;
}
/**
* Validates the users input token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
abstract public function validate_authentication( $user );
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
abstract public function is_available_for_user( $user );
/**
* Generate a random eight-digit string to send out as an auth code.
*
* @since 0.1-dev
*
* @param int $length The code length.
* @param string|array $chars Valid auth code characters.
* @return string
*/
public function get_code( $length = 8, $chars = '1234567890' ) {
$code = '';
if ( is_array( $chars ) ) {
$chars = implode( '', $chars );
}
for ( $i = 0; $i < $length; $i++ ) {
$code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
}
return $code;
}
}

View File

@ -0,0 +1,578 @@
<?php
/**
* Class for creating a Time Based One-Time Password provider.
*
* @package Two_Factor
*/
/**
* Class Two_Factor_Totp
*/
class Two_Factor_Totp extends Two_Factor_Provider {
/**
* The user meta token key.
*
* @var string
*/
const SECRET_META_KEY = '_two_factor_totp_key';
/**
* The user meta token key.
*
* @var string
*/
const NOTICES_META_KEY = '_two_factor_totp_notices';
/**
* Action name for resetting the secret token.
*
* @var string
*/
const ACTION_SECRET_DELETE = 'totp-delete';
const DEFAULT_KEY_BIT_SIZE = 160;
const DEFAULT_CRYPTO = 'sha1';
const DEFAULT_DIGIT_COUNT = 6;
const DEFAULT_TIME_STEP_SEC = 30;
const DEFAULT_TIME_STEP_ALLOWANCE = 4;
/**
* Characters used in base32 encoding.
*
* @var string
*/
private static $base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* Class constructor. Sets up hooks, etc.
*
* @codeCoverageIgnore
*/
protected function __construct() {
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_two_factor_options' ) );
add_action( 'personal_options_update', array( $this, 'user_two_factor_options_update' ) );
add_action( 'edit_user_profile_update', array( $this, 'user_two_factor_options_update' ) );
add_action( 'two_factor_user_settings_action', array( $this, 'user_settings_action' ), 10, 2 );
return parent::__construct();
}
/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @codeCoverageIgnore
*/
public static function get_instance() {
static $instance;
if ( ! isset( $instance ) ) {
$instance = new self();
}
return $instance;
}
/**
* Returns the name of the provider.
*/
public function get_label() {
return _x( 'Time Based One-Time Password (TOTP)', 'Provider Label', 'two-factor' );
}
/**
* Trigger our custom user settings actions.
*
* @param integer $user_id User ID.
* @param string $action Action ID.
*
* @return void
*
* @codeCoverageIgnore
*/
public function user_settings_action( $user_id, $action ) {
if ( self::ACTION_SECRET_DELETE === $action ) {
$this->delete_user_totp_key( $user_id );
}
}
/**
* Get the URL for deleting the secret token.
*
* @param integer $user_id User ID.
*
* @return string
*
* @codeCoverageIgnore
*/
protected function get_token_delete_url_for_user( $user_id ) {
return Two_Factor_Core::get_user_update_action_url( $user_id, self::ACTION_SECRET_DELETE );
}
/**
* Display TOTP options on the user settings page.
*
* @param WP_User $user The current user being edited.
* @return false
*
* @codeCoverageIgnore
*/
public function user_two_factor_options( $user ) {
if ( ! isset( $user->ID ) ) {
return false;
}
wp_nonce_field( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options', false );
$key = $this->get_user_totp_key( $user->ID );
$this->admin_notices( $user->ID );
?>
<div id="two-factor-totp-options">
<?php
if ( empty( $key ) ) :
$key = $this->generate_key();
$site_name = get_bloginfo( 'name', 'display' );
$totp_title = apply_filters( 'two_factor_totp_title', $site_name . ':' . $user->user_login, $user );
?>
<p>
<?php esc_html_e( 'Please scan the QR code or manually enter the key, then enter an authentication code from your app in order to complete setup.', 'two-factor' ); ?>
</p>
<p>
<img src="<?php echo esc_url( $this->get_google_qr_code( $totp_title, $key, $site_name ) ); ?>" id="two-factor-totp-qrcode" />
</p>
<p>
<code><?php echo esc_html( $key ); ?></code>
</p>
<p>
<input type="hidden" name="two-factor-totp-key" value="<?php echo esc_attr( $key ); ?>" />
<label for="two-factor-totp-authcode">
<?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?>
<input type="tel" name="two-factor-totp-authcode" id="two-factor-totp-authcode" class="input" value="" size="20" pattern="[0-9]*" />
</label>
<input type="submit" class="button" name="two-factor-totp-submit" value="<?php esc_attr_e( 'Submit', 'two-factor' ); ?>" />
</p>
<?php else : ?>
<p class="success">
<?php esc_html_e( 'Secret key is configured and registered. It is not possible to view it again for security reasons.', 'two-factor' ); ?>
</p>
<p>
<a class="button" href="<?php echo esc_url( self::get_token_delete_url_for_user( $user->ID ) ); ?>"><?php esc_html_e( 'Reset Key', 'two-factor' ); ?></a>
<em class="description">
<?php esc_html_e( 'You will have to re-scan the QR code on all devices as the previous codes will stop working.', 'two-factor' ); ?>
</em>
</p>
<?php endif; ?>
</div>
<?php
}
/**
* Save the options specified in `::user_two_factor_options()`
*
* @param integer $user_id The user ID whose options are being updated.
*
* @return void
*
* @codeCoverageIgnore
*/
public function user_two_factor_options_update( $user_id ) {
$notices = array();
$errors = array();
if ( isset( $_POST['_nonce_user_two_factor_totp_options'] ) ) {
check_admin_referer( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options' );
// Validate and store a new secret key.
if ( ! empty( $_POST['two-factor-totp-authcode'] ) && ! empty( $_POST['two-factor-totp-key'] ) ) {
// Don't use filter_input() because we can't mock it during tests for now.
$authcode = filter_var( sanitize_text_field( $_POST['two-factor-totp-authcode'] ), FILTER_SANITIZE_NUMBER_INT );
$key = sanitize_text_field( $_POST['two-factor-totp-key'] );
if ( $this->is_valid_key( $key ) ) {
if ( $this->is_valid_authcode( $key, $authcode ) ) {
if ( ! $this->set_user_totp_key( $user_id, $key ) ) {
$errors[] = __( 'Unable to save Two Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'two-factor' );
}
} else {
$errors[] = __( 'Invalid Two Factor Authentication code.', 'two-factor' );
}
} else {
$errors[] = __( 'Invalid Two Factor Authentication secret key.', 'two-factor' );
}
}
if ( ! empty( $errors ) ) {
$notices['error'] = $errors;
}
if ( ! empty( $notices ) ) {
update_user_meta( $user_id, self::NOTICES_META_KEY, $notices );
}
}
}
/**
* Get the TOTP secret key for a user.
*
* @param int $user_id User ID.
*
* @return string
*/
public function get_user_totp_key( $user_id ) {
return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true );
}
/**
* Set the TOTP secret key for a user.
*
* @param int $user_id User ID.
* @param string $key TOTP secret key.
*
* @return boolean If the key was stored successfully.
*/
public function set_user_totp_key( $user_id, $key ) {
return update_user_meta( $user_id, self::SECRET_META_KEY, $key );
}
/**
* Delete the TOTP secret key for a user.
*
* @param int $user_id User ID.
*
* @return boolean If the key was deleted successfully.
*/
public function delete_user_totp_key( $user_id ) {
return delete_user_meta( $user_id, self::SECRET_META_KEY );
}
/**
* Check if the TOTP secret key has a proper format.
*
* @param string $key TOTP secret key.
*
* @return boolean
*/
public function is_valid_key( $key ) {
$check = sprintf( '/^[%s]+$/', self::$base_32_chars );
if ( 1 === preg_match( $check, $key ) ) {
return true;
}
return false;
}
/**
* Display any available admin notices.
*
* @param integer $user_id User ID.
*
* @return void
*
* @codeCoverageIgnore
*/
public function admin_notices( $user_id ) {
$notices = get_user_meta( $user_id, self::NOTICES_META_KEY, true );
if ( ! empty( $notices ) ) {
delete_user_meta( $user_id, self::NOTICES_META_KEY );
foreach ( $notices as $class => $messages ) {
?>
<div class="<?php echo esc_attr( $class ); ?>">
<?php
foreach ( $messages as $msg ) {
?>
<p>
<span><?php echo esc_html( $msg ); ?><span>
</p>
<?php
}
?>
</div>
<?php
}
}
}
/**
* Validates authentication.
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @return bool Whether the user gave a valid code
*
* @codeCoverageIgnore
*/
public function validate_authentication( $user ) {
if ( ! empty( $_REQUEST['authcode'] ) ) {
return $this->is_valid_authcode(
$this->get_user_totp_key( $user->ID ),
sanitize_text_field( $_REQUEST['authcode'] )
);
}
return false;
}
/**
* Checks if a given code is valid for a given key, allowing for a certain amount of time drift
*
* @param string $key The share secret key to use.
* @param string $authcode The code to test.
*
* @return bool Whether the code is valid within the time frame
*/
public static function is_valid_authcode( $key, $authcode ) {
/**
* Filter the maximum ticks to allow when checking valid codes.
*
* Ticks are the allowed offset from the correct time in 30 second increments,
* so the default of 4 allows codes that are two minutes to either side of server time
*
* @deprecated 0.7.0 Use {@see 'two_factor_totp_time_step_allowance'} instead.
* @param int $max_ticks Max ticks of time correction to allow. Default 4.
*/
$max_ticks = apply_filters_deprecated( 'two-factor-totp-time-step-allowance', array( self::DEFAULT_TIME_STEP_ALLOWANCE ), '0.7.0', 'two_factor_totp_time_step_allowance' );
$max_ticks = apply_filters( 'two_factor_totp_time_step_allowance', self::DEFAULT_TIME_STEP_ALLOWANCE );
// Array of all ticks to allow, sorted using absolute value to test closest match first.
$ticks = range( - $max_ticks, $max_ticks );
usort( $ticks, array( __CLASS__, 'abssort' ) );
$time = time() / self::DEFAULT_TIME_STEP_SEC;
foreach ( $ticks as $offset ) {
$log_time = $time + $offset;
if ( hash_equals(self::calc_totp( $key, $log_time ), $authcode ) ) {
return true;
}
}
return false;
}
/**
* Generates key
*
* @param int $bitsize Nume of bits to use for key.
*
* @return string $bitsize long string composed of available base32 chars.
*/
public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
$bytes = ceil( $bitsize / 8 );
$secret = wp_generate_password( $bytes, true, true );
return self::base32_encode( $secret );
}
/**
* Pack stuff
*
* @param string $value The value to be packed.
*
* @return string Binary packed string.
*/
public static function pack64( $value ) {
// 64bit mode (PHP_INT_SIZE == 8).
if ( PHP_INT_SIZE >= 8 ) {
// If we're on PHP 5.6.3+ we can use the new 64bit pack functionality.
if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) {
return pack( 'J', $value ); // phpcs:ignore PHPCompatibility.ParameterValues.NewPackFormat.NewFormatFound
}
$highmap = 0xffffffff << 32;
$higher = ( $value & $highmap ) >> 32;
} else {
/*
* 32bit PHP can't shift 32 bits like that, so we have to assume 0 for the higher
* and not pack anything beyond it's limits.
*/
$higher = 0;
}
$lowmap = 0xffffffff;
$lower = $value & $lowmap;
return pack( 'NN', $higher, $lower );
}
/**
* Calculate a valid code given the shared secret key
*
* @param string $key The shared secret key to use for calculating code.
* @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size.
* @param int $digits The number of digits in the returned code.
* @param string $hash The hash used to calculate the code.
* @param int $time_step The size of the time step.
*
* @return string The totp code
*/
public static function calc_totp( $key, $step_count = false, $digits = self::DEFAULT_DIGIT_COUNT, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
$secret = self::base32_decode( $key );
if ( false === $step_count ) {
$step_count = floor( time() / $time_step );
}
$timestamp = self::pack64( $step_count );
$hash = hash_hmac( $hash, $timestamp, $secret, true );
$offset = ord( $hash[19] ) & 0xf;
$code = (
( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) |
( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) |
( ord( $hash[ $offset + 3 ] ) & 0xff )
) % pow( 10, $digits );
return str_pad( $code, $digits, '0', STR_PAD_LEFT );
}
/**
* Uses the Google Charts API to build a QR Code for use with an otpauth url
*
* @param string $name The name to display in the Authentication app.
* @param string $key The secret key to share with the Authentication app.
* @param string $title The title to display in the Authentication app.
*
* @return string A URL to use as an img src to display the QR code
*
* @codeCoverageIgnore
*/
public static function get_google_qr_code( $name, $key, $title = null ) {
// Encode to support spaces, question marks and other characters.
$name = rawurlencode( $name );
$google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key );
if ( isset( $title ) ) {
$google_url .= urlencode( '&issuer=' . rawurlencode( $title ) );
}
return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $google_url;
}
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @return boolean
*/
public function is_available_for_user( $user ) {
// Only available if the secret key has been saved for the user.
$key = $this->get_user_totp_key( $user->ID );
return ! empty( $key );
}
/**
* Prints the form that prompts the user to authenticate.
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @codeCoverageIgnore
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<p>
<?php esc_html_e( 'Please enter the code generated by your authenticator app.', 'two-factor' ); ?>
</p>
<p>
<label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label>
<input type="tel" autocomplete="one-time-code" name="authcode" id="authcode" class="input" value="" size="20" pattern="[0-9]*" />
</p>
<script type="text/javascript">
setTimeout( function(){
var d;
try{
d = document.getElementById('authcode');
d.focus();
} catch(e){}
}, 200);
</script>
<?php
submit_button( __( 'Authenticate', 'two-factor' ) );
}
/**
* Returns a base32 encoded string.
*
* @param string $string String to be encoded using base32.
*
* @return string base32 encoded string without padding.
*/
public static function base32_encode( $string ) {
if ( empty( $string ) ) {
return '';
}
$binary_string = '';
foreach ( str_split( $string ) as $character ) {
$binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
}
$five_bit_sections = str_split( $binary_string, 5 );
$base32_string = '';
foreach ( $five_bit_sections as $five_bit_section ) {
$base32_string .= self::$base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
}
return $base32_string;
}
/**
* Decode a base32 string and return a binary representation
*
* @param string $base32_string The base 32 string to decode.
*
* @throws Exception If string contains non-base32 characters.
*
* @return string Binary representation of decoded string
*/
public static function base32_decode( $base32_string ) {
$base32_string = strtoupper( $base32_string );
if ( ! preg_match( '/^[' . self::$base_32_chars . ']+$/', $base32_string, $match ) ) {
throw new Exception( 'Invalid characters in the base32 string.' );
}
$l = strlen( $base32_string );
$n = 0;
$j = 0;
$binary = '';
for ( $i = 0; $i < $l; $i++ ) {
$n = $n << 5; // Move buffer left by 5 to make room.
$n = $n + strpos( self::$base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
$j += 5; // Keep track of number of bits in buffer.
if ( $j >= 8 ) {
$j -= 8;
$binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
}
}
return $binary;
}
/**
* Used with usort to sort an array by distance from 0
*
* @param int $a First array element.
* @param int $b Second array element.
*
* @return int -1, 0, or 1 as needed by usort
*/
private static function abssort( $a, $b ) {
$a = abs( $a );
$b = abs( $b );
if ( $a === $b ) {
return 0;
}
return ( $a < $b ) ? -1 : 1;
}
}

View File

@ -0,0 +1,12 @@
#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;
}

View File

@ -0,0 +1,150 @@
/* 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( '<tr class="hidden"></tr>' );
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( '<tr' ) ) {
$( inlineEditKey.what + id ).siblings( 'tr.hidden' ).addBack().remove();
newID = $( r ).attr( 'id' );
$( '#edit-' + id ).before( r ).remove();
if ( newID ) {
row = $( '#' + newID );
} else {
row = $( inlineEditKey.what + id );
}
row.hide().fadeIn();
} else {
$( '#edit-' + id + ' .inline-edit-save .error' ).html( r ).show();
}
} else {
$( '#edit-' + id + ' .inline-edit-save .error' ).html( inlineEditL10n.error ).show();
}
}
);
return false;
},
revert: function() {
var id = $( '#security-keys-section table.widefat tr.inline-editor' ).attr( 'id' );
if ( id ) {
$( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' );
$( '#' + id ).siblings( 'tr.hidden' ).addBack().remove();
id = id.replace( /\w+\-/, '' );
$( this.what + id ).show();
}
return false;
},
getId: function( o ) {
var id = 'TR' === o.tagName ? o.id : $( o ).parents( 'tr' ).attr( 'id' );
return id.replace( /\w+\-/, '' );
}
};
$( document ).ready( function() {
inlineEditKey.init();
} );
}( jQuery ) );

View File

@ -0,0 +1,48 @@
/* global window, u2fL10n, jQuery */
( function( $ ) {
var $button = $( '#register_security_key' );
var $statusNotice = $( '#security-keys-section .security-key-status' );
var u2fSupported = ( window.u2f && 'register' in window.u2f );
if ( ! u2fSupported ) {
$statusNotice.text( u2fL10n.text.u2f_not_supported );
}
$button.click( function() {
var registerRequest;
if ( $( this ).prop( 'disabled' ) ) {
return false;
}
$( this ).prop( 'disabled', true );
$( '.register-security-key .spinner' ).addClass( 'is-active' );
$statusNotice.text( '' );
registerRequest = {
version: u2fL10n.register.request.version,
challenge: u2fL10n.register.request.challenge
};
window.u2f.register( u2fL10n.register.request.appId, [ registerRequest ], u2fL10n.register.sigs, function( data ) {
$( '.register-security-key .spinner' ).removeClass( 'is-active' );
$button.prop( 'disabled', false );
if ( data.errorCode ) {
if ( u2fL10n.text.error_codes[ data.errorCode ] ) {
$statusNotice.text( u2fL10n.text.error_codes[ data.errorCode ] );
} else {
$statusNotice.text( u2fL10n.text.error_codes[ u2fL10n.text.error ] );
}
return false;
}
$( '#do_new_security_key' ).val( 'true' );
$( '#u2f_response' ).val( JSON.stringify( data ) );
// See: http://stackoverflow.com/questions/833032/submit-is-not-a-function-error-in-javascript
$( '<form>' )[0].submit.call( $( '#your-profile' )[0] );
} );
} );
}( jQuery ) );

View File

@ -0,0 +1,16 @@
/* 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 ) );

View File

@ -0,0 +1,44 @@
=== 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
Requires at least: 4.3
Tested up to: 6.0
Requires PHP: 5.6
Stable tag: 0.7.3
Enable Two-Factor Authentication using time-based one-time passwords (OTP, Google Authenticator), Universal 2nd Factor (FIDO U2F, YubiKey), email and backup verification codes.
== Description ==
Use the "Two-Factor Options" section under "Users" → "Your Profile" to enable and configure one or multiple two-factor authentication providers for your account:
- Email codes
- Time Based One-Time Passwords (TOTP)
- FIDO Universal 2nd Factor (U2F)
- Backup Codes
- Dummy Method (only for testing purposes)
For more history, see [this post](https://georgestephanis.wordpress.com/2013/08/14/two-cents-on-two-factor/).
= Actions & Filters =
Here is a list of action and filter hooks provided by the plugin:
- `two_factor_providers` filter overrides the available two-factor providers such as email and time-based one-time passwords. Array values are PHP classnames of the two-factor providers.
- `two_factor_enabled_providers_for_user` filter overrides the list of two-factor providers enabled for a user. First argument is an array of enabled provider classnames as values, the second argument is the user ID.
- `two_factor_user_authenticated` action which receives the logged in `WP_User` object as the first argument for determining the logged in user right after the authentication workflow.
- `two_factor_token_ttl` filter overrides the time interval in seconds that an email token is considered after generation. Accepts the time in seconds as the first argument and the ID of the `WP_User` object being authenticated.
== 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 ==
Development happens [on GitHub](https://github.com/wordpress/two-factor/).
== Changelog ==
See the [release history](https://github.com/wordpress/two-factor/releases).

View File

@ -0,0 +1,48 @@
<?php
/**
* Two Factor
*
* @package Two_Factor
* @author Plugin Contributors
* @copyright 2020 Plugin Contributors
* @license GPL-2.0-or-later
*
* @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.
* Author: Plugin Contributors
* Version: 0.7.3
* Author URI: https://github.com/wordpress/two-factor/graphs/contributors
* Network: True
* Text Domain: two-factor
*/
/**
* Shortcut constant to the path of this file.
*/
define( 'TWO_FACTOR_DIR', plugin_dir_path( __FILE__ ) );
/**
* Version of the plugin.
*/
define( 'TWO_FACTOR_VERSION', '0.7.3' );
/**
* Include the base class here, so that other plugins can also extend it.
*/
require_once TWO_FACTOR_DIR . 'providers/class-two-factor-provider.php';
/**
* Include the core that handles the common bits.
*/
require_once TWO_FACTOR_DIR . 'class-two-factor-core.php';
/**
* A compatability layer for some of the most-used plugins out there.
*/
require_once TWO_FACTOR_DIR . 'class-two-factor-compat.php';
$two_factor_compat = new Two_Factor_Compat();
Two_Factor_Core::add_hooks( $two_factor_compat );

View File

@ -0,0 +1,42 @@
.two-factor-methods-table {
background-color: #fff;
border: 1px solid #e5e5e5;
border-spacing: 0;
}
.two-factor-methods-table thead,
.two-factor-methods-table tfoot {
background: #fff;
}
.two-factor-methods-table thead th {
padding: 0.5em;
}
.two-factor-methods-table .col-primary,
.two-factor-methods-table .col-enabled {
width: 5%;
}
.two-factor-methods-table .col-name {
width: 90%;
}
.two-factor-methods-table tbody th {
text-align: center;
}
.two-factor-methods-table tbody th,
.two-factor-methods-table tbody td {
vertical-align: top;
}
.two-factor-methods-table tbody tr:nth-child(odd) {
background-color: #f9f9f9;
}
.two-factor-methods-table .two-factor-method-label {
display: block;
font-weight: 700;
}