Browse Source

Améliore la gestion de l'OTP, ajout du QR-code

Christophe HENRY 2 years ago
parent
commit
1fb93d25b2

+ 17 - 0
Functions.class.php

@@ -374,5 +374,22 @@ class Functions
     public static function isAjaxCall() {
         return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
     }
+
+    /**
+    * Charge dans la portée locale des variables de $_REQUEST
+    * Ex: chargeVarRequest('liste', 'var') créera $liste et $var venant de $_REQUEST
+    */ 
+    public static function chargeVarRequest() {
+        foreach (func_get_args() as $arg) {
+            global ${$arg};
+            if (array_key_exists($arg, $_REQUEST)) {
+                $valeur = $_REQUEST[$arg];
+            } else {
+                $valeur = '';
+            }
+            ${$arg} = $valeur;
+        }
+    }
+
 }
 ?>

+ 49 - 9
User.class.php

@@ -8,7 +8,12 @@
 
 class User extends MysqlEntity{
 
-    protected $id,$login,$password;
+    const OTP_INTERVAL = 30;
+    const OTP_DIGITS   = 8;
+    const OTP_DIGEST   = 'sha1';
+    private $otpControler;
+
+    protected $id,$login,$password,$otpSecret;
     protected $TABLE_NAME = 'user';
     protected $CLASS_NAME = 'User';
     protected $object_fields =
@@ -16,7 +21,7 @@ class User extends MysqlEntity{
         'id'=>'key',
         'login'=>'string',
         'password'=>'string',
-        'otpSeed'=>'string',
+        'otpSecret'=>'string',
     );
 
     function __construct(){
@@ -27,19 +32,40 @@ class User extends MysqlEntity{
         $this->id = $id;
     }
 
+    function isOtpSecretValid($otpSecret) {
+        // Teste si la longueur est d'au moins 8 caractères
+        // et en Base32: [A-Z] + [2-7]
+        return is_string($otpSecret) && preg_match('/^[a-zA-Z2-7]{8,}$/', $otpSecret);
+    }
+
+    protected function getOtpControler() {
+        if (empty($this->otpControler))
+            $this->otpControler = new \OTPHP\TOTP($this->otpSecret, array('interval'=>self::OTP_INTERVAL, 'digits'=>self::OTP_DIGITS, 'digest'=>self::OTP_DIGEST));
+        return $this->otpControler;
+    }
+
+    function getOtpKey() {
+        $otp = $this->getOtpControler();
+        return str_pad($otp->now(), $otp->digits, '0', STR_PAD_LEFT);
+    }
+
     function exist($login,$password,$salt='',$otpEntered=Null){
         $userManager = new User();
         $user = $userManager->load(array('login'=>$login,'password'=>User::encrypt($password,$salt)));
 
         if (false!=$user) {
-            $otpSeed = @$user->otpSeed; # Si champ null, la propriété n'existe pas !
-            if (!defined('OTP') || is_null($otpSeed) && is_null($otpEntered) ) {
-                return $user;
-            } else {
-                $otp = new \OTPHP\TOTP($otpSeed, array('interval'=>30, 'digits'=>8, 'digest'=>'sha1'));
-                if ($otp->verify($otpEntered) || $otp->verify($otpEntered, time()-10)) {
+            $otpSecret = $user->otpSecret;
+
+            global $configurationManager;
+            switch (True) {
+                case !$configurationManager->get('otpEnabled'):
+                case empty($otpSecret) && empty($otpEntered):
+                    // Pas d'OTP s'il est désactivé dans la configuration où s'il n'est pas demandé et fourni.
                     return $user;
-                }
+            }
+            $otp = $user->getOtpControler();
+            if ($otp->verify($otpEntered) || $otp->verify($otpEntered, time()-10)) {
+                return $user;
             }
         }
 
@@ -101,6 +127,20 @@ class User extends MysqlEntity{
         $this->password = User::encrypt($password,$salt);
     }
 
+    function getOtpSeed(){
+        return $this->otpSecret;
+    }
+
+    function setOtpSeed($otpSecret){
+        return $this->otpSecret = $otpSecret;
+    }
+
+    function resetPassword($resetPassword, $salt=''){
+        $this->setPassword($resetPassword, $salt);
+        $this->otpSecret = '';
+        $this->save();
+    }
+
     static function encrypt($password, $salt=''){
         return sha1($password.$salt);
     }

+ 13 - 8
action.php

@@ -117,8 +117,8 @@ switch ($action){
             $configurationManager->put('feedMaxEvents',$_['feedMaxEvents']);
             $configurationManager->put('language',$_['ChgLanguage']);
             $configurationManager->put('theme',$_['ChgTheme']);
+            $configurationManager->put('otpEnabled',$_['otpEnabled']);
 
-            $userManager->change(array('login'=>$_['login']),array('id'=>$myUser->getId()));
             if(trim($_['password'])!='') {
                 $salt = User::generateSalt();
                 $userManager->change(array('password'=>User::encrypt($_['password'], $salt)),array('id'=>$myUser->getId()));
@@ -137,6 +137,16 @@ switch ($action){
 
             }
 
+            # Modifications dans la base de données, la portée courante et la sesssion
+            # @TODO: gérer cela de façon centralisée
+            $otpSecret = $_['otpSecret'];
+            if ($myUser->isOtpSecretValid($otpSecret)) {
+                $userManager->change(array('login'=>$_['login'], 'otpSecret'=>$otpSecret),array('id'=>$myUser->getId()));
+                $myUser->setLogin($_['login']);
+                $myUser->setOtpSeed($otpSecret);
+                $_SESSION['currentUser'] = serialize($myUser);
+            }
+
     header('location: ./settings.php#preferenceBloc');
     break;
 
@@ -426,13 +436,8 @@ switch ($action){
                 if (false===$tmpUser) {
                     $message = "Unknown user '{$_['login']}'! No password reset.";
                 } else {
-                    $id = $tmpUser->getId();
-                    $salt = $configurationManager->get('cryptographicSalt');
-                    $userManager->change(
-                        array('password'=>User::encrypt($resetPassword, $salt)),
-                        array('id'=>$id)
-                    );
-                    $message = "User '{$_['login']}' (id=$id) Password reset to '$resetPassword'.";
+                    $tmpUser->resetPassword($resetPassword, $configurationManager->get('cryptographicSalt'));
+                    $message = "User '{$_['login']}' (id={$tmpUser->getId()}) Password reset to '$resetPassword'.";
                 }
             }
             error_log($message);

+ 2 - 2
index.php

@@ -28,8 +28,8 @@ $tpl->assign('allEvents',$eventManager->getEventCountPerFolder());
 //utilisé pour récupérer le statut d'un feed dans le template (en erreur ou ok)
 $feedState = new Feed();
 $tpl->assign('feedState',$feedState);
-
-$tpl->assign('otpEnabled', defined('OTP') && OTP);
+//afficher ou non le champ OTP
+$tpl->assign('otpEnabled', $configurationManager->get('otpEnabled'));
 
 $articleDisplayAuthor = $configurationManager->get('articleDisplayAuthor');
 $articleDisplayDate = $configurationManager->get('articleDisplayDate');

+ 0 - 1
locale/en.json

@@ -53,7 +53,6 @@
  "LEED_UPDATE_MESSAGE":"Leed has been updated. Refresh the page to return to your Leed.",
  "LOGIN":"Username",
  "NEW_ARTICLES":"new articles",
- "ONE_TIME_PASSWORD":"OTP Key",
  "OPML_FILE":"OPML File",
  "PASSWORD":"Password",
  "PENDING":"In progress...",

+ 0 - 1
locale/eo.json

@@ -53,7 +53,6 @@
  "LEED_UPDATE_MESSAGE":"Leed estis ĝisdatigita. Reŝargi la paĝon por reiri al via Leed.",
  "LOGIN":"Salutnomo",
  "NEW_ARTICLES":"Novaj artikoloj",
- "ONE_TIME_PASSWORD":"OTP ŝlosilo",
  "OPML_FILE":"Dosiero OPML",
  "PASSWORD":"Pasvorto",
  "PENDING":"Okazonta...",

+ 1 - 1
locale/es.json

@@ -67,4 +67,4 @@
  "SYNCHRONIZE_COFFEE_TIME":"NB : La syncronisación puede tomar cierto tiempo, deje su navegador y vaya tomarse un cafe. :)",
  "SYNCHRONIZE_NOW":"Syncronisar ahora",
  "YOU_MUST_BE_CONNECTED_ACTION":"Usted debe haber iniciado una sesión para continuar."
-}
+}

+ 0 - 1
locale/fr.json

@@ -53,7 +53,6 @@
  "LEED_UPDATE_MESSAGE":"Leed à été mis à jour. Rafraîchir la page pour retourner sur votre Leed.",
  "LOGIN":"Identifiant",
  "NEW_ARTICLES":"nouveaux articles",
- "ONE_TIME_PASSWORD":"Clé OTP",
  "OPML_FILE":"Fichier OPML",
  "PASSWORD":"Mot de passe",
  "PENDING":"En cours…",

+ 17 - 4
otTest.php

@@ -5,14 +5,27 @@
 
 require_once dirname(__FILE__).'/otphp/lib/otphp.php';
 
-$totp1 = new \OTPHP\TOTP('22222222', array('interval'=>30, 'digits'=>8, 'digest'=>'sha1'));
-$totp512 = new \OTPHP\TOTP('22222222', array('interval'=>30, 'digits'=>8, 'digest'=>'sha512'));
-print_r($totp);
+
+# Il faut faire comme ça : https://python-totp.herokuapp.com/
+# Ça génère un OTP aléatoire et le QR qui correspond. FreeOTP le récupère
+# d'un coup. Idéal à placer sur la gestion du compte pour activer l'OTP sans se
+# soucier du secret.
+
+$pass = '42432526';
+$pass = 'abcdefgh';
+$pass = 'abcdefgh23456';
+$pass = 'abcdeijrgfoaefgh23456';
+$pass = "yiemah3ShulaeXaichae";
+$pass = "yiemah3shulaexaichae";
+
+$pass = "yiemah3shul";
+$totp1 = new \OTPHP\TOTP($pass, array('interval'=>30, 'digits'=>8, 'digest'=>'sha1'));
+$totp2 = new \OTPHP\TOTP(strtoupper($pass), array('interval'=>30, 'digits'=>8, 'digest'=>'sha1'));
 
 
 while( True ){
     echo "1 ", str_pad($totp1->now(), $totp1->digits, '0', STR_PAD_LEFT)."\n";
-    echo "512 ", str_pad($totp512->now(), $totp512->digits, '0', STR_PAD_LEFT)."\n";
+    echo "2 ", str_pad($totp2->now(), $totp2->digits, '0', STR_PAD_LEFT)."\n";
     sleep(1);
 }
 

+ 1 - 0
otphp/vendor/base32.php

@@ -56,6 +56,7 @@ class Base32 {
     
     public static function decode($input) {
         if(empty($input)) return;
+        $input = strtoupper($input);
         $paddingCharCount = substr_count($input, self::$map[32]);
         $allowedValues = array(6,4,3,1,0);
         if(!in_array($paddingCharCount, $allowedValues)) return false;

File diff suppressed because it is too large
+ 3312 - 0
phpqrcode.php


+ 28 - 0
qrcode.php

@@ -0,0 +1,28 @@
+<?php
+
+require_once('common.php');
+if (empty($myUser)) exit();
+
+require_once('phpqrcode.php');
+
+$methode = array_keys($_REQUEST)[0];
+switch($methode) {
+    case 'qr': # qrcode.php?qr&label=A&user=B&key=C
+        Functions::chargeVarRequest('label', 'user', 'key', 'issuer', 'algorithm', 'digits', 'period');
+        $qrCode = "otpauth://totp/{$label}:{$user}?secret={$key}";
+        foreach (array('issuer', 'algorithm', 'digits', 'period') as $champ)
+            if (!empty(${$champ}))
+                $qrCode.="&{$champ}={${$champ}}";
+        break;
+    case 'txt': # qrcode.php?txt&TEXTE
+        $qrCode = substr($_SERVER['QUERY_STRING'], 1+strlen($methode));
+        break;
+    default:
+        $qrCode = '';
+}
+
+Functions::chargeVarRequest('_qrSize', '_qrMargin');
+if (empty($_qrSize))   $_qrSize   = 3;
+if (empty($_qrMargin)) $_qrMargin = 4;
+
+QRcode::png($qrCode, false, 'QR_LEVEL_H', $_qrSize, $_qrMargin);

+ 4 - 0
settings.php

@@ -8,6 +8,8 @@
 
 require_once('header.php');
 
+$tpl->assign('serviceUrl', rtrim($_SERVER['HTTP_HOST'].$cookiedir,'/'));
+
 $logger = new Logger('settings');
 $tpl->assign('logs',$logger->flushLogs());
 
@@ -46,6 +48,8 @@ $tpl->assign('articleDisplayFolderSort', $configurationManager->get('articleDisp
 $tpl->assign('articleDisplayMode', $configurationManager->get('articleDisplayMode'));
 $tpl->assign('optionFeedIsVerbose', $configurationManager->get('optionFeedIsVerbose'));
 
+$tpl->assign('otpEnabled', $configurationManager->get('otpEnabled'));
+
 //Suppression de l'état des plugins inexistants
 Plugin::pruneStates();
 

+ 1 - 1
templates/marigolds/header.html

@@ -35,7 +35,7 @@
                         <input id="inputlogin" type="text" class="miniInput left" name="login" placeholder="{function="_t('LOGIN')"}"/>
                         <input type="password" class="miniInput left" name="password" placeholder="{function="_t('PASSWORD')"}"/>
                         {if="$otpEnabled"}
-                        <input type="text" name="otp" class="miniInput left" autocomplete="off" placeholder="{function="_t('ONE_TIME_PASSWORD')"}"/>
+                        <input type="text" name="otp" class="miniInput left" autocomplete="off" placeholder="{function="_t('OTP_CODE')"}"/>
                         {/if}
                         <button class="left">GO!!</button>
                         <span id="rememberMe">

+ 5 - 0
templates/marigolds/locale/en.json

@@ -102,6 +102,11 @@
  "NO_INSTALLED_PLUGINS":"There is no installed plugin yet.",
  "OLDER":"oldest",
  "OPML_FILE":"OPML File",
+ "OTP_CODE": "OTP Code",
+ "OTP_DISABLED_EMPTY":"(disabled if empty)",
+ "OTP_SECRET":"OTP Secret",
+ "OTP_SETTINGS":"One Time Password Settings",
+ "OTP_SETTINGS_DESC":"Adds a connexion code, which changes every 30 secondes. Put here the OTP secret and scan it (of write it) in an application like <a href='https://freeotp.github.io/'>Free OTP</a>. Updated key: ",
  "PASSWORD":"Password",
  "PLUGINS":"Plugins",
  "PLUGINS_INSTALLED":"Plugins configuration",

+ 5 - 0
templates/marigolds/locale/eo.json

@@ -102,6 +102,11 @@
  "NO_INSTALLED_PLUGINS":"Neniu kromaĵo estas instalita nune.",
  "OLDER":"Pli maljuna",
  "OPML_FILE":"Dosiero OPML",
+ "OTP_CODE": "OTP Kodo",
+ "OTP_DISABLED_EMPTY":"(malŝaltita se malplena)",
+ "OTP_SECRET":"OTP Sekretaĵo",
+ "OTP_SETTINGS":"One Time Password Agordoj",
+ "OTP_SETTINGS_DESC":"Aldonas konektan kodon, kiu ŝanĝiĝas ĉiuj 30 sekundoj. Enigu tie la OTP sekretaĵon kaj skani ĝin (aŭ reskribu) en aplikaĵon kiel <a href='https://freeotp.github.io/'>Free OTP</a>. Nuna ŝlosilo: ",
  "PASSWORD":"Pasvorto",
  "PLUGINS":"Kromaĵoj",
  "PLUGINS_INSTALLED":"Agordoj de kromaĵoj",

+ 5 - 0
templates/marigolds/locale/es.json

@@ -102,6 +102,11 @@
  "NO_INSTALLED_PLUGINS":"No se ha instalado ningún complemento ya.",
  "OLDER":"Más antiguo",
  "OPML_FILE":"Archivo OPML",
+ "OTP_CODE": "",
+ "OTP_DISABLED_EMPTY":"",
+ "OTP_SECRET":"",
+ "OTP_SETTINGS":"",
+ "OTP_SETTINGS_DESC":"",
  "PASSWORD":"Contraseña",
  "PLUGINS":"Complementos",
  "PLUGINS_INSTALLED":"Configuración de los complementos",

+ 5 - 0
templates/marigolds/locale/fr.json

@@ -102,6 +102,11 @@
  "NO_INSTALLED_PLUGINS":"Aucun plugin n’est installé pour le moment.",
  "OLDER":"Plus vieux",
  "OPML_FILE":"Fichier OPML",
+ "OTP_CODE": "Code OTP",
+ "OTP_DISABLED_EMPTY":"(désactivé si vide)",
+ "OTP_SECRET":"Secret OTP",
+ "OTP_SETTINGS":"Réglages One Time Password",
+ "OTP_SETTINGS_DESC":"Ajoute un code à la connexion, qui change toutes les 30 secondes. Indiquer ici le secret OTP (au moins 8 lettres ou chiffres de 2 à 7) et le scanner (ou recopier) dans une application comme <a href='https://freeotp.github.io/'>Free OTP</a>. Clé actuelle : ",
  "PASSWORD":"Mot de passe",
  "PLUGINS":"Plugins",
  "PLUGINS_INSTALLED":"Configuration des plugins",

+ 9 - 0
templates/marigolds/settings.html

@@ -174,6 +174,15 @@
                         <p><label for="password">{function="_t('PASSWORD')"} :</label> <input type="text" id="password" name="password" autocomplete="off" value="" placeholder="{function="_t('INSTALL_DISPLAY_CLEAR')"}"></p>
                         <h4>{function="_t('LET_EMPTY_IF_NO_PASS_CHANGE')"}</h4>
                         <h4>{function="_t('HOWTO_RESET_PASSWORD')"}</h4>
+                        <fieldset>
+                            <img src="../../qrcode.php?qr&label={$serviceUrl}&user={$myUser->getLogin()}&key={$myUser->getOtpSeed()}&issuer={$serviceUrl}&algorithm=sha1&digits=8&period=30&_qrSize=4&_qrMargin=1" style='float:left;margin:0em 1em 1em 0em'/>
+                            <legend>{function="_t('OTP_SETTINGS')"}</legend>
+                            <input type="radio" {if="$otpEnabled"} checked="checked" {/if} value="1" id="otpEnabledYes" name="otpEnabled" /><label for="otpEnabledYes">{function="_t('YES')"}</label>
+                            <input type="radio" {if="!$otpEnabled"} checked="checked" {/if} value="0" id="otpEnabledNo" name="otpEnabled" /><label for="otpEnabledNo">{function="_t('NO')"}</label>
+                            <p>{function="_t('OTP_SETTINGS_DESC')"}{$myUser->getOtpKey()}.</p>
+                        <p><label for="otpSecret">{function="_t('OTP_SECRET')"} :</label> <input type="text" id="otpSecret" name="otpSecret" autocomplete="off" placeholder="{function="_t('OTP_DISABLED_EMPTY')"}" value="{$myUser->getOtpSeed()}">
+                        </p>
+                        </fieldset>
                     </section>
 
                     <section>

+ 1 - 1
updates/00007-otp-20160107.sql.dev

@@ -12,4 +12,4 @@
 --######################################################################################################
 
 -- Mise à jour table user
-ALTER TABLE `##MYSQL_PREFIX##user` ADD `otpSeed` VARCHAR(256) NULL DEFAULT NULL;
+ALTER TABLE `##MYSQL_PREFIX##user` ADD `otpSecret` VARCHAR(225);