installed plugin Two Factor version 0.7.3
				
					
				
			
							
								
								
									
										280
									
								
								wp-content/plugins/two-factor/LICENSE.md
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										
											BIN
										
									
								
								wp-content/plugins/two-factor/assets/banner-1544x500.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								wp-content/plugins/two-factor/assets/banner-772x250.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								wp-content/plugins/two-factor/assets/icon-128x128.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								wp-content/plugins/two-factor/assets/icon-256x256.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.3 KiB | 
							
								
								
									
										6
									
								
								wp-content/plugins/two-factor/assets/icon.svg
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										
											BIN
										
									
								
								wp-content/plugins/two-factor/assets/screenshot-1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 171 KiB | 
							
								
								
									
										
											BIN
										
									
								
								wp-content/plugins/two-factor/assets/screenshot-2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 160 KiB | 
							
								
								
									
										
											BIN
										
									
								
								wp-content/plugins/two-factor/assets/screenshot-3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.0 KiB | 
							
								
								
									
										55
									
								
								wp-content/plugins/two-factor/class-two-factor-compat.php
									
									
									
									
									
										Normal 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' ) ); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										1071
									
								
								wp-content/plugins/two-factor/class-two-factor-core.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										748
									
								
								wp-content/plugins/two-factor/includes/Google/u2f-api.js
									
									
									
									
									
										Normal 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); | ||||
|   }); | ||||
| }; | ||||
							
								
								
									
										507
									
								
								wp-content/plugins/two-factor/includes/Yubico/U2F.php
									
									
									
									
									
										Normal 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); | ||||
|     } | ||||
| } | ||||
| @ -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( '← 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 | ||||
| } | ||||
							
								
								
									
										259
									
								
								wp-content/plugins/two-factor/includes/function.login-header.php
									
									
									
									
									
										Normal 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 ‹ %2$s — WordPress' ), $title, $login_title ); | ||||
|  | ||||
| 	if ( wp_is_recovery_mode() ) { | ||||
| 		/* translators: %s: Login screen title. */ | ||||
| 		$login_title = sprintf( __( 'Recovery Mode — %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 | ||||
| } | ||||
| @ -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 ); | ||||
| 	} | ||||
| } | ||||
| @ -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 ) {} | ||||
| } | ||||
| @ -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 | ||||
| 	} | ||||
| } | ||||
| @ -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 | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| } | ||||
| @ -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 ) ); | ||||
							
								
								
									
										48
									
								
								wp-content/plugins/two-factor/providers/js/fido-u2f-admin.js
									
									
									
									
									
										Normal 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 ) ); | ||||
							
								
								
									
										16
									
								
								wp-content/plugins/two-factor/providers/js/fido-u2f-login.js
									
									
									
									
									
										Normal 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 ) ); | ||||
							
								
								
									
										44
									
								
								wp-content/plugins/two-factor/readme.txt
									
									
									
									
									
										Normal 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). | ||||
							
								
								
									
										48
									
								
								wp-content/plugins/two-factor/two-factor.php
									
									
									
									
									
										Normal 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 ); | ||||
							
								
								
									
										42
									
								
								wp-content/plugins/two-factor/user-edit.css
									
									
									
									
									
										Normal 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; | ||||
| } | ||||