From 20908f788843b160948ba4b6420cbdad5aca025b Mon Sep 17 00:00:00 2001 From: Matthieu Marcillaud <marcimat@rezo.net> Date: Fri, 25 Feb 2022 18:11:10 +0100 Subject: [PATCH] =?UTF-8?q?S=C3=A9curit=C3=A9=20des=20authentifications=20?= =?UTF-8?q?avec=20le=20secret=20du=20site=20(g0uZ)=20:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - la clé secret_du_site est partagée entre le disque et la base de données. Il faut les 2 pour le calculer (la clé secret_du_site, et la meta secret_du_site). - Si on demande à chiffrer avec autre chose qu’une clé de longueur adaptée (ie: générée par Chiffrement::keygen()), tel qu’un mot de passe, alors on passe dans sodium_crypto_pwhash(), qui est la fonction adaptée à ce cas, mais le coût est très élevé dans ce cas (temps / mémoire, même au minimum possible) - On s’arrange donc pour que le secret_du_site obtenu soit effectivement de la taille adaptée à la clé de chiffrement. --- ecrire/action/inscrire_auteur.php | 17 ++++--- ecrire/inc/securiser_action.php | 14 +++--- ecrire/src/Chiffrer/Chiffrement.php | 73 ++++++++++++++--------------- ecrire/src/Chiffrer/SpipCles.php | 45 +++++++++++++++++- 4 files changed, 95 insertions(+), 54 deletions(-) diff --git a/ecrire/action/inscrire_auteur.php b/ecrire/action/inscrire_auteur.php index 3bda68d705..59ab85d3f4 100644 --- a/ecrire/action/inscrire_auteur.php +++ b/ecrire/action/inscrire_auteur.php @@ -15,6 +15,11 @@ * * @package SPIP\Core\Inscription **/ + +use Spip\Chiffrer\Chiffrement; +use Spip\Chiffrer\SpipCles; + + if (!defined('_ECRIRE_INC_VERSION')) { return; } @@ -340,8 +345,7 @@ function confirmer_statut_inscription($auteur) { /** - * Attribuer un jeton temporaire pour un auteur - * en assurant l'unicite du jeton. + * Attribuer un jeton temporaire pour un auteur en assurant l'unicite du jeton. * * Chaque appel crée un nouveau jeton pour l’auteur * et invalide donc le précédent @@ -359,7 +363,7 @@ function auteur_attribuer_jeton($id_auteur): string { // tous les jetons connus pour vérifier le jeton d’un auteur. $public = substr(creer_uniqid(), 0, 7) . '.'; $jeton = $public . creer_uniqid(); - $jeton_chiffre_prefixe = $public . \Spip\Chiffrer\Chiffrement::chiffrer($jeton); + $jeton_chiffre_prefixe = $public . Chiffrement::chiffrer($jeton, SpipCles::secret_du_site()); sql_updateq('spip_auteurs', ['cookie_oubli' => $jeton_chiffre_prefixe], 'id_auteur=' . intval($id_auteur)); } while (sql_countsel('spip_auteurs', 'cookie_oubli=' . sql_quote($jeton_chiffre_prefixe, '', 'string')) > 1); @@ -367,7 +371,7 @@ function auteur_attribuer_jeton($id_auteur): string { } /** - * Lire un jeton temporaire d’un auteur + * Lire un jeton temporaire d’un auteur (peut le créer au besoin) * * Cette fonction peut être pratique si plusieurs notifications proches * dans la durée sont envoyées au même auteur. @@ -382,7 +386,7 @@ function auteur_lire_jeton(int $id_auteur, bool $autoInit = false): ?string { if ($jeton_chiffre_prefixe) { include_spip('inc/chiffrer'); $jeton_chiffre = substr($jeton_chiffre_prefixe, 8); - $jeton = \Spip\Chiffrer\Chiffrement::dechiffrer($jeton_chiffre); + $jeton = Chiffrement::dechiffrer($jeton_chiffre, SpipCles::secret_du_site()); if ($jeton) { return $jeton; } @@ -409,10 +413,11 @@ function auteur_verifier_jeton($jeton) { include_spip('inc/chiffrer'); $public = substr($jeton, 0, 8); + // Les auteurs qui ont un jetons ressemblant $auteurs = sql_allfetsel('*', 'spip_auteurs', 'cookie_oubli LIKE ' . sql_quote($public . '%')); foreach ($auteurs as $auteur) { $jeton_chiffre = substr($auteur['cookie_oubli'], 8); - $_jeton = \Spip\Chiffrer\Chiffrement::dechiffrer($jeton_chiffre); + $_jeton = Chiffrement::dechiffrer($jeton_chiffre, SpipCles::secret_du_site()); if ($_jeton and hash_equals($jeton, $_jeton)) { return $auteur; } diff --git a/ecrire/inc/securiser_action.php b/ecrire/inc/securiser_action.php index 31fbe57ee7..970c5b1f0b 100644 --- a/ecrire/inc/securiser_action.php +++ b/ecrire/inc/securiser_action.php @@ -16,6 +16,8 @@ * @package SPIP\Core\Actions **/ + use \Spip\Chiffrer\SpipCles; + if (!defined('_ECRIRE_INC_VERSION')) { return; } @@ -293,18 +295,14 @@ function verifier_action_auteur($action, $hash) { // /** - * Renvoyer le secret du site, et le generer si il n'existe pas encore - * Le secret du site doit rester aussi secret que possible, et est eternel - * On ne doit pas l'exporter - * + * Renvoyer le secret du site (le generer si il n'existe pas encore) + * + * @uses SpipCles::secret_du_site() * @return string */ function secret_du_site() { include_spip('inc/chiffrer'); - $cles = \Spip\Chiffrer\SpipCles::instance(); - $secret = $cles->getSecretSite(); - - return $secret; + return SpipCles::secret_du_site(); } /** diff --git a/ecrire/src/Chiffrer/Chiffrement.php b/ecrire/src/Chiffrer/Chiffrement.php index c9be4fab60..3f2e329f3a 100644 --- a/ecrire/src/Chiffrer/Chiffrement.php +++ b/ecrire/src/Chiffrer/Chiffrement.php @@ -20,57 +20,48 @@ namespace Spip\Chiffrer; */ class Chiffrement { - /** Chiffre un message en utilisant une clé (le secret_du_site par défaut) ou un mot de passe */ + /** Chiffre un message en utilisant une clé ou un mot de passe */ public static function chiffrer( string $message, #[\SensitiveParameter] - ?string $key = null + string $key ): ?string { - $key ??= self::getDefaultKey(); - if (!$key) { - spip_log("chiffrer() sans clé `$message`", 'chiffrer' . _LOG_INFO_IMPORTANTE); - return null; - } - if (strlen($key) !== \SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { - $key = self::generateKeyFromPassword($key); - } + // create a random salt for key derivation + $salt = random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES); + $key = self::deriveKeyFromPassword($key, $salt); $nonce = random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $padded_message = sodium_pad($message, 16); $encrypted = sodium_crypto_secretbox($padded_message, $nonce, $key); - $encoded = base64_encode($nonce . $encrypted); + $encoded = base64_encode($salt . $nonce . $encrypted); sodium_memzero($key); sodium_memzero($nonce); + sodium_memzero($salt); spip_log("chiffrer($message)=$encoded", 'chiffrer' . _LOG_DEBUG); return $encoded; } - /** Déchiffre un message en utilisant une clé (le secret_du_site par défaut) ou un mot de passe */ + /** Déchiffre un message en utilisant une clé ou un mot de passe */ public static function dechiffrer( string $encoded, #[\SensitiveParameter] - ?string $key = null + string $key ): ?string { - $key ??= self::getDefaultKey(); - if (!$key) { - spip_log("dechiffrer() sans clé `$encoded`", 'chiffrer' . _LOG_INFO_IMPORTANTE); - return null; - } - if (strlen($key) !== \SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { - $key = self::generateKeyFromPassword($key); - } $decoded = base64_decode($encoded); - $nonce = mb_substr($decoded, 0, \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); - $encrypted_result = mb_substr($decoded, \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); - $padded_message = sodium_crypto_secretbox_open($encrypted_result, $nonce, $key); - $message = sodium_unpad($padded_message, 16); + $salt = substr($decoded, 0, \SODIUM_CRYPTO_PWHASH_SALTBYTES); + $nonce = substr($decoded, \SODIUM_CRYPTO_PWHASH_SALTBYTES, \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $encrypted = substr($decoded, \SODIUM_CRYPTO_PWHASH_SALTBYTES + \SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $key = self::deriveKeyFromPassword($key, $salt); + $padded_message = sodium_crypto_secretbox_open($encrypted, $nonce, $key); sodium_memzero($key); sodium_memzero($nonce); - if ($message !== false) { - spip_log("dechiffrer($encoded)=$message", 'chiffrer' . _LOG_DEBUG); - return $message; + sodium_memzero($salt); + if ($padded_message === false) { + spip_log("dechiffrer() chiffre corrompu `$encoded`", 'chiffrer' . _LOG_DEBUG); + return null; } - spip_log("dechiffrer() chiffre corrompu `$encoded`", 'chiffrer' . _LOG_DEBUG); - return null; + $message = sodium_unpad($padded_message, 16); + spip_log("dechiffrer($encoded)=$message", 'chiffrer' . _LOG_DEBUG); + return $message; } /** Génère une clé de la taille attendue pour le chiffrement */ @@ -84,17 +75,23 @@ class Chiffrement { * Notamment si on utilise un mot de passe comme clé, il faut le hacher * pour servir de clé à la taille correspondante. */ - private static function generateKeyFromPassword(string $password): string { + private static function deriveKeyFromPassword( + #[\SensitiveParameter] + string $password, + string $salt + ): string { if (strlen($password) === \SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { return $password; } - $hashed = hash('sha256', $password); - return substr($hashed, 0, \SODIUM_CRYPTO_SECRETBOX_KEYBYTES); - } + $key = sodium_crypto_pwhash( + \SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + $password, + $salt, + \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE + ); + sodium_memzero($password); - /** Retourne la clé de chiffrement par défaut (le secret_du_site) */ - private static function getDefaultKey(): ?string { - $keys = SpipCles::instance(); - return $keys->getSecretSite(); + return $key; } } diff --git a/ecrire/src/Chiffrer/SpipCles.php b/ecrire/src/Chiffrer/SpipCles.php index 10ae2a8cbc..1071e13287 100644 --- a/ecrire/src/Chiffrer/SpipCles.php +++ b/ecrire/src/Chiffrer/SpipCles.php @@ -26,6 +26,14 @@ final class SpipCles { return self::$instances[$file]; } + /** + * Retourne le secret du site (shorthand) + * @uses self::getSecretSite() + */ + public static function secret_du_site(): ?string { + return (self::instance())->getSecretSite(); + } + private function __construct(string $file = '') { if ($file) { $this->file = $file; @@ -33,13 +41,27 @@ final class SpipCles { $this->cles = new Cles($this->read()); } + /** + * Renvoyer le secret du site + * + * Le secret du site doit rester aussi secret que possible, et est eternel + * On ne doit pas l'exporter + * + * Le secret est partagé entre une clé disque et une clé bdd + * + * @return string + */ public function getSecretSite(bool $autoInit = true): ?string { - return $this->getKey('secret_du_site', $autoInit); + $key = $this->getKey('secret_du_site', $autoInit); + $meta = $this->getMetaKey('secret_du_site', $autoInit); + // conserve la même longeur. + return $key ^ $meta; } + + /** Renvoyer le secret des authentifications */ public function getSecretAuth(bool $autoInit = false): ?string { return $this->getKey('secret_des_auth', $autoInit); } - public function save(): bool { return ecrire_fichier_securise($this->file, $this->cles->toJson()); } @@ -124,6 +146,25 @@ final class SpipCles { return null; } + private function getMetaKey(string $name, bool $autoInit = true): ?string { + if (!isset($GLOBALS['meta'][$name])) { + include_spip('base/abstract_sql'); + $GLOBALS['meta'][$name] = sql_getfetsel('valeur', 'spip_meta', 'nom = ' . sql_quote($name, '', 'string')); + } + $key = base64_decode($GLOBALS['meta'][$name] ?? ''); + if (strlen($key) === \SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { + return $key; + } + if (!$autoInit) { + return null; + } + $key = Chiffrement::keygen(); + ecrire_meta($name, base64_encode($key), 'non'); + lire_metas(); // au cas ou ecrire_meta() ne fonctionne pas + + return $key; + } + private function read(): array { lire_fichier_securise($this->file, $json); if ( -- GitLab