Browse Source

Ajout de TOTP -- double authentification

L'ajout de TOTP ne change, normalement, rien au comportement habituel.
Pour l'activer en phase de développement:

1/ exécuter à la main le SQL de updates/00007-otp-20160107.sql.dev
2/ ajouter define('OTP', true); dans le constant.php
3/ ajouter manuellement le sel TOTP dans la table `user`
4/ utiliser le thème Marigolds
5/ utiliser FreeOTP (TOTP avec 8 chiffres)

/!\ Attention, le sel/secret est en base32 : les lettres de l'alphabet et les
chiffres de 2 à 7. Les chiffres 1, 8 et 9 sont interdits.

Dans le détail. Le define va activer la fonctionnalité, à ce stade il
est obligatoire (sinon erreurs dans le log) de mettre à jour à la main la base.
Elle n'est pas encore imposée (sql.dev) puisque encore en test.

Ensuite, l'ajout du sel va modifier le comportement à la connexion. En
plus d'un utilisateur et d'un mot de passe justes, il faudra rentrer un
mot de passe à usage unique. Il change toutes les 30 secondes.
Cependant, un OTP expiré peut encore servir pendant 10 secondes.

FreeOTP, disponible via F-Droid, permet d'obtenir la clé à usage unique.
Il faut indiquer le secret (le otpKey côté serveur), choisir TOTP et 8
chiffres.

Ça marche tel quel, mais il reste à faire :
1) les traductions manquantes
2) adapter marigolds_old
3) adapter l'installeur (champs de formulaires)
4) adapter la modification de la configuration
   - actif oui/non
   - sel (conformité base32)

Quand ce sera bon pour tous :
1) Forcer la mise à jour de la base de données.
2) Donner le choix du condensat, choix TOTP/HOTP, nombre de chiffres
3) Empêcher l'usage de 2 fois du même OTP pour le même utilisateur.
4) Adapter la procédure de réinitialiation du mot de passe.
5) Proposer une génération aléatoire d'OTP.
Christophe HENRY 6 years ago
parent
commit
b5cb0cfe1f

+ 18 - 3
User.class.php

@@ -15,7 +15,8 @@ class User extends MysqlEntity{
     array(
         'id'=>'key',
         'login'=>'string',
-        'password'=>'string'
+        'password'=>'string',
+        'otpSeed'=>'string',
     );
 
     function __construct(){
@@ -26,9 +27,23 @@ class User extends MysqlEntity{
         $this->id = $id;
     }
 
-    function exist($login,$password,$salt=''){
+    function exist($login,$password,$salt='',$otpEntered=Null){
         $userManager = new User();
-        return $userManager->load(array('login'=>$login,'password'=>User::encrypt($password,$salt)));
+        $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)) {
+                    return $user;
+                }
+            }
+        }
+
+        return false;
     }
 
     function get($login){

+ 1 - 1
action.php

@@ -450,7 +450,7 @@ switch ($action){
         }else{
             $salt = $configurationManager->get('cryptographicSalt');
             if (empty($salt)) $salt = '';
-            $user = $userManager->exist($_['login'],$_['password'],$salt);
+            $user = $userManager->exist($_['login'],$_['password'],$salt,@$_['otp']);
             if($user==false){
                 header('location: ./index.php?action=wrongLogin');
             }else{

+ 1 - 0
common.php

@@ -39,6 +39,7 @@ $start=microtime(true);
 require_once('constant.php');
 require_once('RainTPL.php');
 require_once('i18n.php');
+require_once('otphp/lib/otphp.php');
 class_exists('Plugin') or require_once('Plugin.class.php');
 class_exists('MysqlEntity') or require_once('MysqlEntity.class.php');
 class_exists('Update') or require_once('Update.class.php');

+ 2 - 0
index.php

@@ -29,6 +29,8 @@ $tpl->assign('allEvents',$eventManager->getEventCountPerFolder());
 $feedState = new Feed();
 $tpl->assign('feedState',$feedState);
 
+$tpl->assign('otpEnabled', defined('OTP') && OTP);
+
 $articleDisplayAuthor = $configurationManager->get('articleDisplayAuthor');
 $articleDisplayDate = $configurationManager->get('articleDisplayDate');
 $articleDisplayFolderSort = $configurationManager->get('articleDisplayFolderSort');

+ 2 - 1
locale/en.json

@@ -53,6 +53,7 @@
  "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...",
@@ -67,4 +68,4 @@
  "SYNCHRONIZE_COFFEE_TIME":"NB : The synchronization can take time, leave your browser do the job and go grab a coffee :).",
  "SYNCHRONIZE_NOW":"Synchronize now",
  "YOU_MUST_BE_CONNECTED_ACTION":"You must be logged in for this action."
-}
+}

+ 1 - 0
locale/eo.json

@@ -53,6 +53,7 @@
  "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...",

+ 2 - 1
locale/fr.json

@@ -53,6 +53,7 @@
  "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…",
@@ -67,4 +68,4 @@
  "SYNCHRONIZE_COFFEE_TIME":"NB : La synchronisation peut prendre un certain temps, laissez votre navigateur tourner et allez vous prendre un café. :)",
  "SYNCHRONIZE_NOW":"Synchroniser maintenant",
  "YOU_MUST_BE_CONNECTED_ACTION":"Vous devez être connecté pour effectuer cette action."
-}
+}

+ 22 - 0
otTest.php

@@ -0,0 +1,22 @@
+<?php
+
+// https://github.com/lelag/otphp
+// http://sebsauvage.net/wiki/doku.php?id=totp
+
+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);
+
+
+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";
+    sleep(1);
+}
+
+// OTP verified for current time
+// $totp->verify(492039); // => true
+// //30s later
+// $totp->verify(492039); // => false

+ 20 - 0
otphp/LICENCE

@@ -0,0 +1,20 @@
+Copyright (c) 2011 Le Lag 
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+

+ 71 - 0
otphp/README.markdown

@@ -0,0 +1,71 @@
+# OTPHP - A PHP One Time Password Library
+
+A php library for generating one time passwords according to [ RFC 4226 ](http://tools.ietf.org/html/rfc4226) and the [ HOTP RFC ](http://tools.ietf.org/html/draft-mraihi-totp-timebased-00)
+
+This is compatible with Google Authenticator apps available for Android and iPhone, and now in use on GMail
+
+This is a port of the rotp ruby library available at https://github.com/mdp/rotp
+
+
+## Quick overview of using One Time Passwords on your phone
+
+* OTP's involve a shared secret, stored both on the phone and the server
+* OTP's can be generated on a phone without internet connectivity(AT&T mode)
+* OTP's should always be used as a second factor of authentication(if your phone is lost, you account is still secured with a password)
+* Google Authenticator allows you to store multiple OTP secrets and provision those using a QR Code(no more typing in the secret)
+
+## Installation
+
+   clone this repository and include lib/otphp.php in your project. 
+
+## Use
+
+### Time based OTP's
+
+    $totp = new \OTPHP\TOTP("base32secret3232");
+    $totp->now(); // => 492039
+
+    // OTP verified for current time
+    $totp->verify(492039); // => true
+    //30s later
+    $totp->verify(492039); // => false
+
+### Counter based OTP's
+
+    $hotp = new \OTPHP\HOTP("base32secretkey3232");
+    $hotp->at(0); // => 260182
+    $hotp->at(1); // => 55283
+    $hotp->at(1401); // => 316439
+
+    // OTP verified with a counter
+    $totp->verify(316439, 1401); // => true
+    $totp->verify(316439, 1402); // => false
+
+### Google Authenticator Compatible
+
+The library works with the Google Authenticator iPhone and Android app, and also
+includes the ability to generate provisioning URI's for use with the QR Code scanner
+built into the app.
+
+    $totp->provisioning_uri(); // => 'otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP'
+    $hotp->provisioning_uri(); // => 'otpauth://hotp/alice@google.com?secret=JBSWY3DPEHPK3PXP&counter=0'
+
+This can then be rendered as a QR Code which can then be scanned and added to the users
+list of OTP credentials.
+
+#### Working example
+
+Scan the following barcode with your phone, using Google Authenticator
+
+![QR Code for OTP](http://chart.apis.google.com/chart?cht=qr&chs=250x250&chl=otpauth%3A%2F%2Ftotp%2Falice%40google.com%3Fsecret%3DJBSWY3DPEHPK3PXP)
+
+Now run the following and compare the output
+
+    <?php
+    require_once('otphp/lib/otphp.php');
+    $totp = new \OTPHP\TOTP("JBSWY3DPEHPK3PXP");
+    echo "Current OTP: ". $totp->now();
+
+## Licence
+
+This software is release under MIT licence.

+ 74 - 0
otphp/lib/hotp.php

@@ -0,0 +1,74 @@
+<?php
+/*
+ * Copyright (c) 2011 Le Lag 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+namespace OTPHP {
+  /**
+   * HOTP - One time password generator 
+   * 
+   * The HOTP class allow for the generation 
+   * and verification of one-time password using 
+   * the HOTP specified algorithm.
+   *
+   * This class is meant to be compatible with 
+   * Google Authenticator
+   *
+   * This class was originally ported from the rotp
+   * ruby library available at https://github.com/mdp/rotp
+   */
+  class HOTP extends OTP {
+    /**
+     *  Get the password for a specific counter value
+     *  @param integer $count the counter which is used to
+     *  seed the hmac hash function.
+     *  @return integer the One Time Password
+     */
+    public function at($count) {
+      return $this->generateOTP($count);
+    }
+
+
+    /**
+     * Verify if a password is valid for a specific counter value
+     *
+     * @param integer $otp the one-time password 
+     * @param integer $counter the counter value
+     * @return  bool true if the counter is valid, false otherwise
+     */
+    public function verify($otp, $counter) {
+      return ($otp == $this->at($counter));
+    }
+
+    /**
+     * Returns the uri for a specific secret for hotp method.
+     * Can be encoded as a image for simple configuration in 
+     * Google Authenticator.
+     *
+     * @param string $name the name of the account / profile
+     * @param integer $initial_count the initial counter 
+     * @return string the uri for the hmac secret
+     */
+    public function provisioning_uri($name, $initial_count) {
+      return "otpauth://hotp/".urlencode($name)."?secret={$this->secret}&counter=$initial_count";
+    }
+  }
+
+}

+ 120 - 0
otphp/lib/otp.php

@@ -0,0 +1,120 @@
+<?php
+/*
+ * Copyright (c) 2011 Le Lag 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+namespace OTPHP {
+/**
+ * One Time Password Generator 
+ * 
+ * The OTP class allow the generation of one-time
+ * password that is described in rfc 4xxx.
+ * 
+ * This is class is meant to be compatible with 
+ * Google Authenticator.
+ *
+ * This class was originally ported from the rotp
+ * ruby library available at https://github.com/mdp/rotp
+ */
+class OTP {
+    /**
+     * The base32 encoded secret key
+     * @var string
+     */
+    public $secret;
+
+    /**
+     * The algorithm used for the hmac hash function
+     * @var string
+     */
+    public $digest;
+
+    /**
+     * The number of digits in the one-time password
+     * @var integer
+     */ 
+    public $digits;
+
+    /**
+     * Constructor for the OTP class
+     * @param string $secret the secret key
+     * @param array $opt options array can contain the
+     * following keys :
+     *   @param integer digits : the number of digits in the one time password
+     *   Currently Google Authenticator only support 6. Defaults to 6.
+     *   @param string digest : the algorithm used for the hmac hash function
+     *   Google Authenticator only support sha1. Defaults to sha1
+     *
+     * @return new OTP class.
+     */
+    public function __construct($secret, $opt = Array()) {
+      $this->digits = isset($opt['digits']) ? $opt['digits'] : 6;
+      $this->digest = isset($opt['digest']) ? $opt['digest'] : 'sha1';
+      $this->secret = $secret;
+    }
+
+    /**
+     * Generate a one-time password
+     *
+     * @param integer $input : number used to seed the hmac hash function.
+     * This number is usually a counter (HOTP) or calculated based on the current
+     * timestamp (see TOTP class).
+     * @return integer the one-time password 
+     */
+    public function generateOTP($input) {
+      $hash = hash_hmac($this->digest, $this->intToBytestring($input), $this->byteSecret());
+      foreach(str_split($hash, 2) as $hex) { // stupid PHP has bin2hex but no hex2bin WTF
+        $hmac[] = hexdec($hex);
+      }
+      $offset = $hmac[19] & 0xf;
+      $code = ($hmac[$offset+0] & 0x7F) << 24 |
+        ($hmac[$offset + 1] & 0xFF) << 16 |
+        ($hmac[$offset + 2] & 0xFF) << 8 |
+        ($hmac[$offset + 3] & 0xFF);
+      return $code % pow(10, $this->digits);
+    }
+
+    /**
+     * Returns the binary value of the base32 encoded secret
+     * @access private
+     * This method should be private but was left public for
+     * phpunit tests to work.
+     * @return binary secret key
+     */
+    public function byteSecret() {
+      return \Base32::decode($this->secret);
+    }
+
+    /**
+     * Turns an integer in a OATH bytestring
+     * @param integer $int
+     * @access private
+     * @return string bytestring
+     */
+    public function intToBytestring($int) {
+      $result = Array();
+      while($int != 0) {
+        $result[] = chr($int & 0xFF);
+        $int >>= 8;
+      }
+      return str_pad(join(array_reverse($result)), 8, "\000", STR_PAD_LEFT);
+    }
+  }
+}

+ 27 - 0
otphp/lib/otphp.php

@@ -0,0 +1,27 @@
+<?php
+/*
+ * Copyright (c) 2011 Le Lag 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+require_once dirname(__FILE__).'/../vendor/libs.php';
+require_once dirname(__FILE__).'/otp.php';
+require_once dirname(__FILE__).'/hotp.php';
+require_once dirname(__FILE__).'/totp.php';
+

+ 106 - 0
otphp/lib/totp.php

@@ -0,0 +1,106 @@
+<?php
+/*
+ * Copyright (c) 2011 Le Lag 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+namespace OTPHP {
+  /**
+   * TOTP - One time password generator 
+   * 
+   * The TOTP class allow for the generation 
+   * and verification of one-time password using 
+   * the TOTP specified algorithm.
+   *
+   * This class is meant to be compatible with 
+   * Google Authenticator
+   *
+   * This class was originally ported from the rotp
+   * ruby library available at https://github.com/mdp/rotp
+   */
+  class TOTP extends OTP {
+    /**
+     * The interval in seconds for a one-time password timeframe
+     * Defaults to 30
+     * @var integer
+     */
+    public $interval;
+
+    public function __construct($s, $opt = Array()) {
+      $this->interval = isset($opt['interval']) ? $opt['interval'] : 30;
+      parent::__construct($s, $opt);
+    }
+
+    /**
+     *  Get the password for a specific timestamp value 
+     *
+     *  @param integer $timestamp the timestamp which is timecoded and 
+     *  used to seed the hmac hash function.
+     *  @return integer the One Time Password
+     */
+    public function at($timestamp) {
+      return $this->generateOTP($this->timecode($timestamp));
+    }
+
+    /**
+     *  Get the password for the current timestamp value 
+     *
+     *  @return integer the current One Time Password
+     */
+    public function now() {
+      return $this->generateOTP($this->timecode(time()));
+    }
+
+    /**
+     * Verify if a password is valid for a specific counter value
+     *
+     * @param integer $otp the one-time password 
+     * @param integer $timestamp the timestamp for the a given time, defaults to current time.
+     * @return  bool true if the counter is valid, false otherwise
+     */
+    public function verify($otp, $timestamp = null) {
+      if($timestamp === null)
+        $timestamp = time();
+      return ($otp == $this->at($timestamp));
+    }
+
+    /**
+     * Returns the uri for a specific secret for totp method.
+     * Can be encoded as a image for simple configuration in 
+     * Google Authenticator.
+     *
+     * @param string $name the name of the account / profile
+     * @return string the uri for the hmac secret
+     */
+    public function provisioning_uri($name) {
+      return "otpauth://totp/".urlencode($name)."?secret={$this->secret}";
+    }
+
+    /**
+     * Transform a timestamp in a counter based on specified internal
+     *
+     * @param integer $timestamp
+     * @return integer the timecode
+     */
+    protected function timecode($timestamp) {
+      return (int)( (((int)$timestamp * 1000) / ($this->interval * 1000)));
+    }
+  }
+
+}

+ 45 - 0
otphp/tests/HOTPTest.php

@@ -0,0 +1,45 @@
+<?php
+/*
+ * Copyright (c) 2011 Le Lag 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+require_once dirname(__FILE__).'/../lib/otphp.php';
+
+class HOPTTest extends PHPUnit_Framework_TestCase {
+  public function test_it_gets_the_good_code() {
+    $o = new \OTPHP\HOTP('JDDK4U6G3BJLEZ7Y');
+    $this->assertEquals(855783,$o->at(0));
+    $this->assertEquals(549607,$o->at(500));
+    $this->assertEquals(654666,$o->at(1500));
+  }
+
+  public function test_it_verify_the_code() {
+    $o = new \OTPHP\HOTP('JDDK4U6G3BJLEZ7Y');
+    $this->assertTrue($o->verify(855783, 0));
+    $this->assertTrue($o->verify(549607, 500));
+    $this->assertTrue($o->verify(654666, 1500));
+  }
+
+  public function test_it_returns_the_provisioning_uri() {
+    $o = new \OTPHP\HOTP('JDDK4U6G3BJLEZ7Y');
+    $this->assertEquals("otpauth://hotp/name?secret=JDDK4U6G3BJLEZ7Y&counter=0",
+      $o->provisioning_uri('name', 0));
+  }
+}

+ 46 - 0
otphp/tests/OTPTest.php

@@ -0,0 +1,46 @@
+<?php
+/*
+ * Copyright (c) 2011 Le Lag 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+require_once dirname(__FILE__).'/../lib/otphp.php';
+
+class TestOTP extends PHPUnit_Framework_TestCase {
+
+  public function test_it_decodes_the_secret() {
+    $o = new \OTPHP\OTP('JDDK4U6G3BJLEZ7Y');
+    $this->assertEquals("H\306\256S\306\330R\262g\370", $o->byteSecret());
+  }
+
+  public function test_it_turns_an_int_into_bytestring() {
+    $o = new \OTPHP\OTP('JDDK4U6G3BJLEZ7Y');
+    $this->assertEquals("\000\000\000\000\000\000\000\000", $o->intToBytestring(0));
+    $this->assertEquals("\000\000\000\000\000\000\000\001", $o->intToBytestring(1));
+    $this->assertEquals("\000\000\000\000\000\000\001\364", $o->intToBytestring(500));
+    $this->assertEquals("\000\000\000\000\000\000\005\334", $o->intToBytestring(1500));
+  }
+
+  public function test_it_generate_otp() {
+    $o = new \OTPHP\OTP('JDDK4U6G3BJLEZ7Y');
+    $this->assertEquals(855783, $o->generateOTP(0));
+    $this->assertEquals(549607, $o->generateOTP(500));
+    $this->assertEquals(654666, $o->generateOTP(1500));
+  }
+}

+ 53 - 0
otphp/tests/TOTPTest.php

@@ -0,0 +1,53 @@
+<?php
+/*
+ * Copyright (c) 2011 Le Lag 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+require_once dirname(__FILE__).'/../lib/otphp.php';
+
+class TOPTTest extends PHPUnit_Framework_TestCase {
+
+  public function test_it_has_an_interval() {
+    $o = new \OTPHP\TOTP('JDDK4U6G3BJLEZ7Y');
+    $this->assertEquals(30,$o->interval);
+    $b = new \OTPHP\TOTP('JDDK4U6G3BJLEZ7Y', Array('interval'=>60));
+    $this->assertEquals(60,$b->interval);
+  }
+
+  public function test_it_gets_the_good_code_at_given_times() {
+    $o = new \OTPHP\TOTP('JDDK4U6G3BJLEZ7Y');
+    $this->assertEquals(855783,$o->at(0));
+    $this->assertEquals(762124,$o->at(319690800));
+    $this->assertEquals(139664,$o->at(1301012137));
+  }
+
+  public function test_it_verify_the_code() {
+    $o = new \OTPHP\TOTP('JDDK4U6G3BJLEZ7Y');
+    $this->assertTrue($o->verify(855783, 0));
+    $this->assertTrue($o->verify(762124, 319690800));
+    $this->assertTrue($o->verify(139664, 1301012137));
+  }
+
+  public function test_it_returns_the_provisioning_uri() {
+    $o = new \OTPHP\TOTP('JDDK4U6G3BJLEZ7Y');
+    $this->assertEquals("otpauth://totp/name?secret=JDDK4U6G3BJLEZ7Y",
+      $o->provisioning_uri('name'));
+  }
+}

+ 29 - 0
otphp/tests/TestTest.php

@@ -0,0 +1,29 @@
+<?php
+/*
+ * Copyright (c) 2011 Le Lag 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+require_once dirname(__FILE__).'/../lib/otphp.php';
+
+class TestTest extends PHPUnit_Framework_TestCase {
+  public function testThatPHPUnitWorks() {
+    $this->assertEquals(1,1);
+  }
+}

+ 83 - 0
otphp/vendor/base32.php

@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * Encode in Base32 based on RFC 4648.
+ * Requires 20% more space than base64  
+ * Great for case-insensitive filesystems like Windows and URL's  (except for = char which can be excluded using the pad option for urls)
+ *
+ * @package default
+ * @author Bryan Ruiz
+ **/
+class Base32 {
+
+   private static $map = array(
+        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', //  7
+        'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
+        'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
+        'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
+        '='  // padding char
+    );
+    
+   private static $flippedMap = array(
+        'A'=>'0', 'B'=>'1', 'C'=>'2', 'D'=>'3', 'E'=>'4', 'F'=>'5', 'G'=>'6', 'H'=>'7',
+        'I'=>'8', 'J'=>'9', 'K'=>'10', 'L'=>'11', 'M'=>'12', 'N'=>'13', 'O'=>'14', 'P'=>'15',
+        'Q'=>'16', 'R'=>'17', 'S'=>'18', 'T'=>'19', 'U'=>'20', 'V'=>'21', 'W'=>'22', 'X'=>'23',
+        'Y'=>'24', 'Z'=>'25', '2'=>'26', '3'=>'27', '4'=>'28', '5'=>'29', '6'=>'30', '7'=>'31'
+    );
+    
+    /**
+     *    Use padding false when encoding for urls
+     *
+     * @return base32 encoded string
+     * @author Bryan Ruiz
+     **/
+    public static function encode($input, $padding = true) {
+        if(empty($input)) return "";
+        $input = str_split($input);
+        $binaryString = "";
+        for($i = 0; $i < count($input); $i++) {
+            $binaryString .= str_pad(base_convert(ord($input[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
+        }
+        $fiveBitBinaryArray = str_split($binaryString, 5);
+        $base32 = "";
+        $i=0;
+        while($i < count($fiveBitBinaryArray)) {    
+            $base32 .= self::$map[base_convert(str_pad($fiveBitBinaryArray[$i], 5,'0'), 2, 10)];
+            $i++;
+        }
+        if($padding && ($x = strlen($binaryString) % 40) != 0) {
+            if($x == 8) $base32 .= str_repeat(self::$map[32], 6);
+            else if($x == 16) $base32 .= str_repeat(self::$map[32], 4);
+            else if($x == 24) $base32 .= str_repeat(self::$map[32], 3);
+            else if($x == 32) $base32 .= self::$map[32];
+        }
+        return $base32;
+    }
+    
+    public static function decode($input) {
+        if(empty($input)) return;
+        $paddingCharCount = substr_count($input, self::$map[32]);
+        $allowedValues = array(6,4,3,1,0);
+        if(!in_array($paddingCharCount, $allowedValues)) return false;
+        for($i=0; $i<4; $i++){ 
+            if($paddingCharCount == $allowedValues[$i] && 
+                substr($input, -($allowedValues[$i])) != str_repeat(self::$map[32], $allowedValues[$i])) return false;
+        }
+        $input = str_replace('=','', $input);
+        $input = str_split($input);
+        $binaryString = "";
+        for($i=0; $i < count($input); $i = $i+8) {
+            $x = "";
+            if(!in_array($input[$i], self::$map)) return false;
+            for($j=0; $j < 8; $j++) {
+                $x .= str_pad(base_convert(@self::$flippedMap[@$input[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
+            }
+            $eightBits = str_split($x, 8);
+            for($z = 0; $z < count($eightBits); $z++) {
+                $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:"";
+            }
+        }
+        return $binaryString;
+    }
+}
+

+ 26 - 0
otphp/vendor/libs.php

@@ -0,0 +1,26 @@
+<?php
+/*
+ * Copyright (c) 2011 Le Lag 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+// Add any needed third party library to this directory
+
+//require_once dirname(__FILE__).'/some_lib/lib.php';
+require_once dirname(__FILE__).'/base32.php';

+ 3 - 0
templates/marigolds/header.html

@@ -34,6 +34,9 @@
                     <form action="action.php?action=login" method="POST" class="{$wrongLoginClass}">
                         <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')"}"/>
+                        {/if}
                         <button class="left">GO!!</button>
                         <span id="rememberMe">
                             <input type="checkbox" name="rememberMe">

+ 15 - 0
updates/00007-otp-20160107.sql.dev

@@ -0,0 +1,15 @@
+--######################################################################################################
+--#####
+--#####     MISE À JOUR Base de données de Leed
+--#####			Date : 07/01/2017
+--#####			Version Leed : v1.7
+--#####
+--##### 		Préfixe des tables : ##MYSQL_PREFIX## est remplacé automatiquement
+--#####
+--##### 		Feature(s) :
+--#####			- Champs pour le One Time Password des utilisateurs
+--#####
+--######################################################################################################
+
+-- Mise à jour table user
+ALTER TABLE `##MYSQL_PREFIX##user` ADD `otpSeed` VARCHAR(256) NULL DEFAULT NULL;