Index: src/include/data/0.8.0.sql ================================================================== --- src/include/data/0.8.0.sql +++ src/include/data/0.8.0.sql @@ -1,2 +1,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; Index: src/include/lib/Garradin/Membres.php ================================================================== --- src/include/lib/Garradin/Membres.php +++ src/include/lib/Garradin/Membres.php @@ -95,51 +95,47 @@ return false; } $membre = $_SESSION['logged_user']; - if (!\KD2\Security_OTP::TOTP($membre['secret_otp'], $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($membre['secret_otp'], $code, $time)) - { - return false; - } - } + if (!$this->checkOTP($membre['secret_otp'], $code)) + { + return false; + } + $_SESSION['otp_required'] = false; return true; } - public function setOTP() - { - $membre = $this->getLoggedUser(); - - $secret = \KD2\Security_OTP::getRandomSecret(); - - DB::getInstance()->simpleExec('UPDATE membres SET secret_otp = ? WHERE id = ?;', $secret, $membre['id']); - - $membre['secret_otp'] = $secret; - - $this->updateSessionData($membre); - - return $secret; - } - - public function disableOTP() - { - $membre = $this->getLoggedUser(); - - DB::getInstance()->simpleExec('UPDATE membres SET secret_otp = NULL WHERE id = ?;', $membre['id']); - - $membre['secret_otp'] = null; - - $this->updateSessionData($membre); + 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) @@ -570,10 +566,66 @@ 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(); Index: src/include/lib/Garradin/Membres/Champs.php ================================================================== --- src/include/lib/Garradin/Membres/Champs.php +++ src/include/lib/Garradin/Membres/Champs.php @@ -388,11 +388,12 @@ $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' + '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)' ]; @@ -402,10 +403,11 @@ '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(); Index: src/templates/admin/mes_infos.tpl ================================================================== --- src/templates/admin/mes_infos.tpl +++ src/templates/admin/mes_infos.tpl @@ -2,25 +2,16 @@ {if $error}
{$error}
-{elseif $otp_status == 'off'} -- L'authentification à double facteur a été désactivée. -
-{elseif $otp_status} -L'authentification à double facteur a été activée.
-
- Votre clé secrète est :
- {$otp_status}
- Recopiez-la ou scannez le QR code pour configurer votre application.
-
+ {$error} +
+ {/if} + + + + +{/if} + +{include file="admin/_foot.tpl"} Index: src/www/admin/mes_infos.php ================================================================== --- src/www/admin/mes_infos.php +++ src/www/admin/mes_infos.php @@ -16,14 +16,10 @@ { if (!Utils::CSRF_check('edit_me')) { $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 { try { $data = []; @@ -36,46 +32,21 @@ } $membres->edit($membre['id'], $data, false); $membres->updateSessionData(); - if (Utils::post('otp') == 'generate') - { - $secret = $membres->setOTP(); - Utils::redirect('/admin/mes_infos.php?otp=' . rawurlencode($secret)); - } - elseif (Utils::post('otp') == 'disable') - { - $secret = $membres->disableOTP(); - Utils::redirect('/admin/mes_infos.php?otp=off'); - } - else - { - Utils::redirect('/admin/'); - } + Utils::redirect('/admin/'); } catch (UserException $e) { $error = $e->getMessage(); } } } $tpl->assign('error', $error); -$tpl->assign('otp_status', Utils::get('otp')); - -if (Utils::get('otp') && Utils::get('otp') != 'off') -{ - $url = \KD2\Security_OTP::getOTPAuthURL($config->get('nom_asso'), Utils::get('otp')); - $qrcode = new \KD2\QRCode($url); - $qrcode = 'data:image/svg+xml;base64,' . base64_encode($qrcode->toSVG()); - $tpl->assign('otp_qrcode', $qrcode); - - $tpl->assign('otp_status', implode(' ', str_split(Utils::get('otp'), 4))); -} - -$tpl->assign('passphrase', Utils::suggestPassword()); + $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 Index: src/www/admin/mes_infos_securite.php ================================================================== --- src/www/admin/mes_infos_securite.php +++ src/www/admin/mes_infos_securite.php @@ -0,0 +1,103 @@ +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');