Overview
Comment: | Sécurité: obligation de confirmer activation OTP, ajout clef PGP pour chiffrement mails sortants, déplacement infos sécurité dans une page à part |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | dev |
Files: | files | file ages | folders |
SHA1: |
71857e568008f5cd6de9ea0130640465 |
User & Date: | bohwaz on 2017-02-17 04:31:38 |
Other Links: | branch diff | manifest | tags |
Context
2017-02-21
| ||
03:55 | Sécurité: vérification de la clé PGP avant d'enregistrer check-in: 36fa993386 user: bohwaz tags: dev | |
2017-02-17
| ||
04:31 | Sécurité: obligation de confirmer activation OTP, ajout clef PGP pour chiffrement mails sortants, déplacement infos sécurité dans une page à part check-in: 71857e5680 user: bohwaz tags: dev | |
02:08 | Ajout signaux pour plugin, fix [743d7e1483fc23c85bd66aa44bd5673479a88913] check-in: a1acf12dcf user: bohwaz tags: dev | |
Changes
Modified src/include/data/0.8.0.sql from [737893df79] to [70e25e73cc].
1 2 | -- Ajouter champ pour OTP ALTER TABLE membres ADD COLUMN secret_otp TEXT NULL; | > > > | 1 2 3 4 5 | -- Ajouter champ pour OTP ALTER TABLE membres ADD COLUMN secret_otp TEXT NULL; -- Ajouter champ clé PGP ALTER TABLE membres ADD COLUMN clef_pgp TEXT NULL; |
Modified src/include/lib/Garradin/Membres.php from [6c7caf4cad] to [4506047015].
︙ | ︙ | |||
93 94 95 96 97 98 99 | if (empty($_SESSION['logged_user'])) { return false; } $membre = $_SESSION['logged_user']; | | < < < < < < | | | | < | | | | | < | < > | | < > | > > > | > | | | < > | 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | if (empty($_SESSION['logged_user'])) { return false; } $membre = $_SESSION['logged_user']; if (!$this->checkOTP($membre['secret_otp'], $code)) { return false; } $_SESSION['otp_required'] = false; return true; } public function getNewOTPSecret() { $out = []; $out['secret'] = \KD2\Security_OTP::getRandomSecret(); $out['secret_display'] = implode(' ', str_split($out['secret'], 4)); $out['url'] = \KD2\Security_OTP::getOTPAuthURL(Config::getInstance()->get('nom_asso'), $out['secret']); $qrcode = new \KD2\QRCode($out['url']); $out['qrcode'] = 'data:image/svg+xml;base64,' . base64_encode($qrcode->toSVG()); return $out; } public function checkOTP($secret, $code) { if (!\KD2\Security_OTP::TOTP($secret, $code)) { // Vérifier encore, mais avec le temps NTP // au cas où l'horloge du serveur n'est pas à l'heure $time = \KD2\Security_OTP::getTimeFromNTP('fr.pool.ntp.org'); if (!\KD2\Security_OTP::TOTP($secret, $code, $time)) { return false; } } return true; } public function recoverPasswordCheck($id) { $db = DB::getInstance(); |
︙ | ︙ | |||
568 569 570 571 572 573 574 575 576 577 578 579 580 581 | if (empty($data)) { return true; } return $db->simpleUpdate('membres', $data, 'id = '.(int)$id); } public function get($id) { $db = DB::getInstance(); $config = Config::getInstance(); return $db->simpleQuerySingle('SELECT *, | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 | if (empty($data)) { return true; } return $db->simpleUpdate('membres', $data, 'id = '.(int)$id); } public function checkPassword($password) { $user = $this->getLoggedUser(); if (!$user) { return false; } return $this->_checkPassword($password, $user['passe']); } public function editSecurity(Array $data = []) { $user = $this->getLoggedUser(); if (!$user) { throw new \LogicException('Utilisateur non connecté.'); } $allowed_fields = ['passe', 'clef_pgp', 'secret_otp']; foreach ($data as $key=>$value) { if (!in_array($key, $allowed_fields)) { 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']) < 5) { throw new UserException('Le mot de passe doit faire au moins 5 caractères.'); } $data['passe'] = $this->_hashPassword($data['passe']); } else { unset($data['passe']); } if (isset($data['clef_pgp'])) { $data['clef_pgp'] = trim($data['clef_pgp']); } $db->simpleUpdate('membres', $data, 'id = '.(int)$user['id']); $this->updateSessionData(); return true; } public function get($id) { $db = DB::getInstance(); $config = Config::getInstance(); return $db->simpleQuerySingle('SELECT *, |
︙ | ︙ |
Modified src/include/lib/Garradin/Membres/Champs.php from [d222f084dc] to [2915d1081b].
︙ | ︙ | |||
386 387 388 389 390 391 392 | // Champs à créer $create = [ 'id INTEGER PRIMARY KEY, -- Numéro attribué automatiquement', 'id_categorie INTEGER NOT NULL, -- Numéro de catégorie', 'date_connexion TEXT NULL, -- Date de dernière connexion', 'date_inscription TEXT NOT NULL DEFAULT CURRENT_DATE, -- Date d\'inscription', | | > > | 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 | // Champs à créer $create = [ 'id INTEGER PRIMARY KEY, -- Numéro attribué automatiquement', 'id_categorie INTEGER NOT NULL, -- Numéro de catégorie', 'date_connexion TEXT NULL, -- Date de dernière connexion', 'date_inscription TEXT NOT NULL DEFAULT CURRENT_DATE, -- Date d\'inscription', 'secret_otp TEXT NULL, -- Code secret pour TOTP', 'clef_pgp TEXT NULL, -- Clé publique PGP' ]; $create_keys = [ 'FOREIGN KEY (id_categorie) REFERENCES membres_categories (id)' ]; // Champs à recopier $copy = [ 'id', 'id_categorie', 'date_connexion', 'date_inscription', 'secret_otp', 'clef_pgp', ]; $anciens_champs = $config->get('champs_membres'); $anciens_champs = is_null($anciens_champs) ? $this->champs : $anciens_champs->getAll(); foreach ($this->champs as $key=>$cfg) { |
︙ | ︙ |
Modified src/templates/admin/mes_infos.tpl from [105fa33fc8] to [701b28771a].
1 2 3 4 5 6 | {include file="admin/_head.tpl" title="Mes informations personnelles" current="mes_infos" js=1} {if $error} <p class="error"> {$error} </p> | < < < | | | < < | < < < > | < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 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 | {include file="admin/_head.tpl" title="Mes informations personnelles" current="mes_infos" js=1} {if $error} <p class="error"> {$error} </p> {/if} <ul class="actions"> <li class="current"><a href="{$admin_url}mes_infos.php">Mes informations personnelles</a></li> <li><a href="{$admin_url}mes_infos_securite.php">Mot de passe et options de sécurité</a></li> </ul> <form method="post" action="{$self_url}"> <fieldset> <legend>Informations personnelles</legend> <dl> {foreach from=$champs item="champ" key="nom"} {if empty($champ.private) && $nom != 'passe'} {html_champ_membre config=$champ name=$nom data=$membre user_mode=true} {/if} {/foreach} </dl> </fieldset> <fieldset> <legend>Changer mon mot de passe</legend> {if $user.droits.membres < Garradin\Membres::DROIT_ADMIN && (!empty($champs.passe.private) || empty($champs.passe.editable))} <p class="help">Vous devez contacter un administrateur pour changer votre mot de passe.</p> {else} <p><a href="{$admin_url}mes_infos_securite.php">Modifier mon mot de passe ou autres informations de sécurité.</a></p> {/if} </fieldset> <p class="submit"> {csrf_field key="edit_me"} <input type="submit" name="save" value="Enregistrer →" /> </p> </form> {include file="admin/_foot.tpl"} |
Added src/templates/admin/mes_infos_securite.tpl version [b578d4e8a9].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | {include file="admin/_head.tpl" title="Mes informations de connexion et sécurité" current="mes_infos" js=1} <ul class="actions"> <li><a href="{$admin_url}mes_infos.php">Mes informations personnelles</a></li> <li class="current"><a href="{$admin_url}mes_infos_securite.php">Mot de passe et options de sécurité</a></li> </ul> {if $confirm} <form method="post" action="{$self_url}"> {if $error} <p class="error"> {$error} </p> {/if} {if !empty($otp)} <p class="alert"> Confirmez l'activation de l'authentification à double facteur TOTP en l'utilisant une première fois. </p> <fieldset> <legend>Confirmer l'activation de l'authentification à double facteur (2FA)</legend> <img class="qrcode" src="{$otp.qrcode}" alt="" /> <dl> <dt>Ma clé secrète est :</dt> <dd><code>{$otp.secret_display}</code></dd> <dd class="help">Recopiez la clé secrète ou scannez le QR code pour configurer votre application TOTP (par exemple <a href="https://freeotp.github.io/">FreeOTP</a>), puis utilisez celle-ci pour générer un code d'accès et confirmer l'activation.</dd> <dt><label for="f_code">Code TOTP</label></dt> <dd class="help">Entrez ici le code donné par l'application d'authentification double facteur.</dd> <dd><input type="text" name="code" id="f_code" value="{form_field name=code}" /></dd> </dl> </fieldset> {/if} <fieldset> <legend>Confirmer les changements</legend> <dl> <dt><label for="f_passe_confirm">Mot de passe actuel</label></dt> <dd class="help">Entrez votre mot de passe actuel pour confirmer les changements demandés.</dd> <dd><input type="password" name="passe_confirm" /></dd> </dl> </fieldset> <p class="submit"> {csrf_field key="edit_me_security"} <input type="hidden" name="passe" value="{form_field name="passe"}" /> <input type="hidden" name="repasse" value="{form_field name="repasse"}" /> <input type="hidden" name="clef_pgp" value="{form_field name="clef_pgp"}" /> <input type="hidden" name="otp_secret" value="{$otp.secret}" /> <input type="hidden" name="otp" value="generate" /> <input type="submit" name="confirm" value="Confirmer →" /> </p> </form> {else} {if $error} <p class="error"> {$error} </p> {/if} <form method="post" action="{$self_url}"> <fieldset> <legend>Changer mon mot de passe</legend> {if $user.droits.membres < Garradin\Membres::DROIT_ADMIN && (!empty($champs.passe.private) || empty($champs.passe.editable))} <p class="help">Vous devez contacter un administrateur pour changer votre mot de passe.</p> {else} <dl> <dd>Vous avez déjà un mot de passe, ne remplissez les champs suivants que si vous souhaitez en changer.</dd> <dt><label for="f_passe">Nouveau mot de passe</label></dt> <dd class="help"> Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres. </dd> <dd class="help"> Pas d'idée ? Voici une suggestion choisie au hasard : <input type="text" readonly="readonly" title="Cliquer pour utiliser cette suggestion comme mot de passe" id="password_suggest" value="{$passphrase}" /> </dd> <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern=".{ldelim}5,{rdelim}" /></dd> <dt><label for="f_repasse">Encore le mot de passe</label> (vérification)</dt> <dd><input type="password" name="repasse" id="f_repasse" value="{form_field name=repasse}" pattern=".{ldelim}5,{rdelim}" /></dd> </dl> {/if} </fieldset> <fieldset> <legend>Authentification à double facteur (2FA)</legend> <p class="help">Pour renforcer la sécurité de votre connexion en cas de vol de votre mot de passe, vous pouvez activer l'authentification à double facteur. Cela nécessite d'installer une application comme <a href="https://freeotp.github.io/">FreeOTP</a> sur votre téléphone.</p> <dl> <dt>Authentification à double facteur (TOTP)</dt> {if $membre.secret_otp} <dd><label><input type="radio" name="otp" value="" checked="checked" /> <strong>Activée</strong></label></dd> <dd><label><input type="radio" name="otp" value="generate" /> Régénérer une nouvelle clé secrète</label></dd> <dd><label><input type="radio" name="otp" value="disable" /> Désactiver l'authentification à double facteur</label></dd> {else} <dd><em>Désactivée</em></dd> <dd><label><input type="checkbox" name="otp" value="generate" /> Activer</label></dd> {/if} </dl> </fieldset> {if $pgp_disponible} <fieldset> <legend>Protéger mes mails personnels par chiffrement PGP/GnuPG</legend> <dl> <dt><label for="f_clef_pgp">Ma clé publique PGP</label></dt> <dd class="help">En inscrivant ici votre clé publique, tous les emails personnels (non collectifs) qui vous sont envoyés seront chiffrés (cryptés) avec cette clé : messages envoyés par les membres, rappels de cotisation, procédure de récupération de mot de passe, etc.</dd> <dd><textarea name="clef_pgp" id="f_clef_pgp" cols="90" rows="5">{form_field name="clef_pgp" data=$user}</textarea></dd> {if $clef_pgp_fingerprint}<dd class="help">L'empreinte de la clé est : {$clef_pgp_fingerprint}</dd>{/if} </dl> <p class="alert"> Attention : en inscrivant ici votre clé PGP, les emails de récupération de mot de passe perdu vous seront envoyés chiffrés et ne pourront être lus sans utiliser le mot de passe protégeant votre clé privée correspondante. </p> </fieldset> {/if} <p class="submit"> {csrf_field key="edit_me_security"} <input type="submit" name="save" value="Enregistrer →" /> </p> </form> <script type="text/javascript"> {literal} g.script('scripts/password.js').onload = function () { initPasswordField('password_suggest', 'f_passe', 'f_repasse'); }; {/literal} </script> {/if} {include file="admin/_foot.tpl"} |
Modified src/www/admin/mes_infos.php from [15a9109208] to [16b21a0d0d].
︙ | ︙ | |||
14 15 16 17 18 19 20 | if (!empty($_POST['save'])) { if (!Utils::CSRF_check('edit_me')) { $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; } | < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < | 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 50 51 52 | if (!empty($_POST['save'])) { if (!Utils::CSRF_check('edit_me')) { $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; } else { try { $data = []; foreach ($config->get('champs_membres')->getAll() as $key=>$c) { if (!empty($c['editable'])) { $data[$key] = Utils::post($key); } } $membres->edit($membre['id'], $data, false); $membres->updateSessionData(); Utils::redirect('/admin/'); } catch (UserException $e) { $error = $e->getMessage(); } } } $tpl->assign('error', $error); $tpl->assign('champs', $config->get('champs_membres')->getAll()); $tpl->assign('membre', $membre); $tpl->display('admin/mes_infos.tpl'); |
Added src/www/admin/mes_infos_securite.php version [89a6bd6a26].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | <?php namespace Garradin; require_once __DIR__ . '/_inc.php'; $membre = $membres->getLoggedUser(); if (!$membre) { throw new UserException("Ce membre n'existe pas."); } $error = false; $confirm = false; if (!empty($_POST['confirm'])) { if (!Utils::CSRF_check('edit_me_security')) { $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; } elseif (trim(Utils::post('passe_confirm')) === '') { $error = 'Merci de bien vouloir renseigner votre mot de passe courant pour confirmer les changements.'; } elseif ($membres->checkPassword(Utils::post('passe_confirm'))) { $error = 'Le mot de passe fourni ne correspond pas au mot de passe actuel. Merci de bien vouloir renseigner votre mot de passe courant pour confirmer les changements.'; } elseif (Utils::post('passe') != Utils::post('repasse')) { $error = 'La vérification ne correspond pas au mot de passe.'; } elseif (Utils::post('otp_secret') && !$membres->checkOTP(Utils::post('otp_secret'), Utils::post('code'))) { $error = 'Le code TOTP entré n\'est pas valide.'; } else { try { $data = [ 'clef_pgp' => Utils::post('clef_pgp'), ]; if (Utils::post('passe') && !empty($config->get('champs_membres')->get('passe')['editable'])) { $data['passe'] = Utils::post('passe'); } if (Utils::post('otp_secret')) { $data['secret_otp'] = Utils::post('otp_secret'); } elseif (Utils::post('otp') == 'disable') { $data['secret_otp'] = null; } $membres->editSecurity($data); Utils::redirect('/admin/'); } catch (UserException $e) { $error = $e->getMessage(); } } $confirm = true; } elseif (Utils::post('save')) { if (!Utils::CSRF_check('edit_me_security')) { $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; } elseif (Utils::post('passe') != Utils::post('repasse')) { $error = 'La vérification ne correspond pas au mot de passe.'; } else { $confirm = true; } } $tpl->assign('error', $error); $tpl->assign('confirm', $confirm); if (Utils::post('otp') == 'generate') { $otp = $membres->getNewOTPSecret(); $tpl->assign('otp', $otp); } $tpl->assign('pgp_disponible', \KD2\Security::canUseEncryption()); $tpl->assign('clef_pgp_fingerprint', !empty($membre['clef_pgp']) ? \KD2\Security::getEncryptionKeyFingerprint($membre['clef_pgp']) : null); $tpl->assign('passphrase', Utils::suggestPassword()); $tpl->assign('champs', $config->get('champs_membres')->getAll()); $tpl->assign('membre', $membre); $tpl->display('admin/mes_infos_securite.tpl'); |