Overview
Comment:Ajout vérification de mot de passe compromis avec l'API HaveIBeenPwned, et mot de passe minimum de 10 caractères au lieu de 8
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 08954e712ceeeb83d5906725b108bc56ab0c3b44
User & Date: bohwaz on 2019-12-03 17:40:32
Other Links: manifest | tags
Context
2019-12-03
17:45
Amélioration fichiers .htaccess pour l'hébergement mutualisé check-in: 72097b2839 user: bohwaz tags: trunk
17:40
Ajout vérification de mot de passe compromis avec l'API HaveIBeenPwned, et mot de passe minimum de 10 caractères au lieu de 8 check-in: 08954e712c user: bohwaz tags: trunk
2019-12-01
20:04
Uniformisation de la position des actions à droite des tableaux, ajout d'une action pour voir les détails de l'écriture check-in: be5b3d5ef5 user: bohwaz tags: trunk
Changes

Modified src/include/data/schema.sql from [efe750515a] to [c8df01c2dd].

394
395
396
397
398
399
400














    id_membre INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE, -- Si non NULL, alors la recherche ne sera visible que par le membre associé
    intitule TEXT NOT NULL,
    creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(creation) IS NOT NULL AND datetime(creation) = creation),
    cible TEXT NOT NULL, -- "membres" ou "compta_journal"
    type TEXT NOT NULL, -- "json" ou "sql"
    contenu TEXT NOT NULL
);





















>
>
>
>
>
>
>
>
>
>
>
>
>
>
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
    id_membre INTEGER NULL REFERENCES membres (id) ON DELETE CASCADE, -- Si non NULL, alors la recherche ne sera visible que par le membre associé
    intitule TEXT NOT NULL,
    creation TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP CHECK (datetime(creation) IS NOT NULL AND datetime(creation) = creation),
    cible TEXT NOT NULL, -- "membres" ou "compta_journal"
    type TEXT NOT NULL, -- "json" ou "sql"
    contenu TEXT NOT NULL
);


CREATE TABLE IF NOT EXISTS compromised_passwords_cache
-- Cache des hash de mots de passe compromis
(
    hash TEXT NOT NULL PRIMARY KEY
);

CREATE TABLE IF NOT EXISTS compromised_passwords_cache_ranges
-- Cache des préfixes de mots de passe compromis
(
    prefix TEXT NOT NULL PRIMARY KEY,
    date INTEGER NOT NULL
);

Modified src/include/lib/Garradin/Membres.php from [84b4e80ec3] to [25c189b224].

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
...
171
172
173
174
175
176
177

178
179
180
181
182
183
184
185
...
226
227
228
229
230
231
232

233
234
235
236
237
238
239
240
    const DROIT_AUCUN = 0;
    const DROIT_ACCES = 1;
    const DROIT_ECRITURE = 2;
    const DROIT_ADMIN = 9;

    const ITEMS_PER_PAGE = 50;

    static protected function _getSalt($length)
    {
        static $str = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        
        $out = '';
        $max = strlen($str) - 1;

        for ($i = 0; $i < $length; $i++)
        {
            $random = Security::random_int(0, $max);
            $out .= $str[$random];
        }

        return $out;
    }

    static public function hashPassword($password)
    {
        // Remove NUL bytes
        // see http://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html
        $password = str_replace("\0", '', $password);

        return password_hash($password, \PASSWORD_DEFAULT);
    }

    // Gestion des données ///////////////////////////////////////////////////////

    public function _checkFields(&$data, $check_editable = true, $check_password = true)
    {
        $champs = Config::getInstance()->get('champs_membres');

        foreach ($champs->getAll() as $key=>$config)
................................................................................
        if (isset($data[$id]) && $db->test('membres', $id . ' = ? COLLATE NOCASE', $data[$id]))
        {
            throw new UserException('La valeur du champ '.$id.' est déjà utilisée par un autre membre, hors ce champ doit être unique à chaque membre.');
        }

        if (isset($data['passe']) && trim($data['passe']) != '')
        {

            $data['passe'] = self::hashPassword($data['passe']);
        }
        else
        {
            unset($data['passe']);
        }

        if (empty($data['id_categorie']))
................................................................................
            {
                throw new UserException('Ce numéro est déjà attribué à un autre membre.');
            }
        }

        if (!empty($data['passe']) && trim($data['passe']))
        {

            $data['passe'] = self::hashPassword($data['passe']);
        }
        else
        {
            unset($data['passe']);
        }

        if (isset($data['id_categorie']) && empty($data['id_categorie']))







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







 







>
|







 







>
|







11
12
13
14
15
16
17

























18
19
20
21
22
23
24
...
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
...
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
    const DROIT_AUCUN = 0;
    const DROIT_ACCES = 1;
    const DROIT_ECRITURE = 2;
    const DROIT_ADMIN = 9;

    const ITEMS_PER_PAGE = 50;


























    // Gestion des données ///////////////////////////////////////////////////////

    public function _checkFields(&$data, $check_editable = true, $check_password = true)
    {
        $champs = Config::getInstance()->get('champs_membres');

        foreach ($champs->getAll() as $key=>$config)
................................................................................
        if (isset($data[$id]) && $db->test('membres', $id . ' = ? COLLATE NOCASE', $data[$id]))
        {
            throw new UserException('La valeur du champ '.$id.' est déjà utilisée par un autre membre, hors ce champ doit être unique à chaque membre.');
        }

        if (isset($data['passe']) && trim($data['passe']) != '')
        {
            Session::checkPasswordValidity($data['passe']);
            $data['passe'] = Session::hashPassword($data['passe']);
        }
        else
        {
            unset($data['passe']);
        }

        if (empty($data['id_categorie']))
................................................................................
            {
                throw new UserException('Ce numéro est déjà attribué à un autre membre.');
            }
        }

        if (!empty($data['passe']) && trim($data['passe']))
        {
            Session::checkPasswordValidity($data['passe']);
            $data['passe'] = Session::hashPassword($data['passe']);
        }
        else
        {
            unset($data['passe']);
        }

        if (isset($data['id_categorie']) && empty($data['id_categorie']))

Modified src/include/lib/Garradin/Membres/Session.php from [b3c77f939b] to [13db0ba2f7].

11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26















27
28
29
30
31
32
33
...
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
...
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
...
348
349
350
351
352
353
354
355
356
357
358
359
360

361
362
363
364
365
366
367
use const Garradin\SECRET_KEY;
use const Garradin\WWW_URL;
use const Garradin\ADMIN_URL;

use KD2\Security;
use KD2\Security_OTP;
use KD2\QRCode;


class Session extends \KD2\UserSession
{
	// Personalisation de la config de UserSession
	protected $cookie_name = 'gdin';
	protected $remember_me_cookie_name = 'gdinp';
	protected $remember_me_expiry = '+3 months';

	const MINIMUM_PASSWORD_LENGTH = 8;
















	// Extension des méthodes de UserSession
	public function __construct()
	{
		$url = parse_url(ADMIN_URL);

		//throw new \Exception('lol');
................................................................................
		if (Security_OTP::TOTP($secret, $code))
		{
			return true;
		}

		// Vérifier encore, mais avec le temps NTP
		// au cas où l'horloge du serveur n'est pas à l'heure
		if (\Garradin\NTP_SERVER 
			&& ($time = Security_OTP::getTimeFromNTP(\Garradin\NTP_SERVER))
			&& Security_OTP::TOTP($secret, $code, $time))
		{
			return true;
		}

		return false;
................................................................................
		$password_confirm = trim($password_confirm);

		if (!hash_equals($password, $password_confirm))
		{
			throw new UserException('Le mot de passe et sa vérification ne sont pas identiques.');
		}

		if (strlen($password) < self::MINIMUM_PASSWORD_LENGTH)
		{
			throw new UserException(sprintf('Le mot de passe doit faire au moins %d caractères.', self::MINIMUM_PASSWORD_LENGTH));
		}

		$password = Membres::hashPassword($password);

		$message = "Bonjour,\n\nLe mot de passe de votre compte a bien été modifié.\n\n";
		$message.= "Votre adresse email : ".$membre->email."\n";
		$message.= "La demande émanait de l'adresse IP : ".Utils::getIP()."\n\n";
		$message.= "Si vous n'avez pas demandé à changer votre mot de passe, merci de nous le signaler.";

		DB::getInstance()->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$membre->id]);
................................................................................
			{
				throw new \RuntimeException(sprintf('Le champ %s n\'est pas autorisé dans cette méthode.', $key));
			}
		}

		if (isset($data['passe']) && trim($data['passe']) !== '')
		{
			if (strlen($data['passe']) < self::MINIMUM_PASSWORD_LENGTH)
			{
				throw new UserException(sprintf('Le mot de passe doit faire au moins %d caractères.', self::MINIMUM_PASSWORD_LENGTH));
			}

			$data['passe'] = Membres::hashPassword(trim($data['passe']));

		}
		else
		{
			unset($data['passe']);
		}

		if (isset($data['clef_pgp']) && trim($data['clef_pgp']) !== '')







>








|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|







 







|
|
<
<
<
|







 







|
|
|
|
<
<
>







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
...
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
...
277
278
279
280
281
282
283
284
285



286
287
288
289
290
291
292
293
...
361
362
363
364
365
366
367
368
369
370
371


372
373
374
375
376
377
378
379
use const Garradin\SECRET_KEY;
use const Garradin\WWW_URL;
use const Garradin\ADMIN_URL;

use KD2\Security;
use KD2\Security_OTP;
use KD2\QRCode;
use KD2\HTTP;

class Session extends \KD2\UserSession
{
	// Personalisation de la config de UserSession
	protected $cookie_name = 'gdin';
	protected $remember_me_cookie_name = 'gdinp';
	protected $remember_me_expiry = '+3 months';

	const MINIMUM_PASSWORD_LENGTH = 10;

	static public function checkPasswordValidity($password)
	{
		if (strlen($password) < self::MINIMUM_PASSWORD_LENGTH)
		{
			throw new UserException(sprintf('Le mot de passe doit faire au moins %d caractères.', self::MINIMUM_PASSWORD_LENGTH));
		}

		$session = new Session(DB::getInstance());
		$session->http = new HTTP;

		if ($session->isPasswordCompromised($password)) {
			throw new UserException('Ce mot de passe figure dans une liste de mots de passe compromis. Si vous l\'avez utilisé sur d\'autres sites il est recommandé de le changer sur ces autres sites également.');
		}
	}

	// Extension des méthodes de UserSession
	public function __construct()
	{
		$url = parse_url(ADMIN_URL);

		//throw new \Exception('lol');
................................................................................
		if (Security_OTP::TOTP($secret, $code))
		{
			return true;
		}

		// Vérifier encore, mais avec le temps NTP
		// au cas où l'horloge du serveur n'est pas à l'heure
		if (\Garradin\NTP_SERVER
			&& ($time = Security_OTP::getTimeFromNTP(\Garradin\NTP_SERVER))
			&& Security_OTP::TOTP($secret, $code, $time))
		{
			return true;
		}

		return false;
................................................................................
		$password_confirm = trim($password_confirm);

		if (!hash_equals($password, $password_confirm))
		{
			throw new UserException('Le mot de passe et sa vérification ne sont pas identiques.');
		}

		self::checkPasswordValidity($password);




		$password = self::hashPassword($password);

		$message = "Bonjour,\n\nLe mot de passe de votre compte a bien été modifié.\n\n";
		$message.= "Votre adresse email : ".$membre->email."\n";
		$message.= "La demande émanait de l'adresse IP : ".Utils::getIP()."\n\n";
		$message.= "Si vous n'avez pas demandé à changer votre mot de passe, merci de nous le signaler.";

		DB::getInstance()->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$membre->id]);
................................................................................
			{
				throw new \RuntimeException(sprintf('Le champ %s n\'est pas autorisé dans cette méthode.', $key));
			}
		}

		if (isset($data['passe']) && trim($data['passe']) !== '')
		{
			$data['passe'] = trim($data['passe']);

			self::checkPasswordValidity($data['passe']);



			$data['passe'] = self::hashPassword($data['passe']);
		}
		else
		{
			unset($data['passe']);
		}

		if (isset($data['clef_pgp']) && trim($data['clef_pgp']) !== '')

Modified src/www/admin/static/scripts/password.js from [c16716738c] to [ae6eb9a86f].

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
..
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
...
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
	        this.select();
	        checkPasswordStrength();
	        checkPasswordMatch();
		};

		strength_elm = document.createElement('span');
		strength_elm.className = 'password_check';
		
		pw_elm.parentNode.appendChild(strength_elm);

		match_elm = document.createElement('span');
		match_elm.className = 'password_check';
		
		pw2_elm.parentNode.appendChild(match_elm);

		pw_elm.onkeyup = checkPasswordStrength;
		pw_elm.onchange = function () { checkPasswordStrength(); checkPasswordMatch(); };
		pw_elm.onblur = function () { checkPasswordStrength(); checkPasswordMatch(); };
		pw2_elm.onkeypress = checkPasswordMatch;
		pw2_elm.onblur = checkPasswordMatch;
................................................................................
		    	if (v[j].length < 4)
		    		continue;

		    	var r = new RegExp(RegExp.quote(v[j]), 'ig');
		    	score -= pass.match(r) ? pass.match(r).length * 5 : 0;
		    }
	    }
	    
	    // award every unique letter until 5 repetitions
	    var letters = new Object();
	    for (var i=0; i<pass.length; i++) {
	        letters[pass[i]] = (letters[pass[i]] || 0) + 1;
	        score += 5.0 / letters[pass[i]];
	    }

................................................................................
	    {
	    	strength_elm.className = strength_elm.className.split(' ')[0] + ' weak';
	        strength_elm.innerHTML = 'Sécurité : <b>mauvaise</b>';
	    }
	    else
	    {
	    	strength_elm.className = strength_elm.className.split(' ')[0] + ' fail';
	        strength_elm.innerHTML = 'Sécurité : <b>aucune</b>';	    	
	    }

	    return true;
	}

	function checkPasswordMatch()
	{







|




|







 







|







 







|







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
..
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
...
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
	        this.select();
	        checkPasswordStrength();
	        checkPasswordMatch();
		};

		strength_elm = document.createElement('span');
		strength_elm.className = 'password_check';

		pw_elm.parentNode.appendChild(strength_elm);

		match_elm = document.createElement('span');
		match_elm.className = 'password_check';

		pw2_elm.parentNode.appendChild(match_elm);

		pw_elm.onkeyup = checkPasswordStrength;
		pw_elm.onchange = function () { checkPasswordStrength(); checkPasswordMatch(); };
		pw_elm.onblur = function () { checkPasswordStrength(); checkPasswordMatch(); };
		pw2_elm.onkeypress = checkPasswordMatch;
		pw2_elm.onblur = checkPasswordMatch;
................................................................................
		    	if (v[j].length < 4)
		    		continue;

		    	var r = new RegExp(RegExp.quote(v[j]), 'ig');
		    	score -= pass.match(r) ? pass.match(r).length * 5 : 0;
		    }
	    }

	    // award every unique letter until 5 repetitions
	    var letters = new Object();
	    for (var i=0; i<pass.length; i++) {
	        letters[pass[i]] = (letters[pass[i]] || 0) + 1;
	        score += 5.0 / letters[pass[i]];
	    }

................................................................................
	    {
	    	strength_elm.className = strength_elm.className.split(' ')[0] + ' weak';
	        strength_elm.innerHTML = 'Sécurité : <b>mauvaise</b>';
	    }
	    else
	    {
	    	strength_elm.className = strength_elm.className.split(' ')[0] + ' fail';
	        strength_elm.innerHTML = 'Sécurité : <b>aucune</b>';
	    }

	    return true;
	}

	function checkPasswordMatch()
	{

Modified src/www/admin/upgrade.php from [9e3246c1b7] to [1719a77790].

260
261
262
263
264
265
266








267
268
269
270
271
272
273

        $db->exec('INSERT INTO "compta_categories" VALUES(NULL,-1,\'Licences fédérales\',\'Licences payées pour les adhérents (par exemple fédération sportive etc.)\',\'652\');');

        $db->import(ROOT . '/include/data/0.9.1.sql');

        $db->commit();
    }









    Utils::clearCaches();

    $config->setVersion(garradin_version());

    Static_Cache::remove('upgrade');








>
>
>
>
>
>
>
>







260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281

        $db->exec('INSERT INTO "compta_categories" VALUES(NULL,-1,\'Licences fédérales\',\'Licences payées pour les adhérents (par exemple fédération sportive etc.)\',\'652\');');

        $db->import(ROOT . '/include/data/0.9.1.sql');

        $db->commit();
    }

    if (version_compare($v, '0.9.5', '<'))
    {
        $db->begin();
        // Créer les tables manquantes
        $db->import(ROOT . '/include/data/schema.sql');
        $db->commit();
    }

    Utils::clearCaches();

    $config->setVersion(garradin_version());

    Static_Cache::remove('upgrade');