From cbbacb8841afb1262db2a98be200e591822c4d63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patrick=20M=C3=BCller?= <p.mueller@pcsg.de>
Date: Wed, 1 Feb 2017 10:56:28 +0100
Subject: [PATCH] feat: Neu-Generierung von Recovery Keys

---
 ajax/getKey.php                           |  10 +--
 ajax/regenerateRecoveryKeys.php           |  88 +++++++++++++++++++
 bin/controls/KeyData.html                 |  13 +--
 bin/controls/Settings.css                 |   8 ++
 bin/controls/Settings.js                  | 101 ++++++++++++++++++----
 locale.xml                                |  30 +++++--
 src/QUI/Auth/Google2Fa/Auth.php           |  25 ++++--
 src/QUI/Auth/Google2Fa/Controls/Login.css |   5 ++
 src/QUI/Auth/Google2Fa/Controls/Login.php |   2 +
 src/QUI/Auth/Google2Fa/Exception.php      |   2 +-
 10 files changed, 239 insertions(+), 45 deletions(-)
 create mode 100644 ajax/regenerateRecoveryKeys.php

diff --git a/ajax/getKey.php b/ajax/getKey.php
index 3422a16..622ef80 100644
--- a/ajax/getKey.php
+++ b/ajax/getKey.php
@@ -39,8 +39,8 @@ function ($userId, $title) {
 
             $keyData['key']    = Security::decrypt($secrets[$title]['key']);
             $keyData['qrCode'] = $Google2FA->getQRCodeInline(
-                'QUIQQER',
-                $AuthUser->getAttribute('email'),
+                $_SERVER['SERVER_NAME'],
+                $AuthUser->getUsername(),
                 $keyData['key']
             );
 
@@ -50,10 +50,10 @@ function ($userId, $title) {
 
             $keyData['recoveryKeys'] = array();
 
-            foreach ($secrets[$title]['recoveryKeys'] as $recoveryCode) {
-                $keyData['recoveryKeys'][] = trim(Security::decrypt($recoveryCode));
+            foreach ($secrets[$title]['recoveryKeys'] as $k => $recoveryKeyData) {
+                $recoveryKeyData['key']    = trim(Security::decrypt($recoveryKeyData['key']));
+                $keyData['recoveryKeys'][] = $recoveryKeyData;
             }
-
         } catch (QUI\Auth\Google2Fa\Exception $Exception) {
             QUI::getMessagesHandler()->addError(
                 QUI::getLocale()->get(
diff --git a/ajax/regenerateRecoveryKeys.php b/ajax/regenerateRecoveryKeys.php
new file mode 100644
index 0000000..71f6190
--- /dev/null
+++ b/ajax/regenerateRecoveryKeys.php
@@ -0,0 +1,88 @@
+<?php
+
+use QUI;
+use PragmaRX\Google2FA\Google2FA;
+use QUI\Utils\Security\Orthos;
+use QUI\Security;
+use QUI\Auth\Google2Fa\Auth;
+
+/**
+ * Re-generate a set of recovery keys for a user authentication key
+ *
+ * @param string $title - key title
+ * @return bool - success
+ */
+QUI::$Ajax->registerFunction(
+    'package_quiqqer_authgoogle2fa_ajax_regenerateRecoveryKeys',
+    function ($userId, $title) {
+        $AuthUser = QUI::getUsers()->get((int)$userId);
+        $title    = Orthos::clear($title);
+        $EditUser = QUI::getUserBySession();
+
+        // @todo Check user edit permission of session user
+
+        try {
+            $secrets = json_decode($AuthUser->getAttribute('quiqqer.auth.google2fa.secrets'), true);
+
+            if (empty($secrets)) {
+                $secrets = array();
+            }
+
+            if (!isset($secrets[$title])) {
+                throw new QUI\Auth\Google2Fa\Exception(array(
+                    'quiqqer/authgoogle2fa',
+                    'exception.ajax.getKey.title.not.found',
+                    array(
+                        'title'  => $title,
+                        'user'   => $AuthUser->getUsername(),
+                        'userId' => $AuthUser->getId()
+                    )
+                ));
+            }
+
+            $secrets[$title]['recoveryKeys'] = Auth::generateRecoveryKeys();
+
+            $AuthUser->setAttribute(
+                'quiqqer.auth.google2fa.secrets',
+                json_encode($secrets)
+            );
+
+            $AuthUser->save();
+        } catch (QUI\Auth\Google2Fa\Exception $Exception) {
+            QUI::getMessagesHandler()->addError(
+                QUI::getLocale()->get(
+                    'quiqqer/authgoogle2fa',
+                    'message.ajax.regenerateRecoveryKeys.error',
+                    array(
+                        'error' => $Exception->getMessage()
+                    )
+                )
+            );
+
+            return false;
+        } catch (\Exception $Exception) {
+            QUI::getMessagesHandler()->addError(
+                QUI::getLocale()->get(
+                    'quiqqer/authgoogle2fa',
+                    'message.ajax.general.error'
+                )
+            );
+
+            return false;
+        }
+
+        QUI::getMessagesHandler()->addSuccess(
+            QUI::getLocale()->get(
+                'quiqqer/authgoogle2fa',
+                'message.ajax.regenerateRecoveryKeys.success',
+                array(
+                    'title' => $title
+                )
+            )
+        );
+
+        return true;
+    },
+    array('userId', 'title'),
+    'Permission::checkAdminUser'
+);
diff --git a/bin/controls/KeyData.html b/bin/controls/KeyData.html
index 94bb5cd..c80883d 100644
--- a/bin/controls/KeyData.html
+++ b/bin/controls/KeyData.html
@@ -1,12 +1,4 @@
 <table class="quiqqer-auth-google2fa-register-showkey-data data-table data-table-flexbox">
-    <thead>
-    <tr>
-        <th colspan="2">
-            {{tableHeader}}
-        </th>
-    </tr>
-    </thead>
-
     <tbody>
     <tr>
         <td>
@@ -42,12 +34,13 @@
     </tr>
     <tr>
         <td>
-            <label class="field-container">
+            <div class="field-container">
                 <span class="field-container-item">{{labelRecoveryKeys}}</span>
                 <div class="field-container-field quiqqer-auth-google2fa-register-showkey-recoverykeys">
                     <ul></ul>
+                    <div class="quiqqer-auth-google2fa-register-showkey-recoverykeys-regenerate-btn"></div>
                 </div>
-            </label>
+            </div>
         </td>
     </tr>
     </tbody>
diff --git a/bin/controls/Settings.css b/bin/controls/Settings.css
index bf7582d..89ec216 100644
--- a/bin/controls/Settings.css
+++ b/bin/controls/Settings.css
@@ -27,4 +27,12 @@
 
 .quiqqer-auth-google2fa-register-generatekey input {
     width: 100%;
+}
+
+.quiqqer-auth-google2fa-register-showkey-data .field-container-item {
+    width: 150px;
+}
+
+.quiqqer-auth-google2fa-register-showkey-recoverykeys ul {
+    padding-left: 20px;
 }
\ No newline at end of file
diff --git a/bin/controls/Settings.js b/bin/controls/Settings.js
index 647361c..9e7d77c 100644
--- a/bin/controls/Settings.js
+++ b/bin/controls/Settings.js
@@ -156,10 +156,6 @@ define('package/quiqqer/authgoogle2fa/bin/controls/Settings', [
          * Event: onInject
          */
         $onInject: function () {
-
-            console.log(this.getAttribute('uid'));
-            console.log(this.getElm().get('data-qui-options-uid'));
-
             this.resize();
             this.refresh();
         },
@@ -309,12 +305,65 @@ define('package/quiqqer/authgoogle2fa/bin/controls/Settings', [
             var KeyData;
             var Row  = this.$Grid.getDataByRow(row);
 
+            var FuncShowRegenerateWarning = function () {
+                var WarnPopup = new QUIConfirm({
+                    title             : QUILocale.get(lg, 'controls.settings.showkey.regenerate.warning.title'),
+                    maxHeight         : 200,
+                    maxWidth          : 500,
+                    icon              : 'fa fa-repeat',
+                    backgroundClosable: false,
+
+                    // buttons
+                    buttons         : true, // {bool} [optional] show the bottom button line
+                    //closeButtonText : Locale.get('qui/controls/windows/Popup', 'btn.close'),
+                    titleCloseButton: false,  // {bool} show the title close button
+                    content         : false,
+                    events          : {
+                        onOpen  : function () {
+                            Popup.Loader.show();
+
+                            var Content = WarnPopup.getContent();
+
+                            Content.set(
+                                'html',
+                                QUILocale.get(lg, 'controls.settings.showkey.regenerate.warning')
+                            );
+                        },
+                        onSubmit: function () {
+                            QUIAjax.post(
+                                'package_quiqqer_authgoogle2fa_ajax_regenerateRecoveryKeys',
+                                function (success) {
+                                    Popup.Loader.hide();
+                                    WarnPopup.close();
+
+                                    if (!success) {
+                                        return;
+                                    }
+
+                                    Popup.close();
+                                    self.$showKey(row);
+                                }, {
+                                    'package': 'quiqqer/authgoogle2fa',
+                                    title    : Row.title,
+                                    userId   : self.getAttribute('uid')
+                                }
+                            );
+                        },
+                        onClose : function () {
+                            Popup.Loader.hide();
+                        }
+                    }
+                });
+
+                WarnPopup.open();
+            };
+
             var Popup = new QUIConfirm({
-                title             : QUILocale.get(
-                    lg, 'controls.settings.showkey.title'
-                ),
-                maxHeight         : 720,
-                maxWidth          : 650,
+                title             : QUILocale.get(lg, 'controls.settings.showkey.template.tableHeader', {
+                    title: Row.title
+                }),
+                maxHeight         : 710,
+                maxWidth          : 665,
                 icon              : 'fa fa-key',	// {false|string} [optional] icon of the window
                 backgroundClosable: true, // {bool} [optional] closes the window on click? standard = true
 
@@ -334,9 +383,6 @@ define('package/quiqqer/authgoogle2fa/bin/controls/Settings', [
                                 key              : KeyData.key,
                                 createUser       : KeyData.createUser,
                                 createDate       : KeyData.createDate,
-                                tableHeader      : QUILocale.get(lg, lgPrefix + 'tableHeader', {
-                                    title: Row.title
-                                }),
                                 labelQrCode      : QUILocale.get(lg, lgPrefix + 'labelQrCode'),
                                 labelKey         : QUILocale.get(lg, lgPrefix + 'labelKey'),
                                 labelCreateUser  : QUILocale.get(lg, lgPrefix + 'labelCreateUser'),
@@ -360,10 +406,35 @@ define('package/quiqqer/authgoogle2fa/bin/controls/Settings', [
                         );
 
                         for (var i = 0, len = KeyData.recoveryKeys.length; i < len; i++) {
-                            new Element('li', {
-                                html: KeyData.recoveryKeys[i]
-                            }).inject(RecoveryListElm);
+                            var RecoveryKeyData = KeyData.recoveryKeys[i];
+
+                            var LiElm = new Element('li').inject(RecoveryListElm);
+
+                            var KeyTextElm = new Element('span', {
+                                html: RecoveryKeyData.key
+                            }).inject(LiElm);
+
+                            if (RecoveryKeyData.used) {
+                                KeyTextElm.setStyle('text-decoration', 'line-through');
+
+                                new Element('span', {
+                                    html: ' (' + RecoveryKeyData.usedDate + ')'
+                                }).inject(LiElm);
+                            }
                         }
+
+                        // Re-generate recovery keys btn
+                        new QUIButton({
+                            textimage: 'fa fa-repeat',
+                            text     : QUILocale.get(lg, 'controls.settings.showkey.regenerate.recoverykeys.btn'),
+                            events   : {
+                                onClick: FuncShowRegenerateWarning
+                            }
+                        }).inject(
+                            Content.getElement(
+                                '.quiqqer-auth-google2fa-register-showkey-recoverykeys-regenerate-btn'
+                            )
+                        )
                     }
                 }
             });
diff --git a/locale.xml b/locale.xml
index 2c6eda3..02a6f1e 100644
--- a/locale.xml
+++ b/locale.xml
@@ -24,10 +24,10 @@
             <de><![CDATA[Beim Verarbeiten der Anfrage ist ein unerwarteter Fehler aufgetreten. Bitte wenden Sie sich an einen Administrator.]]></de>
         </locale>
         <locale name="message.ajax.generateKey.error" html="true">
-            <de><![CDATA[Beim Erstellen des Authentifizierungsschlüssels ist ein Fehler aufgetreten:<br/><br/>[error]]]></de>
+            <de><![CDATA[Beim Erstellen des Authentifizierungs-Schlüssels ist ein Fehler aufgetreten:<br/><br/>[error]]]></de>
         </locale>
         <locale name="message.ajax.generateKey.success">
-            <de><![CDATA[Der Authentifizierungsschlüssel "[title]" wurde erfolgreich erstellt.]]></de>
+            <de><![CDATA[Der Authentifizierungs-Schlüssel "[title]" wurde erfolgreich erstellt.]]></de>
         </locale>
         <locale name="message.ajax.getKey.error" html="true">
             <de><![CDATA[Beim Abruf des Authentifizierungs-Schlüssels ist ein Fehler aufgetreten:<br/><br/>[error]]]></de>
@@ -39,10 +39,16 @@
             <de><![CDATA[Beim Abruf der Authentifizierungs-Schlüssels ist ein Fehler aufgetreten:<br/><br/>[error]]]></de>
         </locale>
         <locale name="message.ajax.deleteKeys.error" html="true">
-            <de><![CDATA[Beim Löschen der Authentifizierungsschlüssels ist ein Fehler aufgetreten:<br/><br/>[error]]]></de>
+            <de><![CDATA[Beim Löschen der Authentifizierungs-Schlüssel ist ein Fehler aufgetreten:<br/><br/>[error]]]></de>
         </locale>
         <locale name="message.ajax.deleteKeys.success">
-            <de><![CDATA[Die gewählten Authentifizierungsschlüssel wurden erfolgreich gelöscht.]]></de>
+            <de><![CDATA[Die gewählten Authentifizierungs-Schlüssel wurden erfolgreich gelöscht.]]></de>
+        </locale>
+        <locale name="message.ajax.regenerateRecoveryKeys.error" html="true">
+            <de><![CDATA[Beim Neu-Generieren der Einmal-Login-Codes ist ein Fehler aufgetreten:<br/><br/>[error]]]></de>
+        </locale>
+        <locale name="message.ajax.regenerateRecoveryKeys.success">
+            <de><![CDATA[Die Einmal-Login-Codes wurden neu generiert. Die bisherigen Codes sind ab jetzt nicht mehr gültig.]]></de>
         </locale>
 
         <!-- Class: Auth -->
@@ -58,7 +64,7 @@
 
         <!-- Login -->
         <locale name="login.code">
-            <de><![CDATA[Authentifizierungs-Code]]></de>
+            <de><![CDATA[Google Authenticator Code]]></de>
         </locale>
         <locale name="login.btn.auth">
             <de><![CDATA[Authentifizieren]]></de>
@@ -70,7 +76,7 @@
 
         <!-- Control: Settings -->
         <locale name="controls.settings.generatekey.title">
-            <de><![CDATA[Neuer Authentifizierungsschlüssel]]></de>
+            <de><![CDATA[Neuer Authentifizierungs-Schlüssel]]></de>
         </locale>
         <locale name="controls.settings.generatekey.title.label">
             <de><![CDATA[Bezeichnung]]></de>
@@ -103,7 +109,7 @@
             <de><![CDATA[Erstellungsdatum]]></de>
         </locale>
         <locale name="controls.settings.showkey.template.labelRecoveryKeys">
-            <de><![CDATA[Einmal-Login-Schlüssel]]></de>
+            <de><![CDATA[Einmal-Login-Codes]]></de>
         </locale>
         <locale name="controls.settings.deleteKeys.title">
             <de><![CDATA[Authentifizierungs-Schlüssel löschen]]></de>
@@ -111,5 +117,15 @@
         <locale name="controls.settings.deleteKeys.info" html="true">
             <de><![CDATA[Sind Sie sicher, dass Sie die folgenden Authentifizierungs-Schlüssel unwiderruflich löschen wollen? Alle mit diesen Schlüsseln verknüpften Geräte können sich danach nicht mehr mit generierten Codes zu diesen Schlüsseln authentifizieren.<br/><br/>[titles]]]></de>
         </locale>
+        <locale name="controls.settings.showkey.regenerate.recoverykeys.btn">
+            <de><![CDATA[Neu generieren]]></de>
+        </locale>
+        <locale name="controls.settings.showkey.regenerate.warning.title">
+            <de><![CDATA[Einmal-Login-Codes neu generieren]]></de>
+        </locale>
+        <locale name="controls.settings.showkey.regenerate.warning">
+            <de><![CDATA[Sind Sie sicher, dass Sie die Einmal-Login-Codes neu generieren wollen? Alle alten Codes sind danach nicht mehr gültig und es können nur die neuen Codes verwendet werden.]]></de>
+        </locale>
+
     </groups>
 </locales>
\ No newline at end of file
diff --git a/src/QUI/Auth/Google2Fa/Auth.php b/src/QUI/Auth/Google2Fa/Auth.php
index 87da03a..5bfe30b 100644
--- a/src/QUI/Auth/Google2Fa/Auth.php
+++ b/src/QUI/Auth/Google2Fa/Auth.php
@@ -92,19 +92,26 @@ public function auth($authData)
             }
 
             // if key did not work check for recovery keys
-            foreach ($secretData['recoveryKeys'] as $k2 => $recoveryKey) {
-                $recoveryKey = trim(Security::decrypt($recoveryKey));
+            foreach ($secretData['recoveryKeys'] as $k2 => $recoveryKeyData) {
+                if ($recoveryKeyData['used']) {
+                    continue;
+                }
+
+                $recoveryKey = trim(Security::decrypt($recoveryKeyData['key']));
 
                 if ($recoveryKey != $authCode) {
                     continue;
                 }
 
-                // remove recovery key from list indefinitely
-                unset($secretData['recoveryKeys'][$k2]);
-                $authSecrets[$k] = $secretData;
+                // set used status of recovery key to true
+                $recoveryKeyData['used']     = true;
+                $recoveryKeyData['usedDate'] = date('Y-m-d H:i:s');
+
+                $secretData['recoveryKeys'][$k2] = $recoveryKeyData;
+                $authSecrets[$k]                 = $secretData;
 
                 $this->User->setAttribute('quiqqer.auth.google2fa.secrets', json_encode($authSecrets));
-                $this->User->save();
+                $this->User->save(QUI::getUsers()->getSystemUser());
 
                 return;
             }
@@ -148,7 +155,11 @@ public static function generateRecoveryKeys($count = 10)
         $Google2FA    = new Google2FA();
 
         for ($i = 0; $i < $count; $i++) {
-            $recoveryKeys[] = Security::encrypt(mb_substr(md5($Google2FA->generateSecretKey(16)), 0, 10));
+            $recoveryKeys[] = array(
+                'key'      => Security::encrypt(md5($Google2FA->generateSecretKey(16))),
+                'used'     => false,
+                'usedDate' => false
+            );
         }
 
         return $recoveryKeys;
diff --git a/src/QUI/Auth/Google2Fa/Controls/Login.css b/src/QUI/Auth/Google2Fa/Controls/Login.css
index 49f4d6d..441d6b6 100644
--- a/src/QUI/Auth/Google2Fa/Controls/Login.css
+++ b/src/QUI/Auth/Google2Fa/Controls/Login.css
@@ -16,6 +16,11 @@
     width: 100%;
 }
 
+.quiqqer-auth-google2fa-login input {
+    display: block;
+    width: 100%;
+}
+
 .quiqqer-auth-google2fa-login [type="submit"] {
     cursor: pointer;
 }
\ No newline at end of file
diff --git a/src/QUI/Auth/Google2Fa/Controls/Login.php b/src/QUI/Auth/Google2Fa/Controls/Login.php
index 6cd730c..f75e731 100644
--- a/src/QUI/Auth/Google2Fa/Controls/Login.php
+++ b/src/QUI/Auth/Google2Fa/Controls/Login.php
@@ -20,6 +20,8 @@ class Login extends Control
      */
     public function getBody()
     {
+        $this->addCSSFile(dirname(__FILE__) . '/Login.css');
+
         $Engine = QUI::getTemplateManager()->getEngine();
         return $Engine->fetch(dirname(__FILE__) . '/Login.html');
     }
diff --git a/src/QUI/Auth/Google2Fa/Exception.php b/src/QUI/Auth/Google2Fa/Exception.php
index 30306d8..188445d 100644
--- a/src/QUI/Auth/Google2Fa/Exception.php
+++ b/src/QUI/Auth/Google2Fa/Exception.php
@@ -2,7 +2,7 @@
 
 namespace QUI\Auth\Google2Fa;
 
-use QUI\Exception as QUIExcpetion;
+use QUI\Users\Exception as QUIExcpetion;
 
 class Exception extends QUIExcpetion
 {
-- 
GitLab