Skip to content
Extraits de code Groupes Projets
Valider 20908f78 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 62851442
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
......@@ -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;
}
......
......@@ -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();
}
/**
......
......@@ -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;
}
}
......@@ -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 (
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Veuillez vous inscrire ou vous pour commenter