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

Refactoring de Chiffrer en découppant en plus d’éléments

les différentes fonctionnalités. C’est mieux structuré.

- Chiffrement gère chiffrer() / dechiffrer() / keygen() (génération d’une nouvelle clé)
en s’appuyant sur Sodium, toujours présent dans PHP depuis PHP 7.2.
On utilise une chifferement symétrique (comme avec openssl précédement),
mais le code est simplifié car libsodium gère l’authentification du message et son salage.

- Password gère verifier() et hacher() en utilisant password_hash donc,
mais en retirant l’option 'salt' qui n’est plus utilisé par PHP > 8,
et effectivement il vaut mieux ne pas le renseigner (PHP gère un salt tournant
tout seul comme un grand). On s’en sert pour régénerer notre password haché en bdd.

- Cles est un conteneur de clés (tableau nom => clé)

- SpipClés gère les clés SPIP et utilisant Cles. Des fonctions backup() et restore() permettent de lire un backup chiffré

J’ai mis l’attribut `#[\SensitiveParameter]` sur différents paramètres en passant,
il devrait être actif à partir de PHP 8.2 pour dire de ne pas afficher la valeur
dans les affichages de backtrace() par exemple, afin de ne pas divulguer
malencontreusement certaines infos en debug.

Également (Cerdic)

- Sodium exige une cle de chiffrement exactement de la bonne longueur : adapter la cle fournir si besoin (cas du chiffrement du backup des cles avec le pass du user)
- La regeneration des cles ne peut se faire que cle par cle : si on a perdu le fichier des cles, c'est OK de regenerer un secret_du_site a la volee pour pouvoir afficher un formulaire de login par exemple, mais on ne doit surtout pas regenerer un secret_des_auth qui invaliderait tous les mots de passe
- Permettre d'avoir plusieurs instances de SpipCles avec des fichiers de cle differents. Peut etre utile pout les tests unitaires, ou pour faire de l'auth multi-sites (on cherche l'auteur dans plusieurs bases SPIP, chacune associee a un fichier de cle different)
parent 6595e493
Chargement en cours
Chargement en cours
Chargement en cours
Chargement en cours
+27 −14
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
@@ -15,7 +15,8 @@
 *
 * @package SPIP\Core\Authentification\SPIP
 **/
use Spip\Core\Chiffrer;
use Spip\Core\Chiffrer\Password;
use Spip\Core\Chiffrer\SpipCles;

if (!defined('_ECRIRE_INC_VERSION')) {
	return;
@@ -62,6 +63,16 @@ function auth_spip_dist($login, $pass, $serveur = '', $phpauth = false) {
	}

	include_spip('inc/chiffrer');
	$cles = SpipCles::instance();
	$secret = $cles->getSecretAuth();
	// Créer les clés si besoin
	if (
		!$secret
		and $row['webmestre'] === 'oui'
		and empty($row['backup_cles'])
	) {
		$cles->generer();
	}

	switch ( strlen($row["pass"]) ) {
		case 32:
@@ -83,15 +94,16 @@ function auth_spip_dist($login, $pass, $serveur = '', $phpauth = false) {
		case 60:
		case 98:
		default:
			if (!Chiffrer::verifier_mot_de_passe($pass, $row["pass"])) {
			// doit-on restaurer un backup des cles ?
				if ($row['webmestre'] === 'oui'
					and !empty($row['backup_cles'])) {
					if (Chiffrer::restaurer_cles_depuis_sauvegarde_chiffree($row['backup_cles'], $row['id_auteur'], $pass, $row['pass'])
					and Chiffrer::verifier_mot_de_passe($pass, $row["pass"])) {
						break;
					}
			if (
				!$secret
				and $row['webmestre'] === 'oui'
				and !empty($row['backup_cles'])
				and $cles->restore($row['backup_cles'], $pass, $row['pass'], $row['id_auteur'])
			) {
				$cles->save();
			}
			if (!Password::verifier($pass, $row["pass"])) {
				unset($row);
			}
			break;
@@ -106,7 +118,7 @@ function auth_spip_dist($login, $pass, $serveur = '', $phpauth = false) {
	// sauf si phpauth : cela reviendrait a changer l'alea a chaque hit, et aucune action verifiable par securiser_action()
	if (!$phpauth) {
		include_spip('inc/acces'); // pour creer_uniqid et verifier_htaccess
		$pass_hash_next = Chiffrer::calculer_hash_sale_mot_de_passe($pass, $row['alea_futur']);
		$pass_hash_next = Password::hacher($pass);
		if ($pass_hash_next) {

			$set = [
@@ -117,7 +129,7 @@ function auth_spip_dist($login, $pass, $serveur = '', $phpauth = false) {
			// a chaque login de webmestre : sauvegarde chiffree des clé du site (avec les pass du webmestre)
			if ($row['statut'] === '0minirezo' and $row['webmestre'] === 'oui') {
				// TODO : ajouter le champ en base
				//$set['backup_cles'] = Chiffrer::sauvegarde_chiffree_cles($row['id_auteur'], $pass);
				//$set['backup_cles'] = $cles:->backup($pass);
			}

			@sql_update(
@@ -378,7 +390,8 @@ function auth_spip_modifier_pass($login, $new_pass, $id_auteur, $serveur = '') {
	$htpass = generer_htpass($new_pass);
	$alea_actuel = creer_uniqid();
	$alea_futur = creer_uniqid();
	$pass = Chiffrer::calculer_hash_sale_mot_de_passe($new_pass, $alea_actuel);
	$pass = Password::hacher($new_pass);
	// TODO: si webmestre, reenregistrer le backup de clé $cles->backup($new_pass);
	$c['pass'] = $pass;
	$c['htpass'] = $htpass;
	$c['alea_actuel'] = $alea_actuel;
+81 −0
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
<?php

/***************************************************************************\
 *  SPIP, Système de publication pour l'internet                           *
 *                                                                         *
 *  Copyright © avec tendresse depuis 2001                                 *
 *  Arnaud Martin, Antoine Pitrou, Philippe Rivière, Emmanuel Saint-James  *
 *                                                                         *
 *  Ce programme est un logiciel libre distribué sous licence GNU/GPL.     *
 *  Pour plus de détails voir le fichier COPYING.txt ou l'aide en ligne.   *
 * \***************************************************************************/

namespace Spip\Core\Chiffrer;

class Chiffrement {

    public static function keygen(): string {
        return sodium_crypto_secretbox_keygen();
    }

	public static function chiffrer(
        string $message,
        #[\SensitiveParameter]
		?string $key = null
    ): ?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) {
            while (strlen($key) < SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
                $key .= $key;
            }
            $key = substr($key, 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
        }
        $nonce = random_bytes(\SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
        $encrypted = sodium_crypto_secretbox($message, $nonce, $key);
        $encoded = base64_encode($nonce . $encrypted);
        sodium_memzero($key);
        sodium_memzero($nonce);
		spip_log("chiffrer($message)=$encoded", 'chiffrer' . _LOG_DEBUG);
		return $encoded;
	}

	public static function dechiffrer(
        string $encoded,
        #[\SensitiveParameter]
		?string $key = null
    ): ?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) {
            while (strlen($key) < SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
                $key .= $key;
            }
            $key = substr($key, 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
        }
        $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');
        $message = sodium_crypto_secretbox_open($encrypted_result, $nonce, $key);
        sodium_memzero($key);
        sodium_memzero($nonce);
        if ($message !== false) {
            spip_log("dechiffrer($encoded)=$message", 'chiffrer' . _LOG_DEBUG);
            return $message;
        }
        spip_log("dechiffrer() chiffre corrompu `$encoded`", 'chiffrer' . _LOG_DEBUG);
        return null;
	}

    private static function getDefaultKey(): ?string {
        $keys = SpipCles::instance();
        return $keys->getSecretSite();
    }
}
+52 −0
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
<?php

/***************************************************************************\
 *  SPIP, Système de publication pour l'internet                           *
 *                                                                         *
 *  Copyright © avec tendresse depuis 2001                                 *
 *  Arnaud Martin, Antoine Pitrou, Philippe Rivière, Emmanuel Saint-James  *
 *                                                                         *
 *  Ce programme est un logiciel libre distribué sous licence GNU/GPL.     *
 *  Pour plus de détails voir le fichier COPYING.txt ou l'aide en ligne.   *
 * \***************************************************************************/

namespace Spip\Core\Chiffrer;

class Cles implements \Countable /* , ContainerInterface */ {
	private array $keys;
	public function __construct(array $keys) {
        $this->keys = $keys;
	}

	public function has(string $name): bool {
		return array_key_exists($name, $this->keys);
	}

	public function get(string $name): ?string {
		return $this->keys[$name] ?? null;
	}

    public function generate(string $name): string {
        $key = Chiffrement::keygen();
        $this->keys[$name] = $key;
        spip_log("Création de la cle $name", 'chiffrer' . _LOG_INFO_IMPORTANTE);
        return $key;
    }

	public function set(
		string $name, 
		#[\SensitiveParameter]
		string $key
	): void {
		$this->keys[$name] = $key;
	}

	public function count(): int {
		return count($this->keys);
	}

	public function toJson(): string {
		$json = array_map('base64_encode', $this->keys);
		return \json_encode($json);
	}
}
+71 −0
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
<?php


/***************************************************************************\
 *  SPIP, Système de publication pour l'internet                           *
 *                                                                         *
 *  Copyright © avec tendresse depuis 2001                                 *
 *  Arnaud Martin, Antoine Pitrou, Philippe Rivière, Emmanuel Saint-James  *
 *                                                                         *
 *  Ce programme est un logiciel libre distribué sous licence GNU/GPL.     *
 *  Pour plus de détails voir le fichier COPYING.txt ou l'aide en ligne.   *
 * \***************************************************************************/

namespace Spip\Core\Chiffrer;

class Password {

	/**
	 * verifier qu'un mot de passe en clair est correct a l'aide de son hash
     * 
	 * Le mot de passe est poivre via la cle secret_des_auth
	 * 
	 * @param string $password_clair
	 * @param string $password_hash
     * @param string $key
	 * @return bool
	 */
	public static function verifier(
		#[\SensitiveParameter]
		string $password_clair, 
		#[\SensitiveParameter]
		string $password_hash, 
        #[\SensitiveParameter]
		?string $key = null
	): bool {
        $key ??= self::getDefaultKey();
		if ($key) {
			$pass_poivre = hash_hmac("sha256", $password_clair, $key);
			return password_verify($pass_poivre, $password_hash);
		}
		spip_log("Aucune clé pour vérifier le mot de passe", 'chiffrer' . _LOG_INFO_IMPORTANTE);
		return false;
	}

	/**
	 * Calculer un hash salé du mot de passe
	 * @param string $password_clair
	 * @param string $salt
	 * @return string
	 */
	public static function hacher(
		#[\SensitiveParameter]
		string $password_clair,
        #[\SensitiveParameter]
		?string $key = null
	): ?string {
        $key ??= self::getDefaultKey();
		// ne pas fournir un hash errone si la cle nous manque
		if ($key) {
			$pass_poivre = hash_hmac("sha256", $password_clair, $key);
			return password_hash($pass_poivre, PASSWORD_DEFAULT);
		}
		spip_log("Aucune clé pour chiffrer le mot de passe", 'chiffrer' . _LOG_INFO_IMPORTANTE);
		return null;
	}

    private static function getDefaultKey(): ?string {
        $keys = SpipCles::instance();
        return $keys->getSecretAuth();
    }
}
+137 −0
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
<?php

/***************************************************************************\
 *  SPIP, Système de publication pour l'internet                           *
 *                                                                         *
 *  Copyright © avec tendresse depuis 2001                                 *
 *  Arnaud Martin, Antoine Pitrou, Philippe Rivière, Emmanuel Saint-James  *
 *                                                                         *
 *  Ce programme est un logiciel libre distribué sous licence GNU/GPL.     *
 *  Pour plus de détails voir le fichier COPYING.txt ou l'aide en ligne.   *
 * \***************************************************************************/

namespace Spip\Core\Chiffrer;

final class SpipCles {
	private static array $instances = [];

	private string $file = _DIR_ETC . "cles.php";
	private Cles $cles;

	public static function instance(string $file = ''): self {
		if (empty(self::$instances[$file])) {
			self::$instances[$file] = new self($file);
		}
		return self::$instances[$file];
	}

	private function __construct(string $file = '') {
		if ($file) {
			$this->file = $file;
		}
		$this->cles = new Cles($this->read());
	}

	public function getSecretSite(bool $autoInit = true): ?string {
		return $this->getKey('secret_du_site', $autoInit);
	}
	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());
	}

	/**
	 * Fournir une sauvegarde chiffree des cles (a l'aide d'une autre clé, comme le pass d'un auteur)
	 * 
	 * @param string $withKey Clé de chiffrage de la sauvegarde
	 * @return string Contenu de la sauvegarde chiffrée générée
	 */
	public function backup(
		#[\SensitiveParameter]
		string $withKey
	): string {
		if (count($this->cles)) {
			return Chiffrement::chiffrer($this->cles->toJson(), $withKey);
		}
		return '';
	}

	/**
	 * Restaurer les cles manquantes depuis une sauvegarde chiffree des cles
	 * (si la sauvegarde est bien valide)
	 * 
	 * @param string $backup Sauvegarde chiffrée (générée par backup())
	 * @param int $id_auteur
	 * @param string $pass
	 * @return void
	 */
	public function restore(
		string $backup, 
		#[\SensitiveParameter]
		string $password_clair, 
		#[\SensitiveParameter]
		string $password_hash,
		int $id_auteur
	): bool {
		if (empty($backup)) {
			return false;
		}

		$sauvegarde = Chiffrement::dechiffrer($backup, $password_clair);
		$json = json_decode($sauvegarde, true);
		if (!$json) {
			return false;
		}

		// cela semble une sauvegarde valide
		$cles_potentielles = array_map('base64_decode', $json);

		// il faut faire une double verif sur secret_des_auth
		// pour s'assurer qu'elle permet bien de decrypter le pass de l'auteur qui fournit la sauvegarde
		// et par extension tous les passwords
		if (!empty($cles_potentielles['secret_des_auth'])) {
			if (!Password::verifier($password_clair, $password_hash, $cles_potentielles['secret_des_auth'])) {
				spip_log("Restauration de la cle `secret_des_auth` par id_auteur $id_auteur erronnee, on ignore", 'chiffrer' . _LOG_INFO_IMPORTANTE);
				unset($cles_potentielles['secret_des_auth']);
			}
		}

		// on merge les cles pour recuperer les cles manquantes
		$restauration = false;
		foreach ($cles_potentielles as $name => $key) {
			if (!$this->cles->has($name)) {
				$this->cles->set($name, $key);
				spip_log("Restauration de la cle $name par id_auteur $id_auteur", 'chiffrer' . _LOG_INFO_IMPORTANTE);
				$restauration = true;
			}
		}
		return $restauration;
	}

	private function getKey(string $name, bool $autoInit): ?string {
		if ($this->cles->has($name)) {
			return $this->cles->get($name);
		}
		if ($autoInit) {
			$this->cles->generate($name);
			$this->save();
			return $this->cles->get($name);
		}
        return null;
	}

	private function read(): array {
		lire_fichier_securise($this->file, $json);
		if (
			$json
			and $json = \json_decode($json, true)
			and is_array($json)
		) {
			return array_map('base64_decode', $json);
		}
		return [];
	}
}
Chargement en cours