Aller au contenu

Chiffrement

phil/backup

Clés

Une BackupKey est formée de 32 octets aléatoires. BackupKey::generate() en tire une, et toBase64() l'écrit pour l'endroit où l'application conserve ses secrets. BackupKey::fromBase64($value) la relit, sans tenir compte des blancs qui l'entourent. Une valeur qui n'est pas formée de 32 octets en base64 standard est refusée avec InvalidBackupValue :

php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'

Qui détient la clé lit toutes les sauvegardes ; qui la perd n'en lit aucune. Conservez-la à l'écart des sauvegardes — dans un magasin de secrets, pas dans le même compartiment de stockage — et gardez-en une copie là où un sinistre qui emporte le serveur n'emporte pas la clé. Pour changer de clé, gardez l'ancienne jusqu'à ce que le dernier jeu chiffré avec elle soit sorti de la rotation.

Le format

BackupCipher::encrypt($source, $target) écrit :

OctetsContenu
7la signature PHILBAK
1la version du format, 1
24l'en-tête du flux secret de libsodium (XChaCha20-Poly1305)
pour chaque bloc : 4la longueur du texte chiffré du bloc, gros-boutiste
pour chaque bloc : longueurle texte chiffré d'au plus 64 Kio du fichier, avec ses 17 octets d'étiquette et d'authentification

Chaque bloc est authentifié avec la signature et la version comme données supplémentaires, et le dernier porte l'étiquette finale du flux ; un fichier vide est un seul bloc final vide. Un fichier de toute taille passe par une quantité fixe de mémoire.

Ce qui est refusé

decrypt($source, $target) refuse, avec BackupFailed :

  • un fichier qui ne commence pas par la signature : … is not an encrypted backup. ;
  • une autre version : … is encrypted in a format this version does not read. ;
  • une autre clé, un octet modifié, des blocs réordonnés, un fichier tronqué — même à la frontière d'un bloc, puisque l'étiquette finale manque alors —, ou des octets après le bloc final : … cannot be decrypted: the key is not the one it was encrypted with, or the file is damaged.

La cible reçoit les blocs à mesure qu'ils sont authentifiés : en cas d'échec, elle contient un début qui ne doit pas servir. Backups déchiffre dans un fichier temporaire qu'il supprime quoi qu'il arrive, et ne restaure rien à partir d'un fichier en échec.

Jeux chiffrés

Avec un chiffreur, Backups::create() écrit chaque fichier du jeu dans un fichier temporaire en clair, le chiffre à sa place avec l'extension .enc et supprime la copie en clair. Un jeu se lit d'après l'extension de ses fichiers : sans clé, un jeu en clair se restaure, et un jeu chiffré est refusé avec The backup … is encrypted, and no backup key is given.

Avec une clé, un fichier en clair est refusé, que find() le reçoive ou que restore() ou verify() le lise, avec The backup … is in clear, and a backup key is given: sets in clear are refused unless they are allowed. Quiconque peut écrire dans le stockage des sauvegardes aurait pu l'y déposer, et rien ne l'authentifie : un vidage daté dans le futur deviendrait sinon le jeu le plus récent, et une archive en clair à côté d'un vrai .sql.enc écrirait des fichiers choisis dans les répertoires de médias. find(null) ne regarde que les jeux chiffrés, et un fichier de base de données n'est apparié qu'à une archive des médias de même chiffrement. Le sixième argument de Backups, $allowUnencrypted, accepte de nouveau les jeux en clair, pour ceux écrits avant que la clé soit donnée.