Valider fadeb3ea rédigé par marcimat's avatar marcimat
Parcourir les fichiers

Sécurité des authentifications avec le secret du site (g0uZ) :

- 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.
parent b9326eec
Chargement en cours
Chargement en cours
Chargement en cours
Chargement en cours
+11 −6
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
@@ -15,6 +15,11 @@
 *
 * @package SPIP\Core\Inscription
 **/

use Spip\Chiffrer\Chiffrement;
use Spip\Chiffrer\SpipCles;


if (!defined('_ECRIRE_INC_VERSION')) {
	return;
}
@@ -342,8 +347,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
@@ -361,7 +365,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);

@@ -369,7 +373,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.
@@ -384,7 +388,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;
		}
@@ -411,10 +415,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;
		}
+6 −8
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
@@ -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();
}

/**
+35 −38
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
@@ -20,58 +20,49 @@ 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;
		}
		$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 */
	public static function keygen(): string {
@@ -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;
	}
}
+43 −2
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
@@ -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 (