├── .gitignore ├── 2FA_qr_code.js ├── CIDR.php ├── LICENSE ├── PHPGangsta └── GoogleAuthenticator.php ├── README.md ├── ToDo ├── composer.json ├── config.inc.php.dist ├── docker-compose.yaml ├── localization ├── cs_CZ.inc ├── da_DK.inc ├── de_DE.inc ├── el_GR.inc ├── en_US.inc ├── es_AR.inc ├── es_ES.inc ├── fr_FR.inc ├── gl_ES.inc ├── he_IL.inc ├── hu_HU.inc ├── it_IT.inc ├── ja_JP.inc ├── lv_LV.inc ├── nb_NO.inc ├── nl_NL.inc ├── nn_NO.inc ├── pl_PL.inc ├── pt_BR.inc ├── ru_RU.inc ├── sk_SK.inc ├── sv_SE.inc ├── tr_TR.inc └── uk_UA.inc ├── qrcode.min.js ├── screenshots ├── 001-login.png ├── 002-2steps.png ├── 003-skip_30_days.png ├── 004-default_settings.png ├── 005-generated_setting.png ├── 006-settings_ok.png ├── 007-elastic_skin_start.png └── 008-elastic_skin_config.png ├── tools └── php-cs-fixer │ ├── composer.json │ └── composer.lock ├── twofactor_gauthenticator.js ├── twofactor_gauthenticator.php └── twofactor_gauthenticator_form.js /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .buildpath 3 | .settings* 4 | .php-cs-fixer.cache 5 | config.inc.php 6 | 7 | # Composer related 8 | vendor/ 9 | composer.lock 10 | 11 | # Code formatting vendor 12 | tools/php-cs-fixer/vendor 13 | 14 | #####=== Vim ===##### 15 | [._]*.s[a-w][a-z] 16 | [._]s[a-w][a-z] 17 | *.un~ 18 | Session.vim 19 | .netrwhist 20 | *~ 21 | 22 | 23 | .DS_Store 24 | 25 | db/* 26 | -------------------------------------------------------------------------------- /2FA_qr_code.js: -------------------------------------------------------------------------------- 1 | if (window.rcmail) { 2 | rcmail.addEventListener('init', function() { 3 | 4 | var url_qr_code_values = 'otpauth://totp/Roundcube:' +$('#prefs-title').html().split(/ - /)[1]+ '?secret=' +$('#2FA_secret').get(0).value +'&issuer=RoundCube2FA%20'+window.location.hostname; 5 | 6 | var qrcode = new QRCode(document.getElementById("2FA_qr_code"), { 7 | text: url_qr_code_values, 8 | width: 200, 9 | height: 200, 10 | colorDark : "#000000", 11 | colorLight : "#ffffff", 12 | correctLevel : QRCode.CorrectLevel.M // like charts.googleapis.com 13 | }); 14 | 15 | $('#2FA_qr_code').prop('title', ''); // enjoy the silence (qrcode.js uses text to set title) 16 | 17 | // white frame to dark mode, only to img generated 18 | $('#2FA_qr_code').children('img').css({ 19 | 'background-color': '#fff', 20 | padding: '4px' 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /CIDR.php: -------------------------------------------------------------------------------- 1 | $size) { 89 | array_pop($subnet); 90 | $j--; 91 | } 92 | $subnet = implode(':', $subnet).'*'; 93 | } 94 | } 95 | 96 | // shortened cidr 97 | $cidr = ($mask ? $subnet.'/'.$mask : $subnet); 98 | 99 | // if $cidr is ipv6, convert $ip to ipv6 for easier comparison 100 | if (strpos($subnet, ':') !== false && $ipVersion == 'v4') { 101 | 102 | $v6bits = array('0000','0000','0000','0000','0000','0000',$ip); 103 | 104 | $ip4parts = explode('.', $v6bits[count($v6bits) - 1]); 105 | $ip6trans = sprintf("%02x%02x:%02x%02x", $ip4parts[0], $ip4parts[1], $ip4parts[2], $ip4parts[3]); 106 | $v6bits[count($v6bits) - 1] = $ip6trans; 107 | 108 | $ip = implode(':', $v6bits); 109 | // shorten ip 110 | $ip = self::IPv6_compress($ip); 111 | 112 | $ipVersion = 'v6'; 113 | } 114 | 115 | if ($ip == $cidr) { 116 | return true; 117 | } 118 | 119 | // wildcard matching (easier since we already shortened or "canonicalized" ip and cidr above) 120 | $pos = strpos($cidr, '*'); 121 | if ($pos !== false) { 122 | if (substr($ip, 0, $pos) == substr($cidr, 0, $pos)) { 123 | return true; 124 | } else { 125 | return false; 126 | } 127 | } 128 | 129 | switch ($ipVersion) { 130 | case 'v4': 131 | return self::IPv4Match($ip, $subnet, $mask); 132 | break; 133 | case 'v6': 134 | return self::IPv6Match($ip, $subnet, $mask); 135 | break; 136 | } 137 | } 138 | 139 | 140 | 141 | /** 142 | * Check IPv6 address is within an IP range 143 | * 144 | * @param string $address a valid IPv6 address 145 | * @param string $subnet a valid IPv6 subnet 146 | * @param string $mask a valid IPv6 subnet mask 147 | * @return boolean whether $address is within the ip range made up of the subnet and mask 148 | */ 149 | private static function IPv6Match($ip, $subnet, $mask) 150 | { 151 | $subnet = inet_pton($subnet); 152 | $ip = inet_pton($ip); 153 | 154 | // thanks to MW on http://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet 155 | $binMask = str_repeat("f", $mask / 4); 156 | switch ($mask % 4) { 157 | case 0: 158 | break; 159 | case 1: 160 | $binMask .= "8"; 161 | break; 162 | case 2: 163 | $binMask .= "c"; 164 | break; 165 | case 3: 166 | $binMask .= "e"; 167 | break; 168 | } 169 | $binMask = str_pad($binMask, 32, '0'); 170 | $binMask = pack("H*", $binMask); 171 | 172 | 173 | return ($ip & $binMask) == $subnet; 174 | } 175 | 176 | /** 177 | * Check IPv4 address is within an IP range 178 | * 179 | * @param string $address a valid IPv4 address 180 | * @param string $subnet a valid IPv4 subnet 181 | * @param string $mask a valid IPv4 subnet mask 182 | * @return boolean whether $address is within the ip rage made up of the subnet and mask 183 | */ 184 | private static function IPv4Match($address, $subnet, $mask) 185 | { 186 | // credit goes to Sam on http://stackoverflow.com/questions/594112/matching-an-ip-to-a-cidr-mask-in-php5 187 | if ((ip2long($address) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet)) { 188 | return true; 189 | } 190 | 191 | return false; 192 | } 193 | 194 | /** 195 | * Compress an IPv6 Address 196 | * 197 | * @param string $ip a valid IPv6 address or CIDR 198 | * @return string IPv6 ip address or CIDR in short form notation 199 | */ 200 | public static function IPv6_compress($ip) 201 | { 202 | $bits = explode('/', $ip); // in case this is a CIDR range 203 | 204 | // want to expand and re-compress in case we have "::" in different spots. 205 | $bits[0] = self::IPv6_expand($bits[0]); 206 | 207 | $bits[0] = inet_ntop(inet_pton($bits[0])); 208 | return strtolower(implode('/', $bits)); 209 | } 210 | 211 | /** 212 | * Expand an IPv6 Address 213 | * 214 | * @param string $ip a valid IPv6 address 215 | * @return string IPv6 ip address in long form notation 216 | */ 217 | public static function IPv6_expand($ip) 218 | { 219 | 220 | $bits = explode('/', $ip); // in case this is a CIDR range 221 | 222 | // add missing components 223 | if (strpos($bits[0], '::') !== false) { 224 | $part = explode('::', $bits[0]); 225 | $part[0] = explode(':', $part[0]); 226 | $part[1] = explode(':', $part[1]); 227 | $missing = array(); 228 | for ($i = 0; $i < (8 - (count($part[0]) + count($part[1]))); $i++) { 229 | array_push($missing, '0000'); 230 | } 231 | $missing = array_merge($part[0], $missing); 232 | $part = array_merge($missing, $part[1]); 233 | } else { 234 | $part = explode(":", $bits[0]); 235 | } 236 | 237 | // Pad components to 4 characters 238 | foreach ($part as &$p) { 239 | while (strlen($p) < 4) { 240 | $p = '0' . $p; 241 | } 242 | } 243 | unset($p); 244 | 245 | $bits[0] = implode(':', $part); 246 | 247 | // if it is the incorrect length, something went wrong. 248 | if (strlen($bits[0]) != 39) { 249 | return false; 250 | } 251 | return strtolower(implode('/', $bits)); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2016 Alexandre Espinosa Menor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PHPGangsta/GoogleAuthenticator.php: -------------------------------------------------------------------------------- 1 | _getBase32LookupTable(); 26 | unset($validChars[32]); 27 | 28 | $secret = ''; 29 | for ($i = 0; $i < $secretLength; $i++) { 30 | $secret .= $validChars[array_rand($validChars)]; 31 | } 32 | return $secret; 33 | } 34 | 35 | /** 36 | * Calculate the code, with given secret and point in time 37 | * 38 | * @param string $secret 39 | * @param int|null $timeSlice 40 | * @return string 41 | */ 42 | public function getCode($secret, $timeSlice = null) 43 | { 44 | if ($timeSlice === null) { 45 | $timeSlice = floor(time() / 30); 46 | } 47 | 48 | $secretkey = $this->_base32Decode($secret); 49 | 50 | // Pack time into binary string 51 | $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); 52 | // Hash it with users secret key 53 | $hm = hash_hmac('SHA1', $time, $secretkey, true); 54 | // Use last nipple of result as index/offset 55 | $offset = ord(substr($hm, -1)) & 0x0F; 56 | // grab 4 bytes of the result 57 | $hashpart = substr($hm, $offset, 4); 58 | 59 | // Unpak binary value 60 | $value = unpack('N', $hashpart); 61 | $value = $value[1]; 62 | // Only 32 bits 63 | $value = $value & 0x7FFFFFFF; 64 | 65 | $modulo = pow(10, $this->_codeLength); 66 | return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT); 67 | } 68 | 69 | /** 70 | * Get QR-Code URL for image, from google charts 71 | * 72 | * @param string $name 73 | * @param string $secret 74 | * @param string $title 75 | * @return string 76 | */ 77 | public function getQRCodeGoogleUrl($name, $secret, $title = null) 78 | { 79 | $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.''); 80 | if (isset($title)) { 81 | $urlencoded .= urlencode('&issuer='.urlencode($title)); 82 | } 83 | return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.''; 84 | } 85 | 86 | /** 87 | * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now 88 | * 89 | * @param string $secret 90 | * @param string $code 91 | * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after) 92 | * @param int|null $currentTimeSlice time slice if we want use other that time() 93 | * @return bool 94 | */ 95 | public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null) 96 | { 97 | if ($currentTimeSlice === null) { 98 | $currentTimeSlice = floor(time() / 30); 99 | } 100 | 101 | for ($i = -$discrepancy; $i <= $discrepancy; $i++) { 102 | $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i); 103 | if ($this->timingSafeEquals($calculatedCode, $code)) { 104 | return true; 105 | } 106 | } 107 | 108 | return false; 109 | } 110 | 111 | /** 112 | * Set the code length, should be >=6 113 | * 114 | * @param int $length 115 | * @return PHPGangsta_GoogleAuthenticator 116 | */ 117 | public function setCodeLength($length) 118 | { 119 | $this->_codeLength = $length; 120 | return $this; 121 | } 122 | 123 | /** 124 | * Helper class to decode base32 125 | * 126 | * @param $secret 127 | * @return bool|string 128 | */ 129 | protected function _base32Decode($secret) 130 | { 131 | if (empty($secret)) { 132 | return ''; 133 | } 134 | 135 | $base32chars = $this->_getBase32LookupTable(); 136 | $base32charsFlipped = array_flip($base32chars); 137 | 138 | $paddingCharCount = substr_count($secret, $base32chars[32]); 139 | $allowedValues = array(6, 4, 3, 1, 0); 140 | if (!in_array($paddingCharCount, $allowedValues)) { 141 | return false; 142 | } 143 | for ($i = 0; $i < 4; $i++) { 144 | if ($paddingCharCount == $allowedValues[$i] && 145 | substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) { 146 | return false; 147 | } 148 | } 149 | $secret = str_replace('=', '', $secret); 150 | $secret = str_split($secret); 151 | $binaryString = ""; 152 | for ($i = 0; $i < count($secret); $i = $i + 8) { 153 | $x = ""; 154 | if (!in_array($secret[$i], $base32chars)) { 155 | return false; 156 | } 157 | for ($j = 0; $j < 8; $j++) { 158 | $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); 159 | } 160 | $eightBits = str_split($x, 8); 161 | for ($z = 0; $z < count($eightBits); $z++) { 162 | $binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : ""; 163 | } 164 | } 165 | return $binaryString; 166 | } 167 | 168 | /** 169 | * Helper class to encode base32 170 | * 171 | * @param string $secret 172 | * @param bool $padding 173 | * @return string 174 | */ 175 | protected function _base32Encode($secret, $padding = true) 176 | { 177 | if (empty($secret)) { 178 | return ''; 179 | } 180 | 181 | $base32chars = $this->_getBase32LookupTable(); 182 | 183 | $secret = str_split($secret); 184 | $binaryString = ""; 185 | for ($i = 0; $i < count($secret); $i++) { 186 | $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT); 187 | } 188 | $fiveBitBinaryArray = str_split($binaryString, 5); 189 | $base32 = ""; 190 | $i = 0; 191 | while ($i < count($fiveBitBinaryArray)) { 192 | $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)]; 193 | $i++; 194 | } 195 | if ($padding && ($x = strlen($binaryString) % 40) != 0) { 196 | if ($x == 8) { 197 | $base32 .= str_repeat($base32chars[32], 6); 198 | } elseif ($x == 16) { 199 | $base32 .= str_repeat($base32chars[32], 4); 200 | } elseif ($x == 24) { 201 | $base32 .= str_repeat($base32chars[32], 3); 202 | } elseif ($x == 32) { 203 | $base32 .= $base32chars[32]; 204 | } 205 | } 206 | return $base32; 207 | } 208 | 209 | /** 210 | * Get array with all 32 characters for decoding from/encoding to base32 211 | * 212 | * @return array 213 | */ 214 | protected function _getBase32LookupTable() 215 | { 216 | return array( 217 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 218 | 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 219 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 220 | 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 221 | '=' // padding char 222 | ); 223 | } 224 | 225 | /** 226 | * A timing safe equals comparison 227 | * more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html 228 | * 229 | * @param string $safeString The internal (safe) value to be checked 230 | * @param string $userString The user submitted (unsafe) value 231 | * 232 | * @return boolean True if the two strings are identical. 233 | */ 234 | private function timingSafeEquals($safeString, $userString) 235 | { 236 | if (function_exists('hash_equals')) { 237 | return hash_equals($safeString, $userString); 238 | } 239 | $safeLen = strlen($safeString); 240 | $userLen = strlen($userString); 241 | 242 | if ($userLen != $safeLen) { 243 | return false; 244 | } 245 | 246 | $result = 0; 247 | 248 | for ($i = 0; $i < $userLen; $i++) { 249 | $result |= (ord($safeString[$i]) ^ ord($userString[$i])); 250 | } 251 | 252 | // They are only identical strings if $result is exactly 0... 253 | return $result === 0; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Two-factor verification 2 | 3 | This RoundCube plugin adds the 2-step verification (OTP) to the login process. 4 | 5 | It works with all TOTP applications [RFC 6238](https://www.rfc-editor.org/info/rfc6238). 6 | 7 | Some code by: 8 | 9 | - [Ricardo Signes](https://github.com/rjbs) 10 | - [Justin Buchanan](https://github.com/jusbuc2k) 11 | - [Ricardo Iván Vieitez Parra](https://github.com/corrideat) 12 | - [GoogleAuthenticator class](https://github.com/PHPGangsta/GoogleAuthenticator/) by Michael Kliewe (to _see_ secrets) 13 | - [qrcode.js](https://github.com/davidshimjs/qrcodejs) by ShimSangmin 14 | - Also thanks to [Victor R. Rodriguez Dominguez](https://github.com/vrdominguez) for some ideas and support 15 | - Stephen K. Gielda 16 | 17 |

18 | 19 | ## Screenshots 20 | 21 | Login 22 |

23 | Login 24 | 25 |

26 | 27 | ## Table of Contents 28 | - [Installation](#installation) 29 | - [Get the plugin](#get-the-plugin) 30 | - [Activate the plugin](#activate-the-plugin) 31 | - [Configuration](#configuration) 32 | - [Variables](#variables) 33 | - [Usage](#usage) 34 | - [Docker Compose](#docker-compose) 35 | - [Development](#development) 36 | - [Code formatting](#code-formatting) 37 | - [Additional Information](#additional-information) 38 | - [Author](#author) 39 | - [Issues](#issues) 40 | - [TOTP Codes](#totp-codes) 41 | - [License](#license) 42 | - [Notes](#notes) 43 | - [Testing](#testing) 44 | - [Using with Kolab](#using-with-kolab) 45 | - [Client implementations](#client-implementations) 46 | - [Uninstall](#uninstall) 47 | - [For version 1.3.x](#for-version-13x) 48 | - [Security incidents](#security-incidents) 49 | - [2022-04-02](#2022-04-02) 50 | 51 |

52 | 53 | ## Installation 54 | 55 | **If you are using Roundcube 1.3.x, please refer to section [For version 1.3.x](#for-version-13x)**. 56 | 57 | ### Get the plugin 58 | 59 | **Method 1:** Clone from GitHub inside the plugins directory of Roundcube: 60 | 61 | 1. `cd plugins` 62 | 2. `git clone https://github.com/alexandregz/twofactor_gauthenticator.git` 63 | 64 | **Method 2:** Use composer from the Roundcube root directory: 65 | 66 | ```sh 67 | composer require alexandregz/twofactor_gauthenticator:dev-master 68 | ``` 69 | 70 | _NOTE:_ Answer **N** when the composer ask you about plugin activation. 71 | 72 | ### Activate the plugin 73 | 74 | Activate the plugin by editing the `HOME_RC/config/config.inc.php` file: 75 | 76 | ```php 77 | $config['plugins'] = [ 78 | // Other plugins... 79 | 'twofactor_gauthenticator', 80 | ]; 81 | ``` 82 | 83 | _NOTE:_ For docker user, add env `ROUNDCUBE_PLUGINS=twofactor_gauthenticator` into docker-compose file. For detailed 84 | information, see [Roundcube Docker Hub](https://hub.docker.com/r/roundcube/roundcubemail/). 85 | 86 | ### Configuration 87 | 88 | Copy `HOME_RC/plugins/twofactor_gauthenticator/config.inc.php.dist` to 89 | `HOME_RC/plugins/twofactor_gauthenticator/config.inc.php`. 90 | 91 | #### Variables 92 | 93 | Variables inside `config.inc.php`: 94 | 95 | | Variable | Variable Type | Default Value | Description | 96 | |-----------------------------------|---------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 97 | | `force_enrollment_users` | boolean | false | If true, all users must log in with 2-step verification. They will receive an alert message and cannot skip the configuration. | 98 | | `whitelist` | array | N/A | A Whitelist of IPs which are allowed to bypass the 2FA, CIDR format available.

_NOTE:_ We need to use .0 IP to define LAN because the class CIDR have a issue about that (we can't use 129.168.1.2/24, for example).

_NOTE2:_ To create a empty whitelist, make sure it looks like this:
`$rcmail_config['whitelist'] = array();` <- There are NO QUOTES inside the parentheses. | 99 | | `allow_save_device_30days` | boolean | false | If true, there will be a checkbox in the the TOTP code prompting page. By ticking it, there will be no 2FA prompt for 30 days. | 100 | | `twofactor_formfield_as_password` | boolean | false | If true, the entered TOTP code will appear as password in the webpage when prompting it. Otherwise, it'll shown as text. | 101 | | `users_allowed_2FA` | array | N/A | Users allowed to use plugin (IMPORTANT: other users DON'T have plugin activated). Regex is supported.

_NOTE:_ plugin must be base32 valid characters ([A-Z][2-7]), see [PHPGansta Library](https://github.com/alexandregz/twofactor_gauthenticator/blob/master/PHPGangsta/GoogleAuthenticator.php#L18), from [Issues 139](https://github.com/alexandregz/twofactor_gauthenticator/issues/139). | 102 | | `enable_fail_logs` | boolean | false | If true, 2FA failure will be logged in file log_errors_2FA.txt under HOME_RC/logs/log_errors_2FA.txt.

Suggested by @pngd [issue 131](https://github.com/alexandregz/twofactor_gauthenticator/issues/131). | 103 | 104 | The tickbox allows users to skip 2FA for 30 days: 105 | 106 | Login 107 | 108 | Variables that only existed in **Samefield branch**: 109 | 110 | | Variable | Variable Type | Default Value | Description | 111 | |-----------------------------|---------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 112 | | `2step_codes_on_login_form` | boolean | false | If config value _2step_codes_on_login_form_ is true, 2-step codes (and recovery) must be sended with password value, append to this, from the login screen: "Normal" codes just following password (passswordCODE), recovery codes after two pipes (passsword\|\|RECOVERYCODE) | 113 | 114 |

115 | 116 | ## Usage 117 | 118 | Go to Roundcube Settings > 2-Factor Authentication: 119 | 120 | Login 121 | 122 | The most easy way to configure it is by clicking "Fill all fields". The plugin automatically creates the secret as 123 | well as the recovery codes for you: 124 | 125 | Login 126 | 127 | You can store/create TOTP codes with any authenticator app by either scanning the QR code or entering the secret 128 | manually. 129 | 130 | Manually entering the secret as well as recovery codes is also possible. 131 | Note that the recovery codes are optional, so you can leave them blank. 132 | 133 | After setting up the authenticator, enter the code and press "Check code". If the code is correct, you can press "Save" 134 | to save the configuration and enable 2-step verification. 135 | 136 | Login 137 | 138 |

139 | 140 | ## Docker Compose 141 | 142 | You can use `docker-compose` file to modify and test plugin: 143 | 144 | - Replace `mail.EXAMPLE.com` for your IMAP and SMTP server. 145 | - `docker-compose up` 146 | - You can use `adminer` to check DB and reset secrets, for example. 147 | 148 |

149 | 150 | ## Development 151 | 152 | ### Code formatting 153 | 154 | Install PHP-CS-Fixer (requires `composer` to be installed): 155 | 156 | ```sh 157 | composer install --working-dir=./tools/php-cs-fixer 158 | ``` 159 | 160 | Run the coding standards fixer (in current working directory): 161 | 162 | ```sh 163 | ./tools/php-cs-fixer/vendor/bin/php-cs-fixer fix . 164 | ``` 165 | 166 |

167 | 168 | ## Additional Information 169 | 170 | ### Author 171 | 172 | Alexandre Espinosa Menor 173 | 174 | ### Issues 175 | 176 | Just open issues using GitHub issues instead of sending me emails, please. 177 | Gmail usually marks messages like this as SPAMs. 178 | 179 | ### TOTP Codes 180 | 181 | TOTP codes have a 2\*30 seconds clock tolerance. (May be editable in future versions) 182 | 183 | ### License 184 | 185 | MIT, see License 186 | 187 | ### Notes 188 | 189 | Tested with RoundCube 0.9.5 and Google app. Also with Roundcube 1.0.4 and 1.6.9 with OpenAuthenticator. 190 | 191 | Remember, time synchronization it's essential to TOTP: "For this to work, the clocks of the user's device and the server 192 | need to 193 | be roughly synchronized (the server will typically accept one-time passwords generated from timestamps that differ by ±1 194 | from the client's timestamp)" ( 195 | from [Wikipedia: Time-based one-time password](https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm)). 196 | 197 | ### Testing 198 | 199 | - Vagrant: https://github.com/alexandregz/vagrant-twofactor_gauthenticator 200 | - Docker: https://hub.docker.com/r/alexandregz/twofactor_gauthenticator/ 201 | 202 | ### Using with Kolab 203 | 204 | Add a symlink into the public_html/assets directory 205 | 206 | [Show explained](https://github.com/alexandregz/twofactor_gauthenticator/issues/29#issuecomment-156838186) 207 | by [Martin Stone](https://github.com/d7415) 208 | 209 | ### Client implementations 210 | 211 | You can use various [OTP clients](https://en.wikipedia.org/wiki/HMAC-based_One-time_Password_Algorithm#Applications) 212 | , by [helmo](https://github.com/helmo). 213 | 214 |

215 | 216 | ## Uninstall 217 | 218 | To deactivate the plugin, there are two methods: 219 | 220 | - For one only: Restore the user prefs from DB to null (rouncubeDB.users.preferences) which the user plugin options 221 | stored. 222 | 223 | - To all: Remove the plugin from config.inc.php thus remove the plugin itself. 224 | 225 |

226 | 227 | ## For version 1.3.x 228 | 229 | Use _1.3.9-version_ branch 230 | 231 | `$ git checkout 1.3.9-version` 232 | 233 | If you are using other versions other than 1.3.9, use _master_ version normally (thanks 234 | to [tborgans](https://github.com/tborgans)) 235 | 236 | Login 237 |

238 | Login 239 | 240 |

241 | 242 | ## Security incidents 243 | 244 | ### 2022-04-02 245 | 246 | Reported by kototilt@haiiro.dev (thx for the report and the PoC script) 247 | 248 | I made a little modification to the script to disallow user to save config without param session generated from a 249 | rendered page to force user to introduce previously 2FA code and navigate across site. 250 | 251 | _NOTE:_ I also checked if the user has 2FA activated because with only first condition -check SESSION- app kick me out 252 | before activating 2FA. 253 | 254 |
255 | 256 | **Function `twofactor_gauthenticator_save()`** 257 | 258 | On function `twofactor_gauthenticator_save()` I added this code: 259 | 260 | ```php 261 | // save config 262 | function twofactor_gauthenticator_save() 263 | { 264 | $rcmail = rcmail::get_instance(); 265 | 266 | // 2022-04-03: Corrected security incidente reported by kototilt@haiiro.dev 267 | // "2FA in twofactor_gauthenticator can be bypassed allowing an attacker to disable 2FA or change the TOTP secret." 268 | // 269 | // Solution: if user don't have session created by any rendered page, we kick out 270 | $config_2FA = self::__get2FAconfig(); 271 | if(!$_SESSION['twofactor_gauthenticator_2FA_login'] && $config_2FA['activate']) { 272 | $this->__exitSession(); 273 | } 274 | ``` 275 | 276 | The idea is to create a session variable from a rendered page, redirected from `__goingRoundcubeTask` function ( 277 | redirector to `roundcube tasks`) 278 | 279 |
280 | 281 | **Tests with PoC Python Script** 282 | 283 | Previously, with security compromised: 284 | 285 | ```bash 286 | alex@vosjod:~/Desktop/report$ ./poc.py 287 | Password:xxxxxxxx 288 | 1. Fetching login page (http://localhost:8888/roundcubemail-1.4.8) 289 | 2. Logging in 290 | POST http://localhost:8888/roundcubemail-1.4.8/?_task=login 291 | 3. Disabling 2FA 292 | POST http://localhost:8888/roundcubemail-1.4.8/?_task=settings&_action=plugin.twofactor_gauthenticator-save 293 | POST returned task "settings" 294 | 2FA disabled! 295 | ``` 296 | 297 | Modified code and tested again, not allowed to deactivated/modified without going to a RC task (with 2FA 298 | authentication): 299 | 300 | ```bash 301 | alex@vosjod:~/Desktop/report$ ./poc.py 302 | Password:xxxxxxxxx 303 | 1. Fetching login page (http://localhost:8888/roundcubemail-1.4.8) 304 | 2. Logging in 305 | POST http://localhost:8888/roundcubemail-1.4.8/?_task=login 306 | 3. Disabling 2FA 307 | POST http://localhost:8888/roundcubemail-1.4.8/?_task=settings&_action=plugin.twofactor_gauthenticator-save 308 | POST returned task "login" 309 | Expected "settings" task, something went wrong 310 | ``` 311 | -------------------------------------------------------------------------------- /ToDo: -------------------------------------------------------------------------------- 1 | - show errors with incorrect code, not just a logout message 2 | 3 | - permanent recovery code (simon@magrin.com) 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexandregz/twofactor_gauthenticator", 3 | "description": "This RoundCube plugin adds the Google 2-step verification to the login proccess (OTP)", 4 | "keywords": ["authentication","security"], 5 | "homepage": "https://github.com/alexandregz/twofactor_gauthenticator", 6 | "type": "roundcube-plugin", 7 | "license": "MIT", 8 | "version": "2.0.0", 9 | "authors": [ 10 | { 11 | "name": "Alexandre Espinosa Menor", 12 | "email": "aemenor@gmail.com", 13 | "role": "Developer" 14 | } 15 | ], 16 | "repositories": [ 17 | { 18 | "type": "composer", 19 | "url": "https://plugins.roundcube.net" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=5.3.0" 24 | }, 25 | "extra": { 26 | "roundcube": { 27 | "min-version": "0.9.0" 28 | } 29 | }, 30 | "minimum-stability": "stable" 31 | } 32 | -------------------------------------------------------------------------------- /config.inc.php.dist: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore) nebo google-authenticator.'; 13 | 14 | $labels['msg_help'] = 'Uživatelská příručka'; 15 | 16 | $labels['show_secret'] = 'Zobrazit tajný kód'; 17 | $labels['hide_secret'] = 'Skrýt tajný kód'; 18 | $labels['create_secret'] = 'Vytvořit tajný kód'; 19 | 20 | $labels['show_qr_code'] = 'Zobrazit QR kód'; 21 | $labels['hide_qr_code'] = 'Skrýt QR kód'; 22 | 23 | $labels['recovery_codes'] = 'Kódy pro obnovení'; 24 | $labels['show_recovery_codes'] = 'Zobrazit kódy pro obnovení'; 25 | $labels['hide_recovery_codes'] = 'Skrýt kódy pro obnovení'; 26 | 27 | $labels['setup_all_fields'] = 'Vyplňte všechna pole (a ujistěte se, že jste klikli na tlačítko Uložit)'; 28 | 29 | $labels['enrollment_dialog_title'] = 'Registrace dvoufázového ověření'; 30 | $labels['enrollment_dialog_msg'] = 'Kódy dvoufázového ověření jsou vyžadovány pro vyšší zabezpečení. Prosím, nastavte je nyní.'; 31 | 32 | $labels['check_code'] = 'Zkontrolovat kód'; 33 | $labels['code_ok'] = 'Správný kód'; 34 | $labels['code_ko'] = 'Špatný kód'; 35 | 36 | $labels['dont_ask_me_30days'] = 'Neptat se na tomto stroji znovu na kódy následujích 30 dnů'; 37 | 38 | $labels['check_code_to_activate'] = 'Pro uložení, naskenujte QR kód a vložte následně vygenerovaný dvoufázový kód níže.'; 39 | 40 | // Messages used for the different portions of the plugin 41 | $messages = array(); 42 | $messages['successfully_saved'] = 'Nastavení dvoufázového ověření bylo úspěšně uloženo.'; 43 | 44 | -------------------------------------------------------------------------------- /localization/da_DK.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore).'; 14 | 15 | $labels['msg_help'] = 'Brugervejledning'; 16 | 17 | $labels['show_secret'] = 'Vis hemmelig kode'; 18 | $labels['hide_secret'] = 'Skjul hemmelig kode'; 19 | $labels['create_secret'] = 'Generer hemmelig kode'; 20 | 21 | $labels['show_qr_code'] = 'Vis QR kode'; 22 | $labels['hide_qr_code'] = 'Skjul QR kode'; 23 | 24 | $labels['recovery_codes'] = 'Genopretnings koder'; 25 | $labels['show_recovery_codes'] = 'Vis genopretnings koder'; 26 | $labels['hide_recovery_codes'] = 'Skjul genopretnings koder'; 27 | 28 | $labels['setup_all_fields'] = 'Opret alle felter (behøver Gem)'; 29 | 30 | $labels['enrollment_dialog_title'] = '2-trins OTP verificering'; 31 | $labels['enrollment_dialog_msg'] = '2-trins OTP verificerings koder er påkrævet for sikkerhed, konfigurer venligst'; 32 | 33 | $labels['check_code'] = 'Kontroller kode'; 34 | $labels['code_ok'] = 'Koden er OK'; 35 | $labels['code_ko'] = 'Koden er forkert'; 36 | 37 | $labels['dont_ask_me_30days'] = 'Spørg mig ikke igen de næste 30 dage på denne computer'; 38 | 39 | $labels['check_code_to_activate'] = 'For at gemme, scan QR koden og indtast verificeringskoden nederst.'; 40 | 41 | // Messages used for the different portions of the plugin 42 | $messages = array(); 43 | $messages['successfully_saved'] = 'Gemt succesefuldt.'; 44 | -------------------------------------------------------------------------------- /localization/de_DE.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore) oder Google-Authenticator ein Secret erstellen und dieses verwenden.'; 13 | 14 | $labels['msg_help'] = 'Benutzerhandbuch'; 15 | 16 | $labels['show_secret'] = 'Zeige Secret'; 17 | $labels['hide_secret'] = 'Verstecke Secret'; 18 | $labels['create_secret'] = 'Erstelle Secret'; 19 | 20 | $labels['show_qr_code'] = 'Zeige QR-Code'; 21 | $labels['hide_qr_code'] = 'Verstecke QR-Code'; 22 | 23 | $labels['recovery_codes'] = 'Wiederherstellungscodes'; 24 | $labels['show_recovery_codes'] = 'Zeige Wiederherstellungscodes'; 25 | $labels['hide_recovery_codes'] = 'Verstecke Wiederherstellungscodes'; 26 | 27 | $labels['setup_all_fields'] = 'Erstelle fehlende Werte (Speichern klicken, um Ihre Einstellungen zu sichern)'; 28 | 29 | $labels['enrollment_dialog_title'] = 'Zwei-Faktor-Authentifizierung'; 30 | $labels['enrollment_dialog_msg'] = 'Bestätigungscodes für Zwei-Faktor-Authentifizierung werden aus Sicherheitsgründen benötigt, bitte konfigurieren!'; 31 | 32 | $labels['check_code'] = 'Überprüfe Code'; 33 | $labels['code_ok'] = 'Code OK'; 34 | $labels['code_ko'] = 'Falscher Code'; 35 | 36 | $labels['dont_ask_me_30days'] = 'Nicht erneut nach dem Code fragen für die nächsten 30 Tage'; 37 | 38 | $labels['check_code_to_activate'] = 'Um zu speichern, muss mindestens 1 Code zuvor geprüpft werden'; 39 | 40 | // Messages used for the different portions of the plugin 41 | $messages = array(); 42 | $messages['successfully_saved'] = 'Konfiguration erfolgreich gespeichert.'; 43 | -------------------------------------------------------------------------------- /localization/el_GR.inc: -------------------------------------------------------------------------------- 1 | (2-Factor Authentication)'; 6 | $labels['code'] = 'πιστοποίηση TOTP'; 7 | 8 | $labels['two_step_verification_form'] = 'Δπλή πιστοποίηση (2FA):'; 9 | 10 | $labels['secret'] = 'Μυστικό Κλειδί (Secret)'; 11 | $labels['qr_code'] = 'QR Code'; 12 | $labels['msg_infor'] = 'Μπορείτε να σαρώσετε τον QR code που περιέχει της ρυθμίσεις διπλής πιστοποίησης (2FA) με μια εφαρμογή TOTP (TOTP app) όπως ο OpenAuthenticator (Play Store | Istore) ή google-authenticator'; 13 | 14 | $labels['msg_help'] = 'Εγχειρίδιο χρήσης'; 15 | 16 | $labels['show_secret'] = 'Εμφάνιση Κλειδιού'; 17 | $labels['hide_secret'] = 'Απόκρυψη Κλειδιού'; 18 | $labels['create_secret'] = 'Δημιουργία Κλειδιού'; 19 | 20 | $labels['show_qr_code'] = 'Εμφάνιση QR Code'; 21 | $labels['hide_qr_code'] = 'Απόκρυψη QR Code'; 22 | 23 | $labels['recovery_codes'] = 'Κωδικοί ανάκτησης'; 24 | $labels['show_recovery_codes'] = 'Εμφάνιση Κωδικών Ανάκτησης'; 25 | $labels['hide_recovery_codes'] = 'Άπόκρυψη Κωδικών Ανάκτησης'; 26 | 27 | $labels['setup_all_fields'] = 'Συμπλήρωση όλων των πεδίων
(Θα πρέπει να πατήσετε Αποθήκευση για την καταχώρηση των ρυθμίσεων σας)'; 28 | 29 | $labels['enrollment_dialog_title'] = '2-Factor authentication enrollment'; 30 | $labels['enrollment_dialog_msg'] = 'H διπλή πιστοποίηση (2-Factor authentication) απαιτείται για αυξημένη ασφάλεια. Μπορείτε να την ορίσετε τώρα.'; 31 | 32 | $labels['check_code'] = 'Έλεγχος Κωδικού'; 33 | $labels['code_ok'] = 'Ο Κωδικός επαληθέφτηκε'; 34 | $labels['code_ko'] = 'Λάθος Κωδικός'; 35 | 36 | $labels['dont_ask_me_30days'] = 'Να μην ρωτηθώ πάλι για κωδικούς για τις επόμενες τριάντα (30) ημέρες'; 37 | 38 | $labels['check_code_to_activate'] = ' Για την αποθήκευση, παρακαλούμε να σαρώσετε τον κωδικό QR και να εισάγετε τον τρέχοντα κωδικό (2-Factor code) πιο κάτω.'; 39 | 40 | // Messages used for the different portions of the plugin 41 | $messages = array(); 42 | $messages['successfully_saved'] = 'Οι ρυθμίσεις διπλής πιστοποίσης καταχωρήθηκαν επιτυχώς.'; 43 | -------------------------------------------------------------------------------- /localization/en_US.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore) or google-authenticator'; 13 | 14 | $labels['msg_help'] = 'User manual'; 15 | 16 | $labels['show_secret'] = 'Show secret'; 17 | $labels['hide_secret'] = 'Hide secret'; 18 | $labels['create_secret'] = 'Create secret'; 19 | 20 | $labels['show_qr_code'] = 'Show QR Code'; 21 | $labels['hide_qr_code'] = 'Hide QR Code'; 22 | 23 | $labels['recovery_codes'] = 'Recovery codes'; 24 | $labels['show_recovery_codes'] = 'Show recovery codes'; 25 | $labels['hide_recovery_codes'] = 'Hide recovery codes'; 26 | 27 | $labels['setup_all_fields'] = 'Fill all fields (make sure you click save to store your settings)'; 28 | 29 | $labels['enrollment_dialog_title'] = '2-Factor authentication enrollment'; 30 | $labels['enrollment_dialog_msg'] = '2-Factor authentication codes are required for increased security, please configure them now.'; 31 | 32 | $labels['check_code'] = 'Check code'; 33 | $labels['code_ok'] = 'Code OK'; 34 | $labels['code_ko'] = 'Incorrect code'; 35 | 36 | $labels['dont_ask_me_30days'] = 'Don't ask me codes again on this computer for 30 days'; 37 | 38 | $labels['check_code_to_activate'] = 'To save, please scan the QR Code and enter the current 2-Factor code below.'; 39 | 40 | // Messages used for the different portions of the plugin 41 | $messages = array(); 42 | $messages['successfully_saved'] = '2-Factor authentication settings saved successfully.'; 43 | 44 | -------------------------------------------------------------------------------- /localization/es_AR.inc: -------------------------------------------------------------------------------- 1 | clave generada con OpenAuthenticator (Play Store | Istore) o google-authenticator'; 14 | 15 | $labels['msg_help'] = 'Manual del usuario'; 16 | 17 | $labels['show_secret'] = 'Mostrar clave'; 18 | $labels['hide_secret'] = 'Ocultar clave'; 19 | $labels['create_secret'] = 'Generar nueva clave'; 20 | 21 | $labels['show_qr_code'] = 'Mostrar código QR'; 22 | $labels['hide_qr_code'] = 'Ocultar código QR'; 23 | 24 | $labels['recovery_codes'] = 'Códigos de recuperación'; 25 | $labels['show_recovery_codes'] = 'Mostrar códigos de recuperación'; 26 | $labels['hide_recovery_codes'] = 'Ocultar códigos de recuperación'; 27 | 28 | $labels['setup_all_fields'] = 'Configurar todos los campos (requiere guardar)'; 29 | 30 | $labels['enrollment_dialog_title'] = 'Verificación de Google de dos pasos'; 31 | $labels['enrollment_dialog_msg'] = 'Los códigos de autenticación de dos pasos se requieren por seguridad. Por favor configurar.'; 32 | 33 | $labels['check_code'] = 'Comprobar código'; 34 | $labels['code_ok'] = 'Código correcto'; 35 | $labels['code_ko'] = 'Código incorrecto'; 36 | 37 | $labels['dont_ask_me_30days'] = 'No solicitar códigos en esta computadora durante 30 días'; 38 | 39 | $labels['check_code_to_activate'] = 'Para poder guardar, antes debe de chequearse algún código'; 40 | 41 | // Messages used for the different portions of the plugin 42 | $messages = array(); 43 | $messages['successfully_saved'] = 'Configuración guardada exitosamente'; 44 | 45 | -------------------------------------------------------------------------------- /localization/es_ES.inc: -------------------------------------------------------------------------------- 1 | secreto generado en tu ordenador con OpenAuthenticator (Play Store | Istore) o google-authenticator e usarlo'; 14 | 15 | $labels['msg_help'] = 'Manual del usuario'; 16 | 17 | $labels['show_secret'] = 'Ver secreto'; 18 | $labels['hide_secret'] = 'Esconder secreto'; 19 | $labels['create_secret'] = 'Crear secreto'; 20 | 21 | $labels['show_qr_code'] = 'Ver código QR'; 22 | $labels['hide_qr_code'] = 'Esconder código QR'; 23 | 24 | $labels['recovery_codes'] = 'Códigos de recuperación'; 25 | $labels['show_recovery_codes'] = 'Ver códigos de recuperación'; 26 | $labels['hide_recovery_codes'] = 'Esconder códigos de recuperación'; 27 | 28 | $labels['setup_all_fields'] = 'Configurar todos los campos (necesita Guardar)'; 29 | 30 | $labels['enrollment_dialog_title'] = '2steps Google verification'; 31 | $labels['enrollment_dialog_msg'] = 'Está activada la Verificación en dos pasos por seguridad, es obligatorio emplearla para autenticarse'; 32 | 33 | $labels['check_code'] = 'Comprobar código'; 34 | $labels['code_ok'] = 'Codigo correcto'; 35 | $labels['code_ko'] = 'Codigo erróneo'; 36 | 37 | $labels['dont_ask_me_30days'] = 'No solicitar códigos en este ordenador durante 30 días'; 38 | 39 | $labels['check_code_to_activate'] = 'Para poder guardar, antes debe de comprobarse algún código'; 40 | 41 | // Messages used for the different portions of the plugin 42 | $messages = array(); 43 | $messages['successfully_saved'] = 'Los datos han sido guardados correctamente.'; 44 | 45 | -------------------------------------------------------------------------------- /localization/fr_FR.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore).'; 14 | 15 | $labels['msg_help'] = 'Manuel d\'utilisation'; 16 | 17 | $labels['show_secret'] = 'Afficher le secret'; 18 | $labels['hide_secret'] = 'Masquer le secret'; 19 | $labels['create_secret'] = 'Créer un secret'; 20 | 21 | $labels['show_qr_code'] = 'Afficher le QR Code'; 22 | $labels['hide_qr_code'] = 'Masquer le QR Code'; 23 | 24 | $labels['recovery_codes'] = 'Codes de récupération'; 25 | $labels['show_recovery_codes'] = 'Afficher les codes de récupération'; 26 | $labels['hide_recovery_codes'] = 'Masquer les codes de récupération'; 27 | 28 | $labels['setup_all_fields'] = 'Configurer les champs (Enregistrement nécessaire)'; 29 | 30 | $labels['enrollment_dialog_title'] = 'Vérification OTP en 2 étapes'; 31 | $labels['enrollment_dialog_msg'] = 'Les codes de vérification en 2 étapes sont requis pour des raisons de sécurité, configurez-en SVP.'; 32 | 33 | $labels['check_code'] = 'Vérifier le code'; 34 | $labels['code_ok'] = 'Code OK'; 35 | $labels['code_ko'] = 'Code incorrect'; 36 | 37 | $labels['dont_ask_me_30days'] = 'Ne plus me demander de codes pour 30 jours.'; 38 | 39 | $labels['check_code_to_activate'] = 'Pour enregistrer, scannez le QR Code et entrez un premier code de vérification ci-dessous.'; 40 | 41 | // Messages used for the different portions of the plugin 42 | $messages = array(); 43 | $messages['successfully_saved'] = 'Configuration sauvegardée.'; 44 | 45 | -------------------------------------------------------------------------------- /localization/gl_ES.inc: -------------------------------------------------------------------------------- 1 | segredo xerado na tua computadora con OpenAuthenticator (Play Store | Istore) o google-authenticator e empregalo'; 14 | 15 | $labels['msg_help'] = 'Manual do usuario'; 16 | 17 | $labels['show_secret'] = 'Ver segredo'; 18 | $labels['hide_secret'] = 'Agochar segredo'; 19 | $labels['create_secret'] = 'Crear segredo'; 20 | 21 | $labels['show_qr_code'] = 'Ver código QR'; 22 | $labels['hide_qr_code'] = 'Agochar código QR'; 23 | 24 | $labels['recovery_codes'] = 'Códigos de recuperación'; 25 | $labels['show_recovery_codes'] = 'Ver códigos de recuperación'; 26 | $labels['hide_recovery_codes'] = 'Agochar códigos de recuperación'; 27 | 28 | $labels['setup_all_fields'] = 'Configurar tódolos campos (necesita Gardar)'; 29 | 30 | $labels['enrollment_dialog_title'] = '2steps Google verification'; 31 | $labels['enrollment_dialog_msg'] = 'Está activada a Verificación en dous pasos por seguridade, debese empregar obrigatoriamente. Faga o favor de configurala'; 32 | 33 | $labels['check_code'] = 'Comprobar código'; 34 | $labels['code_ok'] = 'Codigo correcto'; 35 | $labels['code_ko'] = 'Codigo erróneo'; 36 | 37 | $labels['dont_ask_me_30days'] = 'Non solicitar códigos nesta computadora durante 30 días'; 38 | 39 | $labels['check_code_to_activate'] = 'Para poder gardar, antes debese ter comprobado a validez dalgún código'; 40 | 41 | // Messages used for the different portions of the plugin 42 | $messages = array(); 43 | $messages['successfully_saved'] = 'Os datos para a conta foron gardados correctamente.'; 44 | 45 | -------------------------------------------------------------------------------- /localization/he_IL.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore) or google-authenticator'; 11 | $labels['msg_help'] = 'מדריך למשתמש'; 12 | $labels['show_secret'] = 'הראה קוד סודי'; 13 | $labels['hide_secret'] = 'הסתר קוד סודי'; 14 | $labels['create_secret'] = 'צור קודי סודי'; 15 | $labels['show_qr_code'] = 'הראה קוד QR'; 16 | $labels['hide_qr_code'] = 'הסתר קוד QR'; 17 | $labels['recovery_codes'] = 'קודים לשחזור'; 18 | $labels['show_recovery_codes'] = 'הראה קודים לשחזור'; 19 | $labels['hide_recovery_codes'] = 'הסתר קודים לשחזור'; 20 | $labels['setup_all_fields'] = 'מלא את כל השדות (וודא שאתה לוחץ שמור כדי לאחסן את ההגדרות)'; 21 | $labels['enrollment_dialog_title'] = 'הרשמה לאימות דו-שלבי'; 22 | $labels['enrollment_dialog_msg'] = 'קודים לאימות דו-שלבי נדרשים לאבטחה מירבית, אנא הגדר אותם כעת.'; 23 | $labels['check_code'] = 'בדוק קוד'; 24 | $labels['code_ok'] = 'הקוד תקין'; 25 | $labels['code_ko'] = 'הקוד לא תקין'; 26 | $labels['dont_ask_me_30days'] = 'אל תבקש ממני קודים אלו שוב במחשב זה למשך 30 יום'; 27 | $labels['check_code_to_activate'] = 'כדי לשמור, אנא סרוק את קוד ה-QR והכנס למטה את קוד האימות הדו-שלבי הנוכחי.'; 28 | // Messages used for the different portions of the plugin 29 | $messages = array(); 30 | $messages['successfully_saved'] = 'הגדרות האימות הדו-שלבי נשמרו בהצלחה.'; 31 | -------------------------------------------------------------------------------- /localization/hu_HU.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore) vagy Google Hitelesítő'; 13 | 14 | $labels['msg_help'] = 'Felhasználói kézikönyv'; 15 | 16 | $labels['show_secret'] = 'Titok mutatása'; 17 | $labels['hide_secret'] = 'Titok elrejtése'; 18 | $labels['create_secret'] = 'Titok készítése'; 19 | 20 | $labels['show_qr_code'] = 'QR-kód mutatása'; 21 | $labels['hide_qr_code'] = 'QR-kód elrejtése'; 22 | 23 | $labels['recovery_codes'] = 'Helyreállítási kódok'; 24 | $labels['show_recovery_codes'] = 'Helyreállítási kódok mutatása'; 25 | $labels['hide_recovery_codes'] = 'Helyreállítási kódok elrejtése'; 26 | 27 | $labels['setup_all_fields'] = 'Töltse ki az összes mezőt (feltétlenül kattintson a Mentés gombra a beállítások tárolásához)'; 28 | 29 | $labels['enrollment_dialog_title'] = 'Kétfaktoros hitelesítés regisztrációja'; 30 | $labels['enrollment_dialog_msg'] = 'Kétfaktoros hitelesítő kód szükséges a biztonság növeléséhez, kérem állítsa be most.'; 31 | 32 | $labels['check_code'] = 'Kód ellenőrzése'; 33 | $labels['code_ok'] = 'Helyes kód'; 34 | $labels['code_ko'] = 'Hibás kód'; 35 | 36 | $labels['dont_ask_me_30days'] = 'Ne kérdezze tőlem a kódot a következő 30 napban'; 37 | 38 | $labels['check_code_to_activate'] = 'A mentéshez kérem olvassa be a QR-kódot és írja be a kapott kétfaktoros kódot.'; 39 | 40 | // Messages used for the different portions of the plugin 41 | $messages = array(); 42 | $messages['successfully_saved'] = 'Kétfaktoros hitelesítési beállítások sikeresen mentve.'; 43 | 44 | -------------------------------------------------------------------------------- /localization/it_IT.inc: -------------------------------------------------------------------------------- 1 | secret generato con OpenAuthenticator (Play Store | Istore) o google-authenticator e utilizzare quello'; 14 | 15 | $labels['msg_help'] = 'Manuale d\'uso'; 16 | 17 | $labels['show_secret'] = 'Mostra secret'; 18 | $labels['hide_secret'] = 'Nascondi secret'; 19 | $labels['create_secret'] = 'Crea secret'; 20 | 21 | $labels['show_qr_code'] = 'Mostra codice QR'; 22 | $labels['hide_qr_code'] = 'Nascondi codice QR'; 23 | 24 | $labels['recovery_codes'] = 'Codici di ripristino'; 25 | $labels['show_recovery_codes'] = 'Mostra codici di ripristino'; 26 | $labels['hide_recovery_codes'] = 'Nascondi codici di ripristino'; 27 | 28 | $labels['setup_all_fields'] = 'Configura tutti i campi (richiede un salvataggio)'; 29 | 30 | $labels['enrollment_dialog_title'] = 'Verifica in due passaggi Google'; 31 | $labels['enrollment_dialog_msg'] = 'Codici di verifica due passaggi sono necessari per sicurezza, prego configurare'; 32 | 33 | $labels['check_code'] = 'Controlla codice'; 34 | $labels['code_ok'] = 'Codice OK'; 35 | $labels['code_ko'] = 'Codicec non corretto'; 36 | 37 | $labels['dont_ask_me_30days'] = 'Non chiedermi più codici per i prossimi 30 giorni su questo computer'; 38 | 39 | $labels['check_code_to_activate'] = 'Per salvare devi prima verificare il codice'; 40 | 41 | // Messages used for the different portions of the plugin 42 | $messages = array(); 43 | $messages['successfully_saved'] = 'Configurazione salvata correttamente.'; 44 | -------------------------------------------------------------------------------- /localization/ja_JP.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore) または google-authenticator で作成した秘密鍵を追加して使用できます'; 14 | $labels['msg_help'] = 'ユーザーマニュアル'; 15 | $labels['show_secret'] = '秘密鍵表示'; 16 | $labels['hide_secret'] = '秘密鍵非表示'; 17 | $labels['create_secret'] = '秘密鍵作成'; 18 | 19 | $labels['show_qr_code'] = 'QRコード表示'; 20 | $labels['hide_qr_code'] = 'QRコード非表示'; 21 | 22 | $labels['recovery_codes'] = 'リカバリーコード'; 23 | $labels['show_recovery_codes'] = 'リカバリーコード表示'; 24 | $labels['hide_recovery_codes'] = 'リカバリーコード非表示'; 25 | 26 | $labels['setup_all_fields'] = '全フィールド設定(保存必要)'; 27 | 28 | $labels['enrollment_dialog_title'] = 'Google二段階認証'; 29 | $labels['enrollment_dialog_msg'] = '二段階認証の設定が必要です。設定を行ってください'; 30 | 31 | $labels['check_code'] = 'コードチェック'; 32 | $labels['code_ok'] = 'コードOK'; 33 | $labels['code_ko'] = 'コードが違ってます'; 34 | 35 | $labels['dont_ask_me_30days'] = '子の端末で以後30日間コードを求めるな'; 36 | 37 | $labels['check_code_to_activate'] = '保存する前にコードを確認してください'; 38 | 39 | // Messages used for the different portions of the plugin 40 | $messages = array(); 41 | $messages['successfully_saved'] = '設定が正しく保存されました'; 42 | -------------------------------------------------------------------------------- /localization/lv_LV.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Apple Store) vai google-authenticator aplikāciju'; 13 | 14 | $labels['msg_help'] = 'Lietotāja rokasgrāmata'; 15 | 16 | $labels['show_secret'] = 'Parādīt drošības kodu'; 17 | $labels['hide_secret'] = 'Paslēpt drošības kodu'; 18 | $labels['create_secret'] = 'Izveidot drošības kodu'; 19 | 20 | $labels['show_qr_code'] = 'Parādīt QR kodu'; 21 | $labels['hide_qr_code'] = 'Paslēpt QR kodu'; 22 | 23 | $labels['recovery_codes'] = 'Atgūšanas kodi'; 24 | $labels['show_recovery_codes'] = 'Parādīt atgūšanas kodus'; 25 | $labels['hide_recovery_codes'] = 'Paslēpt atgūšanas kodus'; 26 | 27 | $labels['setup_all_fields'] = 'Automātiski aizpildīt visus laukus'; 28 | 29 | $labels['enrollment_dialog_title'] = '2-Factor authentication enrollment'; 30 | $labels['enrollment_dialog_msg'] = '2-Faktoru autentifikācijas kodi nepieciešami, lai uzlabotu drošību.'; 31 | 32 | $labels['check_code'] = 'Pārbaudīt kodu'; 33 | $labels['code_ok'] = 'Pārbaude veiksmīga'; 34 | $labels['code_ko'] = 'Nepareizs kods'; 35 | 36 | $labels['dont_ask_me_30days'] = 'Nejautāt kodu no šīs ierīces nākamās 30 dienas'; 37 | 38 | $labels['check_code_to_activate'] = 'Lai saglabātu, ievadiet zemāk esošo 2-Faktoru kodu'; 39 | 40 | // Messages used for the different portions of the plugin 41 | $messages = array(); 42 | $messages['successfully_saved'] = '2-Faktoru autentifikācijas iestatījumi saglabāti.'; 43 | -------------------------------------------------------------------------------- /localization/nb_NO.inc: -------------------------------------------------------------------------------- 1 | secret generated with OpenAuthenticator (Play Store | Istore) or google-authenticator and use that'; 15 | 16 | $labels['msg_help'] = 'User manual'; 17 | 18 | $labels['show_secret'] = 'Vis hemmeligheten'; 19 | $labels['hide_secret'] = 'Skjul hemmeligheten'; 20 | $labels['create_secret'] = 'Ny hemmelighet'; 21 | 22 | $labels['show_qr_code'] = 'Vis QR-koden'; 23 | $labels['hide_qr_code'] = 'Skjul QR-koden'; 24 | 25 | $labels['recovery_codes'] = 'Gjenopprettingskoder'; 26 | $labels['show_recovery_codes'] = 'Vis gjenopprettingskodene'; 27 | $labels['hide_recovery_codes'] = 'Skjul gjenopprettingskodene'; 28 | 29 | $labels['setup_all_fields'] = 'Sett opp alle felt (trenger å lagre skjermaet)'; 30 | 31 | $labels['enrollment_dialog_title'] = '2-trinns Google bekreftelse'; 32 | $labels['enrollment_dialog_msg'] = 'Du må sett opp 2-trinns bekreftelse koder for sikkerhet.'; 33 | 34 | $labels['check_code'] = 'Sjekk kode'; 35 | $labels['code_ok'] = 'God kode'; 36 | $labels['code_ko'] = 'Kode feil'; 37 | 38 | // Messages used for the different portions of the plugin 39 | $messages = array(); 40 | $messages['successfully_saved'] = 'Configuration are saved successfully.'; 41 | 42 | -------------------------------------------------------------------------------- /localization/nl_NL.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore).'; 11 | $labels['msg_help'] = 'Gebruiksaanwijzing'; 12 | $labels['show_secret'] = 'Secret weergeven'; 13 | $labels['hide_secret'] = 'Secret verbergen'; 14 | $labels['create_secret'] = 'Secret aanmaken'; 15 | $labels['show_qr_code'] = 'QR Code weergeven'; 16 | $labels['hide_qr_code'] = 'QR Code verbergen'; 17 | $labels['recovery_codes'] = 'Herstelcodes'; 18 | $labels['show_recovery_codes'] = 'Herstelcodes weergeven'; 19 | $labels['hide_recovery_codes'] = 'Herstelcodes verbergen'; 20 | $labels['setup_all_fields'] = 'Alle velden instellen (vereist )'; 21 | $labels['enrollment_dialog_title'] = '2-staps verificatie'; 22 | $labels['enrollment_dialog_msg'] = '2-staps verificatiecodes zijn vereist voor veiligheid, stel ze alstublieft in'; 23 | $labels['check_code'] = 'Check code'; 24 | $labels['code_ok'] = 'Code correct'; 25 | $labels['code_ko'] = 'Code incorrect'; 26 | $labels['dont_ask_me_30days'] = 'Vraag me de komende 30 dagen niet opnieuw op deze computer'; 27 | $labels['check_code_to_activate'] = 'Om op te slaan, scan alstublieft de QR Code en voer de verificatiecode hieronder in.'; 28 | // Messages used for the different portions of the plugin 29 | $messages = array(); 30 | $messages['successfully_saved'] = 'Instellingen zijn succesvol opgeslagen.'; 31 | 32 | -------------------------------------------------------------------------------- /localization/nn_NO.inc: -------------------------------------------------------------------------------- 1 | secret generated with OpenAuthenticator (Play Store | Istore) or google-authenticator and use that'; 15 | 16 | $labels['msg_help'] = 'User manual'; 17 | 18 | $labels['show_secret'] = 'Vis hemmelegheita'; 19 | $labels['hide_secret'] = 'Skjul hemmelegheita'; 20 | $labels['create_secret'] = 'Ny hemmelegheit'; 21 | 22 | $labels['show_qr_code'] = 'Vis QR-koden'; 23 | $labels['hide_qr_code'] = 'Skjul QR-koden'; 24 | 25 | $labels['recovery_codes'] = 'Gjenopprettingskodar'; 26 | $labels['show_recovery_codes'] = 'Vis gjenopprettingskodane'; 27 | $labels['hide_recovery_codes'] = 'Skjul gjenopprettingskodane'; 28 | 29 | $labels['setup_all_fields'] = 'Setje opp alle felt (trenger å lagre skjermaet)'; 30 | 31 | $labels['enrollment_dialog_title'] = '2-trinns Google verifikasjon'; 32 | $labels['enrollment_dialog_msg'] = 'Du må setje opp 2-trinns verifikasjon kodar for sikkerheit.'; 33 | 34 | $labels['check_code'] = 'Sjekk kode'; 35 | $labels['code_ok'] = 'God kode'; 36 | $labels['code_ko'] = 'Kode feil'; 37 | 38 | // Messages used for the different portions of the plugin 39 | $messages = array(); 40 | $messages['successfully_saved'] = 'Configuration are saved successfully.'; 41 | 42 | -------------------------------------------------------------------------------- /localization/pl_PL.inc: -------------------------------------------------------------------------------- 1 | tajny kod wygenerowany przez aplikację OpenAuthenticator (Play Store | Istore) lub google-authenticator i użyć go'; 14 | 15 | $labels['msg_help'] = 'Podręcznik użytkownika'; 16 | 17 | $labels['show_secret'] = 'Pokaż tajny klucz'; 18 | $labels['hide_secret'] = 'Ukryj tajny klucz'; 19 | $labels['create_secret'] = 'Generuj tajny klucz'; 20 | 21 | $labels['show_qr_code'] = 'Pokaż Kod QR'; 22 | $labels['hide_qr_code'] = 'Ukryj Kod QR'; 23 | 24 | $labels['recovery_codes'] = 'Hasła jednorazowe'; 25 | $labels['show_recovery_codes'] = 'Pokaż hasła jednorazowe'; 26 | $labels['hide_recovery_codes'] = 'Ukryj hasła jednorazowe'; 27 | 28 | $labels['setup_all_fields'] = 'Generuj wszystkie pola (Po weryfikacji kodu kliknij zapisz)'; 29 | 30 | $labels['enrollment_dialog_title'] = 'Dwuskładnikowe uwierzytelnianie'; 31 | $labels['enrollment_dialog_msg'] = 'Ze względów bezpieczeństwa konieczne jest skonfigurowanie i włączenie dwuskładnikowego uwierzytelniania'; 32 | 33 | $labels['check_code'] = 'Sprawdź Kod'; 34 | $labels['code_ok'] = 'Kod OK'; 35 | $labels['code_ko'] = 'Błędny kod'; 36 | 37 | $labels['dont_ask_me_30days'] = 'Nie pytaj ponownie o kod na tym komputerze przez następne 30 dni'; 38 | 39 | $labels['check_code_to_activate'] = 'Aby zapisać, zeskanuj kod QR i zweryfikuj go przyciskiem Sprawdź Kod'; 40 | 41 | // Messages used for the different portions of the plugin 42 | $messages = array(); 43 | $messages['successfully_saved'] = 'Konfiguracja zapisana prawidłowo.'; 44 | -------------------------------------------------------------------------------- /localization/pt_BR.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore) ou Android - Play Store ou em sua versão Apple iOS - AppStore.'; 13 | 14 | $labels['msg_help'] = 'Manual do usuário'; 15 | 16 | $labels['show_secret'] = 'Mostrar contra senha'; 17 | $labels['hide_secret'] = 'Esconder contra senha'; 18 | $labels['create_secret'] = 'Criar contra senha'; 19 | 20 | $labels['show_qr_code'] = 'Mostrar QR Code'; 21 | $labels['hide_qr_code'] = 'Esconder QR Code'; 22 | 23 | $labels['recovery_codes'] = 'Códigos de Recuperação'; 24 | $labels['show_recovery_codes'] = 'Mostrar códigos de recuperação'; 25 | $labels['hide_recovery_codes'] = 'Esconder códigos de recuperação'; 26 | 27 | $labels['setup_all_fields'] = 'Preencha todos os campos e clique em salvar para gerar o QR Code.'; 28 | 29 | $labels['enrollment_dialog_title'] = 'Habilitar Verificação em Duas Etapas [2FA]'; 30 | $labels['enrollment_dialog_msg'] = 'Por questões de segurança é obrigatório o cadastro e uso da Verificação em Duas Etapas [2FA]. Por favor efetue a configuração neste momento.'; 31 | 32 | $labels['check_code'] = 'Validar QR Code'; 33 | $labels['code_ok'] = 'QR Code Válido'; 34 | $labels['code_ko'] = 'QR Code Incorreto'; 35 | 36 | $labels['dont_ask_me_30days'] = 'Não perguntar por 30 dias'; 37 | 38 | $labels['check_code_to_activate'] = 'Para salvar, escaneie o QR code e introduza o código da Verificação em Duas Etapas [2FA] abaixo.'; 39 | 40 | // Messages used for the different portions of the plugin 41 | $messages = array(); 42 | $messages['successfully_saved'] = 'Configuração da Verificação em Duas Etapas [2FA] foi salva com sucesso.'; 43 | -------------------------------------------------------------------------------- /localization/ru_RU.inc: -------------------------------------------------------------------------------- 1 | ключ сгенерированный OpenAuthenticator (Play Store | Istore) или google-authenticator и использовать его'; 14 | 15 | $labels['msg_help'] = 'Руководство пользователя'; 16 | 17 | $labels['show_secret'] = 'Показать ключ'; 18 | $labels['hide_secret'] = 'Скрыть ключ'; 19 | $labels['create_secret'] = 'Создать ключ'; 20 | 21 | $labels['show_qr_code'] = 'Показать QR-код'; 22 | $labels['hide_qr_code'] = 'Скрыть QR-код'; 23 | 24 | $labels['recovery_codes'] = 'Коды восстановления'; 25 | $labels['show_recovery_codes'] = 'Показать коды'; 26 | $labels['hide_recovery_codes'] = 'Скрыть коды'; 27 | 28 | $labels['setup_all_fields'] = 'Заполнить все поля (необходимо Сохранить)'; 29 | 30 | $labels['enrollment_dialog_title'] = 'Двухэтапная Google аутентификация'; 31 | $labels['enrollment_dialog_msg'] = 'Коды двухэтапной аутентификации требуются для безопасности, пожалуйста настройте'; 32 | 33 | $labels['check_code'] = 'Проверить код'; 34 | $labels['code_ok'] = 'Код в порядке'; 35 | $labels['code_ko'] = 'Неверный код'; 36 | 37 | $labels['dont_ask_me_30days'] = 'Не спрашивать коды на этом компьютере 30 дней'; 38 | 39 | $labels['check_code_to_activate'] = 'Для сохранения, необходимо проверить код'; 40 | 41 | // Messages used for the different portions of the plugin 42 | $messages = array(); 43 | $messages['successfully_saved'] = 'Настройки успешно сохранены.'; 44 | 45 | -------------------------------------------------------------------------------- /localization/sk_SK.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore) alebo Google Authenticator alebo Authy'; 13 | 14 | $labels['msg_help'] = 'Používateľská príručka'; 15 | 16 | $labels['show_secret'] = 'Zobraziť tajný kód'; 17 | $labels['hide_secret'] = 'Skryť tajný kód'; 18 | $labels['create_secret'] = 'Vytvoriť tajný kód'; 19 | 20 | $labels['show_qr_code'] = 'Zobraziť QR kód'; 21 | $labels['hide_qr_code'] = 'Skryť QR kód'; 22 | 23 | $labels['recovery_codes'] = 'Kódy pre obnovenie'; 24 | $labels['show_recovery_codes'] = 'Zobraziť kódy pro obnovenie'; 25 | $labels['hide_recovery_codes'] = 'Skryť kódy pro obnovenie'; 26 | 27 | $labels['setup_all_fields'] = 'Vyplňte všetky polia (a uistite sa, že ste klikli na tlačidlo Uložiť)'; 28 | 29 | $labels['enrollment_dialog_title'] = 'Zapnutie dvojstupňového overenia'; 30 | $labels['enrollment_dialog_msg'] = 'Kódy dvojstupňového overenia sú potrebné pre vyššie zabezpečenie. Teraz ich prosím nastavte.'; 31 | 32 | $labels['check_code'] = 'Skontrolovať kód'; 33 | $labels['code_ok'] = 'Správny kód'; 34 | $labels['code_ko'] = 'Neplatný kód'; 35 | 36 | $labels['dont_ask_me_30days'] = 'Nepýtať si na tomto zariadení kódy 30 dní '; 37 | 38 | $labels['check_code_to_activate'] = 'Pre aktiváciu naskenujte QR kód v TOTP aplikácii (napr. Google Authenticator alebo Authy) a následne zadajte vygenerovaný kód.'; 39 | 40 | // Messages used for the different portions of the plugin 41 | $messages = array(); 42 | $messages['successfully_saved'] = 'Dvojstupňové overenie bolo úspešne zapnuté.'; 43 | -------------------------------------------------------------------------------- /localization/sv_SE.inc: -------------------------------------------------------------------------------- 1 | OpenAuthenticator (Play Store | Istore) eller google-authenticator'; 14 | 15 | $labels['msg_help'] = 'Bruksanvisning'; 16 | 17 | $labels['show_secret'] = 'Visa hemlig nyckel'; 18 | $labels['hide_secret'] = 'Dölj hemlig nyckel'; 19 | $labels['create_secret'] = 'Skapa hemlig nyckel'; 20 | 21 | $labels['show_qr_code'] = 'Visa QR-kod'; 22 | $labels['hide_qr_code'] = 'Dölj QR-kod'; 23 | 24 | $labels['recovery_codes'] = 'Återställningskoder'; 25 | $labels['show_recovery_codes'] = 'Visa återställningskoder'; 26 | $labels['hide_recovery_codes'] = 'Dölj återställningskoder'; 27 | 28 | $labels['setup_all_fields'] = 'Fyll i alla fält (se till att trycka på spara annars går alla ändringar förlorade)'; 29 | 30 | $labels['enrollment_dialog_title'] = 'Konfigurera tvåstegsverifiering'; 31 | $labels['enrollment_dialog_msg'] = 'Tvåstegsverifiering krävs för ökad säkerhet, vänligen konfigurera dem nu.'; 32 | 33 | $labels['check_code'] = 'Kontrollera kod'; 34 | $labels['code_ok'] = 'Koden godkänd'; 35 | $labels['code_ko'] = 'Inkorrekt kod'; 36 | 37 | $labels['dont_ask_me_30days'] = 'Kom ihåg mig på den här enheten i 30 dagar'; 38 | 39 | $labels['check_code_to_activate'] = 'Kontrollera kod för att aktivera'; 40 | 41 | // Messages used for the different portions of the plugin 42 | $messages = array(); 43 | $messages['successfully_saved'] = 'Tvåstegsverifieringsinställningar sparade'; 44 | 45 | -------------------------------------------------------------------------------- /localization/tr_TR.inc: -------------------------------------------------------------------------------- 1 | 13 | Android : Uygulamaya git
14 | IOS : Uygulamaya git'; 15 | 16 | $labels['msg_help'] = 'Kullanıcı kılavuzu'; 17 | 18 | $labels['show_secret'] = 'Gizli Kodu Göster'; 19 | $labels['hide_secret'] = 'Kodu Gizle'; 20 | $labels['create_secret'] = 'Giz kod oluştur'; 21 | 22 | $labels['show_qr_code'] = 'QR Kodu Göster'; 23 | $labels['hide_qr_code'] = 'QR Kodu Gizle'; 24 | 25 | $labels['recovery_codes'] = 'Kurtarma Kodu'; 26 | $labels['show_recovery_codes'] = 'Kurtarma Kodunu Göster'; 27 | $labels['hide_recovery_codes'] = 'Kurtarma Kodunu Gizle'; 28 | 29 | $labels['setup_all_fields'] = 'Tüm alanları doldurun (ayarlarınızı saklamak için kaydete tıkladığınızdan emin olun)'; 30 | 31 | $labels['enrollment_dialog_title'] = 'İki Faktörlü Doğrulama Kaydı'; 32 | $labels['enrollment_dialog_msg'] = 'Daha fazla güvenlik için 2 Faktörlü kimlik doğrulama kodları gereklidir; lütfen bunları şimdi yapılandırın'; 33 | 34 | $labels['check_code'] = 'Kodu Kontrol Et'; 35 | $labels['code_ok'] = 'Kod Doğrulandı. Başarılı!'; 36 | $labels['code_ko'] = 'Kod Hatalı!'; 37 | 38 | $labels['dont_ask_me_30days'] = '30 gün boyunca bu bilgisayarda bana bir daha kod doğrulaması yapılmasın.'; 39 | 40 | $labels['check_code_to_activate'] = 'Kaydetmek için lütfen QR Kodunu tarayın ve geçerli 2 Faktörlü kodu aşağıya girin.'; 41 | 42 | // Messages used for the different portions of the plugin 43 | $messages = array(); 44 | $messages['successfully_saved'] = '2 Faktörlü kimlik doğrulama ayarları başarıyla kaydedildi.'; 45 | -------------------------------------------------------------------------------- /localization/uk_UA.inc: -------------------------------------------------------------------------------- 1 | google-authenticator: Android, iOS'; 14 | $labels['msg_help'] = 'Посібник користувача'; 15 | 16 | $labels['show_secret'] = 'Показати ключ'; 17 | $labels['hide_secret'] = 'Сховати ключ'; 18 | $labels['create_secret'] = 'Створити ключ'; 19 | 20 | $labels['new_secret'] = 'Створити НОВИЙ ключ'; 21 | 22 | $labels['show_qr_code'] = 'Показати QR-код'; 23 | $labels['hide_qr_code'] = 'Сховати QR-код'; 24 | 25 | $labels['recovery_codes'] = 'Коди відновлення'; 26 | $labels['remaining_recovery_codes'] = 'Невикористані коди'; 27 | $labels['show_recovery_codes'] = 'Показати коди'; 28 | $labels['hide_recovery_codes'] = 'Сховати коди'; 29 | $labels['new_recovery_codes'] = 'Перегенерувати резервни коди'; 30 | 31 | $labels['setup_all_fields'] = 'Заповнити всі поля (необхідно Зберегти)'; 32 | 33 | $labels['enrollment_dialog_title'] = 'Двоетапна Google аутентифікація'; 34 | $labels['enrollment_dialog_msg'] = 'Коди двоетапної автентифікації потрібні для безпеки, будь ласка, налаштуйте'; 35 | 36 | $labels['check_code'] = 'Перевірити код'; 37 | $labels['code_ok'] = 'Код перевірено'; 38 | $labels['code_ko'] = 'Код недійсний'; 39 | 40 | $labels['dont_ask_me_days'] = 'Не запитувати коди на цьому комп'ютері {DAYS} днів'; 41 | 42 | $labels['check_code_to_activate'] = 'Для збереження, проскануйте QR-код і введіть поточний код нижче.'; 43 | 44 | $labels['test_passcode'] = 'Перевірте згенерований пароль тут'; 45 | 46 | $labels['proceed'] = 'Продовжити'; 47 | 48 | 49 | // Messages used for the different portions of the plugin 50 | $messages = array(); 51 | $messages['successfully_saved'] = 'Налаштування 2-факторної автентифікації успішно збережено.'; 52 | 53 | -------------------------------------------------------------------------------- /qrcode.min.js: -------------------------------------------------------------------------------- 1 | var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); -------------------------------------------------------------------------------- /screenshots/001-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandregz/twofactor_gauthenticator/c2db854a0df124641bcc749f01391a7601f67c47/screenshots/001-login.png -------------------------------------------------------------------------------- /screenshots/002-2steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandregz/twofactor_gauthenticator/c2db854a0df124641bcc749f01391a7601f67c47/screenshots/002-2steps.png -------------------------------------------------------------------------------- /screenshots/003-skip_30_days.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandregz/twofactor_gauthenticator/c2db854a0df124641bcc749f01391a7601f67c47/screenshots/003-skip_30_days.png -------------------------------------------------------------------------------- /screenshots/004-default_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandregz/twofactor_gauthenticator/c2db854a0df124641bcc749f01391a7601f67c47/screenshots/004-default_settings.png -------------------------------------------------------------------------------- /screenshots/005-generated_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandregz/twofactor_gauthenticator/c2db854a0df124641bcc749f01391a7601f67c47/screenshots/005-generated_setting.png -------------------------------------------------------------------------------- /screenshots/006-settings_ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandregz/twofactor_gauthenticator/c2db854a0df124641bcc749f01391a7601f67c47/screenshots/006-settings_ok.png -------------------------------------------------------------------------------- /screenshots/007-elastic_skin_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandregz/twofactor_gauthenticator/c2db854a0df124641bcc749f01391a7601f67c47/screenshots/007-elastic_skin_start.png -------------------------------------------------------------------------------- /screenshots/008-elastic_skin_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandregz/twofactor_gauthenticator/c2db854a0df124641bcc749f01391a7601f67c47/screenshots/008-elastic_skin_config.png -------------------------------------------------------------------------------- /tools/php-cs-fixer/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require-dev": { 3 | "friendsofphp/php-cs-fixer": "^3.66" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /twofactor_gauthenticator.js: -------------------------------------------------------------------------------- 1 | if (window.rcmail) { 2 | rcmail.addEventListener('init', function(evt) { 3 | 4 | // ripped from PHPGansta/GoogleAuthenticator.php 5 | function createSecret(secretLength) { 6 | if(!secretLength) secretLength = 16; 7 | 8 | var lookupTable = new Array( 9 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 10 | 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 11 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 12 | 'Y', 'Z', '2', '3', '4', '5', '6', '7' // 31 13 | //'=' // padding char 14 | ); 15 | 16 | var secret = ''; 17 | var random = new Uint8Array(secretLength); 18 | var cryptoapi = window.crypto || window.msCrypto; // Support IE11 for now 19 | cryptoapi.getRandomValues(random); 20 | for (var i = 0; i < secretLength; i++) { 21 | secret += lookupTable[random[i]%lookupTable.length]; 22 | } 23 | return secret; 24 | } 25 | 26 | // populate all fields 27 | function setup2FAfields() { 28 | if($('#2FA_secret').get(0).value) return; 29 | 30 | 31 | $('#twofactor_gauthenticator-form :input').each(function(){ 32 | if($(this).get(0).type == 'password') $(this).get(0).type = 'text'; 33 | }); 34 | 35 | // secret button 36 | $('#2FA_create_secret').prop('id', '2FA_change_secret'); 37 | $('#2FA_change_secret').get(0).value = rcmail.gettext('hide_secret', 'twofactor_gauthenticator'); 38 | $('#2FA_change_secret').click(click2FA_change_secret); 39 | $('#2FA_change_secret').removeAttr('disabled'); // now we disable all buttons previosly and user needs to "setup_all_fields" 40 | 41 | $('#2FA_activate').prop('checked', true); 42 | $('#2FA_show_recovery_codes').get(0).value = rcmail.gettext('hide_recovery_codes', 'twofactor_gauthenticator'); 43 | $('#2FA_show_recovery_codes').removeAttr('disabled'); // now we disable all buttons previosly and user needs to "setup_all_fields" 44 | $('#2FA_qr_code').slideDown(); 45 | 46 | $('#2FA_secret').get(0).value = createSecret(); 47 | $("[name^='2FA_recovery_codes']").each(function() { 48 | $(this).get(0).value = createSecret(10); 49 | }); 50 | 51 | // add qr-code before msg_infor 52 | var url_qr_code_values = 'otpauth://totp/' +$('#prefs-title').html().split(/ - /)[1]+ '?secret=' +$('#2FA_secret').get(0).value +'&issuer=RoundCube2FA%20'+window.location.hostname; 53 | $('table tr:last').before('' +rcmail.gettext('qr_code', 'twofactor_gauthenticator')+ '
'); 55 | 56 | var qrcode = new QRCode(document.getElementById("2FA_qr_code"), { 57 | text: url_qr_code_values, 58 | width: 200, 59 | height: 200, 60 | colorDark : "#000000", 61 | colorLight : "#ffffff", 62 | correctLevel : QRCode.CorrectLevel.L // like charts.googleapis.com 63 | }); 64 | 65 | $('#2FA_change_qr_code').click(click2FA_change_qr_code); 66 | $('#2FA_qr_code').prop('title', ''); // enjoy the silence (qrcode.js uses text to set title) 67 | 68 | // white frame to dark mode, only to img generated 69 | $('#2FA_qr_code').children('img').css({ 70 | 'background-color': '#fff', 71 | padding: '4px' 72 | }); 73 | 74 | // disable save button. It needs check code to enabled again 75 | $('#2FA_setup_fields').prev().attr('disabled','disabled').attr('title', rcmail.gettext('check_code_to_activate', 'twofactor_gauthenticator')); 76 | alert(rcmail.gettext('check_code_to_activate', 'twofactor_gauthenticator')); 77 | } 78 | 79 | $('#2FA_setup_fields').click(function(){ 80 | setup2FAfields(); 81 | }); 82 | 83 | // to show/hide secret 84 | click2FA_change_secret = function(){ 85 | if($('#2FA_secret').get(0).type == 'text') { 86 | $('#2FA_secret').get(0).type = 'password'; 87 | $('#2FA_change_secret').get(0).value = rcmail.gettext('show_secret', 'twofactor_gauthenticator'); 88 | } 89 | else 90 | { 91 | $('#2FA_secret').get(0).type = 'text'; 92 | $('#2FA_change_secret').get(0).value = rcmail.gettext('hide_secret', 'twofactor_gauthenticator'); 93 | } 94 | }; 95 | $('#2FA_change_secret').click(click2FA_change_secret); 96 | 97 | // to show/hide recovery_codes 98 | $('#2FA_show_recovery_codes').click(function(){ 99 | 100 | if($("[name^='2FA_recovery_codes']")[0].type == 'text') { 101 | $("[name^='2FA_recovery_codes']").each(function() { 102 | $(this).get(0).type = 'password'; 103 | }); 104 | $('#2FA_show_recovery_codes').get(0).value = rcmail.gettext('show_recovery_codes', 'twofactor_gauthenticator'); 105 | } 106 | else { 107 | $("[name^='2FA_recovery_codes']").each(function() { 108 | $(this).get(0).type = 'text'; 109 | }); 110 | $('#2FA_show_recovery_codes').get(0).value = rcmail.gettext('hide_recovery_codes', 'twofactor_gauthenticator'); 111 | } 112 | }); 113 | 114 | // to show/hide qr_code 115 | click2FA_change_qr_code = function(){ 116 | if( $('#2FA_qr_code').is(':visible') ) { 117 | $('#2FA_qr_code').slideUp(); 118 | $(this).get(0).value = rcmail.gettext('show_qr_code', 'twofactor_gauthenticator'); 119 | } 120 | else { 121 | $('#2FA_qr_code').slideDown(); 122 | $(this).get(0).value = rcmail.gettext('hide_qr_code', 'twofactor_gauthenticator'); 123 | } 124 | } 125 | $('#2FA_change_qr_code').click(click2FA_change_qr_code); 126 | 127 | // create secret 128 | $('#2FA_create_secret').click(function(){ 129 | $('#2FA_secret').get(0).value = createSecret(); 130 | }); 131 | 132 | // ajax 133 | $('#2FA_check_code').click(function(){ 134 | url = "./?_action=plugin.twofactor_gauthenticator-checkcode&code=" +$('#2FA_code_to_check').val() + '&secret='+$('#2FA_secret').val(); 135 | $.post(url, function(data){ 136 | alert(data); 137 | if(data == rcmail.gettext('code_ok', 'twofactor_gauthenticator')) 138 | $('#2FA_setup_fields').prev().removeAttr('disabled'); 139 | 140 | }); 141 | }); 142 | 143 | // Define Variables 144 | var tabtwofactorgauthenticator = $('
  • ') 145 | .attr('id', 'settingstabplugintwofactor_gauthenticator') 146 | .addClass('listitem twofactor_gauthenticator'); 147 | var button = $('') 148 | .attr('href', rcmail.env.comm_path + '&_action=plugin.twofactor_gauthenticator') 149 | .html(rcmail.gettext('twofactor_gauthenticator', 'twofactor_gauthenticator')) 150 | .attr('role', 'button') 151 | //.attr('onclick', 'return rcmail.command(\'show\', \'plugin.twofactor_gauthenticator\', this, event)') 152 | .attr('tabindex', '0') 153 | .attr('aria-disabled', 'false') 154 | .appendTo(tabtwofactorgauthenticator); 155 | 156 | // Button & Register commands 157 | rcmail.add_element(tabtwofactorgauthenticator, 'tabs'); 158 | rcmail.register_command('plugin.twofactor_gauthenticator', function() { rcmail.goto_url('plugin.twofactor_gauthenticator') }, true); 159 | rcmail.register_command('plugin.twofactor_gauthenticator-save', function() { 160 | if(!$('#2FA_secret').get(0).value) { 161 | $('#2FA_secret').get(0).value = createSecret(); 162 | } 163 | rcmail.gui_objects.twofactor_gauthenticatorform.submit(); 164 | }, true); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /twofactor_gauthenticator.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * Some ideas and code: Ricardo Signes , Ricardo Iván Vieitez Parra (https://github.com/corrideat), Justin Buchanan (https://github.com/jusbuc2k) 12 | * , https://github.com/pokrface, Peter Tobias, Víctor R. Rodríguez Domínguez (https://github.com/vrdominguez), etc. 13 | * Date: 2013-11-30 14 | */ 15 | require_once 'PHPGangsta/GoogleAuthenticator.php'; 16 | 17 | require_once 'CIDR.php'; 18 | 19 | class twofactor_gauthenticator extends rcube_plugin 20 | { 21 | private $_number_recovery_codes = 4; 22 | 23 | // relative to $config['log_dir'] 24 | private $_logs_file = 'log_errors_2FA.txt'; 25 | 26 | public function init() 27 | { 28 | $rcmail = rcmail::get_instance(); 29 | 30 | // Completely block AJAX requests for unauthenticated users (by Stephen K. Gielda ) 31 | if (!$rcmail->user->ID && !isset($_SESSION['twofactor_gauthenticator_login']) && isset($_REQUEST['_remote'])) { 32 | 33 | // Direct JSON response to prevent leakage 34 | header('Content-Type: application/json'); 35 | echo json_encode(array( 36 | 'error' => 'Session expired or invalid', 37 | 'redirect' => '?_task=login&_err=session' 38 | )); 39 | exit; 40 | } 41 | 42 | // Block data access via AJAX for partially authenticated users who have 2FA enabled (by Stephen K. Gielda ) 43 | if (isset($_SESSION['twofactor_gauthenticator_login']) && 44 | (!isset($_SESSION['twofactor_gauthenticator_2FA_login']) || 45 | $_SESSION['twofactor_gauthenticator_2FA_login'] < $_SESSION['twofactor_gauthenticator_login']) && 46 | isset($_REQUEST['_remote']) && 47 | $rcmail->action !== 'plugin.twofactor_gauthenticator-checkcode' && 48 | $rcmail->task !== 'login') { 49 | 50 | // Get user's 2FA config 51 | $user_prefs = $rcmail->user->get_prefs(); 52 | $tfa_config = isset($user_prefs['twofactor_gauthenticator']) ? $user_prefs['twofactor_gauthenticator'] : null; 53 | 54 | // Only block if 2FA is enabled for this user 55 | if ($tfa_config && isset($tfa_config['activate']) && $tfa_config['activate']) { 56 | // Direct JSON response to prevent leakage 57 | header('Content-Type: application/json'); 58 | echo json_encode(array( 59 | 'error' => '2FA authentication required', 60 | 'redirect' => '?_task=login&_err=session' 61 | )); 62 | exit; 63 | } 64 | } 65 | 66 | // hooks 67 | $this->add_hook('login_after', array($this, 'login_after')); 68 | $this->add_hook('send_page', array($this, 'check_2FAlogin')); 69 | $this->add_hook('render_page', array($this, 'popup_msg_enrollment')); 70 | 71 | $this->load_config(); 72 | 73 | $allowedPlugin = $this->__pluginAllowedByConfig(); 74 | 75 | // skipping all logic and plugin not appears 76 | if (!$allowedPlugin) { 77 | return false; 78 | } 79 | 80 | $this->add_texts('localization/', true); 81 | 82 | // check code with ajax 83 | $this->register_action('plugin.twofactor_gauthenticator-checkcode', array($this, 'checkCode')); 84 | 85 | // config 86 | $this->register_action('twofactor_gauthenticator', array($this, 'twofactor_gauthenticator_init')); 87 | $this->register_action('plugin.twofactor_gauthenticator-save', array($this, 'twofactor_gauthenticator_save')); 88 | $this->include_script('twofactor_gauthenticator.js'); 89 | $this->include_script('qrcode.min.js'); 90 | 91 | // settings we will export to the form javascript 92 | //$this_output = $this->api->output; 93 | //if ($this_output) { 94 | // $this->api->output->set_env('allow_save_device_30days',$rcmail->config->get('allow_save_device_30days',true)); 95 | // $this->api->output->set_env('twofactor_formfield_as_password',$rcmail->config->get('twofactor_formfield_as_password',false)); 96 | //} 97 | } 98 | 99 | // check if user are valid from config.inc.php or true (by default) if config.inc.php not exists 100 | public function __pluginAllowedByConfig() 101 | { 102 | $rcmail = rcmail::get_instance(); 103 | 104 | $this->load_config(); 105 | 106 | // users allowed to use plugin (not showed for others!). 107 | // -- From config.inc.php file. 108 | // -- You can use regexp: admin.*@domain.com 109 | $users = $rcmail->config->get('users_allowed_2FA'); 110 | if (is_array($users)) { // exists "users" from config.inc.php 111 | foreach ($users as $u) { 112 | if (isset($rcmail->user->data['username'])) { 113 | preg_match("/$u/", $rcmail->user->data['username'], $matches); 114 | 115 | if (isset($matches[0])) { 116 | return true; 117 | } 118 | } 119 | } 120 | 121 | // not allowed for all, except explicit 122 | return false; 123 | } 124 | 125 | // by default, all users have plugin activated 126 | return true; 127 | } 128 | 129 | // Use the form login, but removing inputs with jquery and action (see twofactor_gauthenticator_form.js) 130 | public function login_after($args) 131 | { 132 | $_SESSION['twofactor_gauthenticator_login'] = time(); 133 | 134 | $rcmail = rcmail::get_instance(); 135 | 136 | 137 | $config_2FA = self::__get2FAconfig(); 138 | if (!($config_2FA['activate'] ?? false)) { 139 | if ($rcmail->config->get('force_enrollment_users')) { 140 | $this->__goingRoundcubeTask('settings', 'plugin.twofactor_gauthenticator'); 141 | } 142 | return; 143 | } 144 | 145 | if ($this->__cookie($set = false) || !$this->__pluginAllowedByConfig()) { 146 | $_SESSION['twofactor_gauthenticator_login'] -= 1; // so that we may use ge to check for valid session 147 | $this->__goingRoundcubeTask('mail'); 148 | return; 149 | } 150 | 151 | $rcmail->output->set_pagetitle($this->gettext('twofactor_gauthenticator')); 152 | 153 | $rcmail->output->set_env('allow_save_device_30days', $rcmail->config->get('allow_save_device_30days', true)); 154 | $rcmail->output->set_env('twofactor_formfield_as_password', $rcmail->config->get('twofactor_formfield_as_password', false)); 155 | 156 | $this->add_texts('localization', true); 157 | $this->include_script('twofactor_gauthenticator_form.js'); 158 | 159 | $rcmail->output->send('login'); 160 | } 161 | 162 | // capture webpage if someone try to use ?_task=mail|addressbook|settings|... and check auth code 163 | public function check_2FAlogin($p) 164 | { 165 | $rcmail = rcmail::get_instance(); 166 | $config_2FA = self::__get2FAconfig(); 167 | 168 | if ($config_2FA['activate'] ?? false) { 169 | // with IP allowed, we don't need to check anything 170 | if ($rcmail->config->get('whitelist')) { 171 | foreach ($rcmail->config->get('whitelist') as $ip_to_check) { 172 | if (isset($_SERVER['HTTP_CLIENT_IP']) && array_key_exists('HTTP_CLIENT_IP', $_SERVER)) { 173 | $realip = $_SERVER['HTTP_CLIENT_IP']; 174 | } elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) { 175 | $realips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); 176 | $realips = array_map('trim', $realips); 177 | $realip = $realips[0]; 178 | } else { 179 | $realip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; 180 | } 181 | if (CIDR::match($realip, $ip_to_check)) { 182 | if (isset($_SESSION['twofactor_gauthenticator_login'])) { 183 | if ($rcmail->task === 'login') { 184 | $this->__goingRoundcubeTask('mail'); 185 | } 186 | return $p; 187 | } 188 | } 189 | } 190 | } 191 | 192 | 193 | $code = rcube_utils::get_input_value('_code_2FA', rcube_utils::INPUT_POST); 194 | $remember = rcube_utils::get_input_value('_remember_2FA', rcube_utils::INPUT_POST); 195 | 196 | if ($code) { 197 | if (self::__checkCode($code) || self::__isRecoveryCode($code)) { 198 | if (self::__isRecoveryCode($code)) { 199 | self::__consumeRecoveryCode($code); 200 | } 201 | 202 | if (rcube_utils::get_input_value('_remember_2FA', rcube_utils::INPUT_POST) === 'yes') { 203 | $this->__cookie($set = true); 204 | } 205 | 206 | $this->__goingRoundcubeTask('mail'); 207 | } else { 208 | if ($rcmail->config->get('enable_fail_logs')) { 209 | $this->__logError(); 210 | } 211 | $this->__exitSession(); 212 | } 213 | } 214 | // we're into some task but marked with login... 215 | elseif ($rcmail->task !== 'login' && ! $_SESSION['twofactor_gauthenticator_2FA_login'] >= $_SESSION['twofactor_gauthenticator_login']) { 216 | $this->__exitSession(); 217 | } 218 | 219 | } elseif ($rcmail->config->get('force_enrollment_users') && ($rcmail->task !== 'settings' || $rcmail->action !== 'plugin.twofactor_gauthenticator')) { 220 | if ($rcmail->task !== 'login') { // resolve some redirection loop with logout 221 | $this->__goingRoundcubeTask('settings', 'plugin.twofactor_gauthenticator'); 222 | } 223 | } 224 | 225 | return $p; 226 | } 227 | 228 | // ripped from new_user_dialog plugin 229 | public function popup_msg_enrollment() 230 | { 231 | $rcmail = rcmail::get_instance(); 232 | $config_2FA = self::__get2FAconfig(); 233 | 234 | if (!($config_2FA['activate'] ?? false) 235 | && $rcmail->config->get('force_enrollment_users') && $rcmail->task == 'settings' && $rcmail->action == 'plugin.twofactor_gauthenticator') { 236 | // add overlay input box to html page 237 | $rcmail->output->add_footer(html::tag( 238 | 'form', 239 | array( 240 | 'id' => 'enrollment_dialog', 241 | 'method' => 'post'), 242 | html::tag('h3', null, $this->gettext('enrollment_dialog_title')) . 243 | $this->gettext('enrollment_dialog_msg') 244 | )); 245 | 246 | $rcmail->output->add_script( 247 | "$('#enrollment_dialog').show().dialog({ modal:true, resizable:false, closeOnEscape: true, width:420 });", 248 | 'docready' 249 | ); 250 | } 251 | } 252 | 253 | // show config 254 | public function twofactor_gauthenticator_init() 255 | { 256 | $rcmail = rcmail::get_instance(); 257 | 258 | $this->add_texts('localization/', true); 259 | $this->register_handler('plugin.body', array($this, 'twofactor_gauthenticator_form')); 260 | 261 | $rcmail->output->set_pagetitle($this->gettext('twofactor_gauthenticator')); 262 | $rcmail->output->send('plugin'); 263 | } 264 | 265 | // save config 266 | public function twofactor_gauthenticator_save() 267 | { 268 | $rcmail = rcmail::get_instance(); 269 | 270 | // Verify user is authenticated before allowing changes (by Stephen K. Gielda ) 271 | if (!$rcmail->user->ID) { 272 | header('Location: ?_task=login'); 273 | exit; 274 | } 275 | 276 | // 2022-04-03: Corrected security incidente reported by kototilt@haiiro.dev 277 | // "2FA in twofactor_gauthenticator can be bypassed allowing an attacker to disable 2FA or change the TOTP secret." 278 | // 279 | // Solution: if user don't have session created by any rendered page, we kick out 280 | $config_2FA = self::__get2FAconfig(); 281 | if (!$_SESSION['twofactor_gauthenticator_2FA_login'] && $config_2FA['activate']) { 282 | $this->__exitSession(); 283 | } 284 | 285 | $this->add_texts('localization/', true); 286 | $this->register_handler('plugin.body', array($this, 'twofactor_gauthenticator_form')); 287 | $rcmail->output->set_pagetitle($this->gettext('twofactor_gauthenticator')); 288 | 289 | // POST variables 290 | $activate = rcube_utils::get_input_value('2FA_activate', rcube_utils::INPUT_POST); 291 | $secret = rcube_utils::get_input_value('2FA_secret', rcube_utils::INPUT_POST); 292 | $recovery_codes = rcube_utils::get_input_value('2FA_recovery_codes', rcube_utils::INPUT_POST); 293 | 294 | // remove recovery codes without value 295 | $recovery_codes = array_values(array_diff($recovery_codes, array(''))); 296 | 297 | $data = self::__get2FAconfig(); 298 | $data['secret'] = $secret; 299 | $data['activate'] = $activate ? true : false; 300 | $data['recovery_codes'] = $recovery_codes; 301 | self::__set2FAconfig($data); 302 | 303 | // if we can't save time into SESSION, the plugin logouts 304 | $_SESSION['twofactor_gauthenticator_2FA_login'] = time(); 305 | 306 | $rcmail->output->show_message($this->gettext('successfully_saved'), 'confirmation'); 307 | 308 | $rcmail->overwrite_action('plugin.twofactor_gauthenticator'); 309 | $rcmail->output->send('plugin'); 310 | } 311 | 312 | // form config 313 | public function twofactor_gauthenticator_form() 314 | { 315 | $rcmail = rcmail::get_instance(); 316 | 317 | $this->add_texts('localization/', true); 318 | $rcmail->output->set_env('product_name', $rcmail->config->get('product_name')); 319 | 320 | $data = self::__get2FAconfig(); 321 | 322 | // Fields will be positioned inside of a table 323 | $table = new html_table(array('cols' => 2)); 324 | 325 | // Activate/deactivate 326 | $field_id = '2FA_activate'; 327 | $checkbox_activate = new html_checkbox(array('name' => $field_id, 'id' => $field_id, 'type' => 'checkbox')); 328 | $table->add('title', html::label($field_id, rcube::Q($this->gettext('activate')))); 329 | $checked = (isset($data['activate']) && $data['activate']) ? null : 1; // :-? 330 | $table->add(null, $checkbox_activate->show($checked)); 331 | 332 | 333 | // secret 334 | $field_id = '2FA_secret'; 335 | $input_descsecret = new html_inputfield(array('name' => $field_id, 'id' => $field_id, 'size' => 60, 'type' => 'password', 'value' => $data['secret'] ?? '', 'autocomplete' => 'new-password')); 336 | $table->add('title', html::label($field_id, rcube::Q($this->gettext('secret')))); 337 | $html_secret = $input_descsecret->show(); 338 | if ($data['secret'] ?? '') { 339 | $html_secret .= '   '; 340 | } else { 341 | $html_secret .= '   '; 342 | } 343 | $table->add(null, $html_secret); 344 | 345 | 346 | // recovery codes 347 | $table->add('title', $this->gettext('recovery_codes')); 348 | 349 | $html_recovery_codes = ''; 350 | $i = 0; 351 | for ($i = 0; $i < $this->_number_recovery_codes; $i++) { 352 | $value = isset($data['recovery_codes'][$i]) ? $data['recovery_codes'][$i] : ''; 353 | $html_recovery_codes .= '   '; 354 | } 355 | if ($data['secret'] ?? '') { 356 | $html_recovery_codes .= ''; 357 | } else { 358 | $html_recovery_codes .= ''; 359 | } 360 | $table->add(null, $html_recovery_codes); 361 | 362 | 363 | // qr-code 364 | if ($data['secret'] ?? '') { 365 | $table->add('title', $this->gettext('qr_code')); 366 | $table->add(null, ' 367 | '); 368 | 369 | // new JS qr-code, without call to Google 370 | $this->include_script('2FA_qr_code.js'); 371 | } 372 | 373 | // infor 374 | $table->add(null, '
    '.$this->gettext('msg_infor').''); 375 | 376 | // button to setup all fields if doesn't exists secret 377 | $html_setup_all_fields = ''; 378 | if (empty($data['secret'])) { 379 | $html_setup_all_fields = ''; 380 | } 381 | 382 | $html_check_code = '

       '; 383 | 384 | $html_help_code = '

    ⓘ '.$this->gettext('msg_help'); 385 | 386 | // Build the table with the divs around it 387 | $out = html::div( 388 | array('class' => 'settingsbox'), 389 | html::tag('h3', array('id' => 'prefs-title', 'class' => ''), $this->gettext('twofactor_gauthenticator') . ' - ' . $rcmail->user->data['username']) . 390 | html::div( 391 | array('class' => 'boxcontent'), 392 | $table->show() . 393 | html::p( 394 | null, 395 | $rcmail->output->button(array( 396 | 'command' => 'plugin.twofactor_gauthenticator-save', 397 | 'type' => 'input', 398 | 'class' => 'button mainaction', 399 | 'label' => 'save' 400 | )) 401 | 402 | // button show/hide secret 403 | //.'' 404 | 405 | // button to setup all fields 406 | .$html_setup_all_fields 407 | .$html_check_code 408 | .$html_help_code 409 | ) 410 | ) 411 | ); 412 | 413 | // Construct the form 414 | $rcmail->output->add_gui_object('twofactor_gauthenticatorform', 'twofactor_gauthenticator-form'); 415 | 416 | $out = $rcmail->output->form_tag(array( 417 | 'id' => 'twofactor_gauthenticator-form', 418 | 'name' => 'twofactor_gauthenticator-form', 419 | 'method' => 'post', 420 | 'action' => './?_task=settings&_action=plugin.twofactor_gauthenticator-save', 421 | ), $out); 422 | 423 | $out = "
    ".$out."
    "; 424 | 425 | return $out; 426 | } 427 | 428 | // used with ajax 429 | public function checkCode() 430 | { 431 | $code = rcube_utils::get_input_value('code', rcube_utils::INPUT_GET); 432 | //$secret = rcube_utils::get_input_value('secret', rcube_utils::INPUT_GET); 433 | $secret = rcube_utils::get_input_value('secret', rcube_utils::INPUT_GET); 434 | 435 | if (self::__checkCode($code, $secret)) { 436 | echo $this->gettext('code_ok'); 437 | } else { 438 | echo $this->gettext('code_ko'); 439 | } 440 | exit; 441 | } 442 | 443 | //------------- private methods 444 | 445 | // redirect to some RC task and remove 'login' user pref 446 | private function __goingRoundcubeTask($task = 'mail', $action = null) 447 | { 448 | 449 | $_SESSION['twofactor_gauthenticator_2FA_login'] = time(); 450 | header('Location: ?_task='.$task . ($action ? '&_action='.$action : '')); 451 | exit; 452 | } 453 | 454 | private function __exitSession() 455 | { 456 | unset($_SESSION['twofactor_gauthenticator_login']); 457 | unset($_SESSION['twofactor_gauthenticator_2FA_login']); 458 | 459 | $rcmail = rcmail::get_instance(); 460 | header('Location: ?_task=logout&_token='.$rcmail->get_request_token()); 461 | exit; 462 | } 463 | 464 | private function __get2FAconfig() 465 | { 466 | $rcmail = rcmail::get_instance(); 467 | $user = $rcmail->user; 468 | 469 | $arr_prefs = $user->get_prefs(); 470 | return $arr_prefs['twofactor_gauthenticator'] ?? null; 471 | } 472 | 473 | // we can set array to NULL to remove 474 | private function __set2FAconfig($data) 475 | { 476 | $rcmail = rcmail::get_instance(); 477 | $user = $rcmail->user; 478 | 479 | $arr_prefs = $user->get_prefs(); 480 | $arr_prefs['twofactor_gauthenticator'] = $data; 481 | 482 | return $user->save_prefs($arr_prefs); 483 | } 484 | 485 | private function __isRecoveryCode($code) 486 | { 487 | $prefs = self::__get2FAconfig(); 488 | return in_array($code, $prefs['recovery_codes']); 489 | } 490 | 491 | private function __consumeRecoveryCode($code) 492 | { 493 | $prefs = self::__get2FAconfig(); 494 | $prefs['recovery_codes'] = array_values(array_diff($prefs['recovery_codes'], array($code))); 495 | 496 | self::__set2FAconfig($prefs); 497 | } 498 | 499 | 500 | // GoogleAuthenticator class methods (see PHPGangsta/GoogleAuthenticator.php for more infor) 501 | // returns string 502 | private function __createSecret() 503 | { 504 | $ga = new PHPGangsta_GoogleAuthenticator(); 505 | return $ga->createSecret(); 506 | } 507 | 508 | // returns string 509 | private function __getSecret() 510 | { 511 | $prefs = self::__get2FAconfig(); 512 | return $prefs['secret']; 513 | } 514 | 515 | // Commented. If you have problems with qr-code.js, you can uncomment and use this 516 | // 517 | // // returns string (url to img) 518 | // private function __getQRCodeGoogle() 519 | // { 520 | // $rcmail = rcmail::get_instance(); 521 | 522 | // $ga = new PHPGangsta_GoogleAuthenticator(); 523 | // return $ga->getQRCodeGoogleUrl($rcmail->user->data['username'], self::__getSecret(), 'RoundCube2FA'); 524 | // } 525 | 526 | // returns boolean 527 | private function __checkCode($code, $secret = null) 528 | { 529 | $ga = new PHPGangsta_GoogleAuthenticator(); 530 | return $ga->verifyCode(($secret ? $secret : self::__getSecret()), $code, 2); // 2 = 2*30sec clock tolerance 531 | } 532 | 533 | 534 | // remember option by https://github.com/corrideat/ 535 | private function __cookie($set = true) 536 | { 537 | $rcmail = rcmail::get_instance(); 538 | $user_agent = hash_hmac('md5', filter_input(INPUT_SERVER, 'USER_AGENT') ?: "\0\0\0\0\0", $rcmail->config->get('des_key')); 539 | $key = hash_hmac('sha256', implode("\2\1\2", array($rcmail->user->data['username'], $this->__getSecret())), $rcmail->config->get('des_key'), true); 540 | $iv = hash_hmac('md5', implode("\3\2\3", array($rcmail->user->data['username'], $this->__getSecret())), $rcmail->config->get('des_key'), true); 541 | $name = hash_hmac('md5', $rcmail->user->data['username'], $rcmail->config->get('des_key')); 542 | 543 | if ($set) { 544 | $expires = time() + 2592000; // 30 days from now 545 | $rand = mt_rand(); 546 | $signature = hash_hmac('sha512', implode("\1\0\1", array($rcmail->user->data['username'], $this->__getSecret(), $user_agent, $rand, $expires)), $rcmail->config->get('des_key'), true); 547 | $plain_content = sprintf("%d:%d:%s", $expires, $rand, $signature); 548 | $encrypted_content = openssl_encrypt($plain_content, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv); 549 | if ($encrypted_content !== false) { 550 | $b64_encrypted_content = strtr(base64_encode($encrypted_content), '+/=', '-_,'); 551 | rcube_utils::setcookie($name, $b64_encrypted_content, $expires); 552 | return true; 553 | } 554 | return false; 555 | } else { 556 | $b64_encrypted_content = filter_input(INPUT_COOKIE, $name, FILTER_VALIDATE_REGEXP, array('options' => array('regexp' => '/[a-zA-Z0-9_-]+,{0,3}/'))); 557 | if (is_string($b64_encrypted_content) && !empty($b64_encrypted_content) && strlen($b64_encrypted_content) % 4 === 0) { 558 | $encrypted_content = base64_decode(strtr($b64_encrypted_content, '-_,', '+/='), true); 559 | if ($encrypted_content !== false) { 560 | $plain_content = openssl_decrypt($encrypted_content, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv); 561 | if ($plain_content !== false) { 562 | $now = time(); 563 | list($expires, $rand, $signature) = explode(':', $plain_content, 3); 564 | if ($expires > $now && ($expires - $now) <= 2592000) { 565 | $signature_verification = hash_hmac('sha512', implode("\1\0\1", array($rcmail->user->data['username'], $this->__getSecret(), $user_agent, $rand, $expires)), $rcmail->config->get('des_key'), true); 566 | // constant time 567 | $cmp = strlen($signature) ^ strlen($signature_verification); 568 | $signature = $signature ^ $signature_verification; 569 | for ($i = 0; $i < strlen($signature); $i++) { 570 | $cmp += ord($signature [$i]); 571 | } 572 | return ($cmp === 0); 573 | } 574 | } 575 | } 576 | } 577 | return false; 578 | } 579 | } 580 | // END remember 581 | 582 | 583 | // log error into $_logs_file directory 584 | private function __logError() 585 | { 586 | $rcmail = rcmail::get_instance(); 587 | $_log_dir = $rcmail->config->get('log_dir'); 588 | file_put_contents($_log_dir.'/'.$this->_logs_file, date("Y-m-d H:i:s")."|".$_SERVER['HTTP_X_FORWARDED_FOR']."|".$_SERVER['REMOTE_ADDR']."\n", FILE_APPEND); 589 | } 590 | } 591 | -------------------------------------------------------------------------------- /twofactor_gauthenticator_form.js: -------------------------------------------------------------------------------- 1 | if (window.rcmail) { 2 | rcmail.addEventListener('init', function() { 3 | // remove the user/password/... input from login 4 | $('form > table > tbody > tr').each(function(){ 5 | $(this).remove(); 6 | }); 7 | 8 | // change task & action 9 | $('form').attr('action', './'); 10 | $('input[name=_task]').attr('value', 'mail'); 11 | $('input[name=_action]').attr('value', ''); 12 | 13 | //determine twofactor field type based on config settings 14 | if(rcmail.env.twofactor_formfield_as_password) 15 | var twoFactorCodeFieldType = 'password'; 16 | else 17 | var twoFactorCodeFieldType = 'text'; 18 | 19 | //twofactor input form 20 | var text = ''; 21 | text += ''; 22 | text += ''; 23 | text += ''; 24 | text += ''; 25 | 26 | // remember option 27 | if(rcmail.env.allow_save_device_30days){ 28 | text += ''; 29 | text += ''; 30 | text += ''; 31 | } 32 | 33 | // create textbox 34 | $('form > table > tbody:last').append(text); 35 | 36 | // focus 37 | $('#2FA_code').focus(); 38 | 39 | }); 40 | 41 | }; 42 | --------------------------------------------------------------------------------