Une bibliothèque de cryptographie PHP contemporaine vers 2023.
Synopsis:
Cette bibliothèque est un wrapper autour de la bibliothèque PHP Sodium et de la bibliothèque PHP OpenSSL.
Le code de sodium dans cette bibliothèque est basé sur l'exemple donné dans la documentation de la fonction PHP Sodium_Crypto_SecretBox ().
Le code OpenSSL de cette bibliothèque est basé sur l'exemple donné dans la documentation de la fonction PHP OpenSSL_ECCRYPT ().
Cette bibliothèque vise à garantir que les données qu'il cryptent est aussi sécurisée que les clés secrètes utilisées pour la crypter. Des mesures sont également prises dans le but de s'assurer que les données cryptées sont à l'épreuve.
Cette bibliothèque ne peut pas résoudre le problème difficile de la gestion des clés.

Cette bibliothèque est un travail en cours.
Je partage ce code avec des amis et des collègues, sollicitant autant de critiques et de commentaires que possible. Lorsque je pense que cette bibliothèque est aussi bonne que je peux le faire, je mettrai à jour cette note d'état. Dans l'intervalle, les changements de rupture sont presque certains et les faiblesses de cryptage sont tout à fait possibles. Si vous trouvez quelque chose que vous pensez que je devrais savoir, faites-le moi savoir!
Je veux être clair avec vous que cette bibliothèque est grande et complexe et qu'elle n'a pas vu beaucoup d'utilisation; Il est sans aucun doute chargé d'insectes subtiles qui n'ont pas encore été découvertes. Je pense que cette base de code a le potentiel de mûrir dans un outil solide et fiable, mais nous devons passer par le processus.
Veuillez lire cette section.
Il existe de nombreuses façons de vous tromper avec votre code cryptographique. Cette bibliothèque a été écrite comme une tentative de réduire les buts de pied cryptographiques; J'espère que cela n'en a pas présenté!
La première chose à savoir sur la crypto est que vos données sont aussi sécurisées que vos clés. Il y a plus à savoir sur la gestion clé que je ne peux vous dire ici (et je ne suis pas un expert de toute façon), mais voici quelques choses à penser:
Quelques autres choses à connaître:
get_error() après le chiffrement pour vous assurer que cela ne fait pas d'erreur).Une autre chose, qui m'a surpris quand je l'ai appris, bien que ce soit assez évident une fois que vous le savez, c'est que vous ne devriez pas compresser vos données avant de la crypter. Ce n'est pas toujours un problème, mais dans certaines circonstances, cela peut être, donc il est probablement préférable de ne jamais le faire.
Le problème de compression est que si un attaquant peut contrôler certaines des données d'entrée, il peut inclure une valeur particulière, puis si la sortie diminue en taille, il peut savoir que l'autre entrée également incluse dans la valeur particulière. Aie.
Cette base de code n'est pas mature ou bien testée, avant de l'utiliser, vous devez lire tout le code pour vous assurer qu'il répond à vos normes de qualité. Si vous le faites, je serais heureux de vous entendre.
Si vous pouvez penser à quelque chose d'autre que tout le monde devrait savoir et faire attention, faites-le moi savoir!
Vous ne voulez pas rtfm ..? Et me voici, écrivant tout ça ... Sheesh. Lisez au moins les avertissements énumérés ci-dessus.
#!/bin/bash
set -euo pipefail;
mkdir -p kickass-demo/lib
cd kickass-demo
git clone https://github.com/jj5/kickass-crypto.git lib/kickass-crypto 2>/dev/null
php lib/kickass-crypto/bin/gen-demo-config.php > config.php
cat > demo.php <<'EOF'
<?php
require_once __DIR__ . '/lib/kickass-crypto/inc/sodium.php';
require_once __DIR__ . '/config.php';
$ciphertext = kickass_round_trip()->encrypt( 'secret text' );
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
echo "the secret data is: $plaintext.n";
EOF
php demo.php
Pour un peu plus d'élaboration, consultez peut-être l'exemple de code.
Ou si vous voulez que cette bibliothèque fonctionne cette bibliothèque, lisez le code dans le cadre de la bibliothèque ou l'autre code.
Gee, ça a commencé simplement assez, mais ça a été un peu compliqué à la fin.
Je voulais aller aller à quelques données relativement sensibles (numéros de version des lignes pour un contrôle de concurrence optimiste) entre mon serveur et ses clients de manière relativement sécurisée, le secret et le falsification préféré.
J'avais entendu dire que la bibliothèque OpenSSL était disponible en PHP, j'ai donc recherché des informations concernant la façon de l'utiliser. J'ai trouvé un exemple de code dans la documentation PHP pour la fonction openssl_encrypt ().
Initialement, il ne m'a pas été clair comment utiliser ce code. Il était en particulier difficile de comprendre quoi faire avec les trois parties: la balise d'authentification, le vecteur d'initialisation et le texte de chiffre. Finalement, j'ai compris que je pouvais simplement les concaténer. Mais si je le faisais, je devrais standardiser sur leur longueur et leur placement afin que je puisse les récupérer plus tard ...
... Et puis j'ai pensé qu'il serait préférable de masquer ma taille réelle de données en la rembourrant à des longueurs fixes à certaines limites, donc j'ai fait ça ...
... Et puis je voulais prendre en charge des données riches qui exigeaient une forme de sérialisation. Initialement, j'utilisais la fonction PHP Serialize (), mais cela a été modifié plus tard pour json_encode ().
L'exemple de code n'a rien indiqué du tout sur la façon de faire tourner les clés de manière prise en charge. J'ai donc proposé les deux cas d'utilisation pris en charge par cette bibliothèque avec différentes approches de la gestion des clés pour les scénarios aller-retour et en repos. Cette bibliothèque vous permet de tourner dans de nouvelles clés tout en maintenant la prise en charge des clés plus anciennes, comme vous n'aurez probablement pas besoin de le faire.
Ensuite, j'ai superposé une approche minutieuse de la gestion des exceptions et des rapports d'erreurs, certains tests unitaires et validation, l'atténuation des attaques de synchronisation, les localisateurs de services, la démonstration d'utilisation, les limites de taille des données, l'initialisation en phrase secrète, les scripts de génération de clés, la télémétrie et des choses comme ça.
Fondamentalement, cette bibliothèque entière était à peu près tout ce que je ressentais que je devais faire pour pouvoir réellement utiliser l'implémentation de bibliothèque PHP intégrée OpenSSL.
Et puis ... les gens ont commencé à me parler de la bibliothèque de sodium, et suggérant que je l'utilise à la place. Étant donné que j'avais déjà fait un tas de travaux pour la gestion des clés et la sérialisation d'entrée et la mise en forme et le codage des messages, etc., j'ai pensé que je pouvais simplement réutiliser tout cela et fournir un emballage autour du sodium. C'est donc ce que j'ai fait.
Maintenant, si vous utilisez cette bibliothèque, vous pouvez décider si vous souhaitez utiliser l'implémentation de sodium ou l'implémentation OpenSSL. Parce que les deux implémentations peuvent coexister avec plaisir, vous pouvez également écrire du code pour passer de l'un à l'autre, si vous le souhaitez. Les implémentations ne partagent jamais la configuration ou les formats de données de clés, ils sont entièrement séparés. (Cela dit, il n'est pas exactement trivial de changer d'algorithmes de chiffrement et vous devez probablement vous déconcerter pour migrer toutes vos données et si vous ne pouvez pas le faire, vous allez passer un mauvais moment, alors ne prévoyez pas de changer d'algorithmes, si vous n'êtes pas sûr de commencer par le sodium et de vous y tenir.)
Je ne considère pas cette bibliothèque enroulant ma propre crypto , mais je pense plutôt que pour savoir comment utiliser réellement du sodium et openser . Si j'ai fait des erreurs, évidentes ou autres, j'apprécierais vraiment d'en entendre parler.
En supposant que je me souviens de le mettre à jour de temps en temps, il y a un système de démonstration ici:
L'installation de démonstration montre comment les données chiffrées aller-retour entre le client et le serveur à l'aide de HTML et HTTP.
Le code de démonstration est disponible dans cette bibliothèque dans le src / démo / répertoire si vous souhaitez l'héberger vous-même.
En supposant que je me souviens de les mettre à jour de temps en temps, les documents PHP sont ici:
Comme mentionné ci-dessus, vous pouvez consulter le code de Git avec une commande comme celle-ci:
git clone https://github.com/jj5/kickass-crypto.git
Ce code n'est pas sorti, il n'y a pas de version stable.
Si vous souhaitez inclure la bibliothèque client pour une utilisation dans votre application, incluez le fichier Inc / Sodium.php ou Inc / OpenSSL.PHP qui s'occupera de tout le reste; Utilisez quelque chose comme ceci:
require_once __DIR__ . '/lib/kickass-crypto/inc/sodium.php';
Après avoir chargé cette bibliothèque, vous accéderez généralement via les localisateurs de service kickass_round_trip() ou kickass_at_rest() qui sont documentés ci-dessous, quelque chose comme ceci:
$ciphertext = kickass_round_trip()->encrypt( 'secret text' );
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
echo "the secret data is: $plaintext.n";
Il a fallu beaucoup de travail pour rendre les choses aussi simples!
Si vous souhaitez héberger le code de démonstration, vous devez héberger les fichiers dans SRC / Demo / et inclure un fichier config.php valide dans le répertoire de base de projet (c'est le répertoire qui comprend ce fichier readme.md). À des fins de démonstration, un fichier config.php valide n'a besoin que de définir une chaîne constante pour CONFIG_SODIUM_SECRET_CURR , mais elle doit être une chaîne longue et aléatoire, vous pouvez générer une chaîne appropriée avec:
php bin/gen-key.php
Ou vous pouvez simplement générer un fichier config.php démo entier avec:
php bin/gen-demo-config.php > config.php
Voici quelques notes sur les fichiers logiciels et les lignes de code.
Total Number of Files = 128
Total Number of Source Code Files = 128
| Annuaire | Fichiers | Par la langue |
|---|---|---|
| test | 63 | php = 59, sh = 4 |
| code | 35 | php = 35 |
| bac | 22 | php = 13, sh = 9 |
| Inc | 7 | php = 7 |
| démo | 1 | php = 1 |
| Langue | Fichiers | Pourcentage |
|---|---|---|
| php | 115 | (89,84%) |
| shot | 13 | (10,16%) |
Total Physical Source Lines of Code (SLOC) = 9,210
Development Effort Estimate, Person-Years (Person-Months) = 2.06 (24.70)
(Basic COCOMO model, Person-Months = 2.4 * (KSLOC**1.05))
Schedule Estimate, Years (Months) = 0.70 (8.46)
(Basic COCOMO model, Months = 2.5 * (person-months**0.38))
Estimated Average Number of Developers (Effort/Schedule) = 2.92
Total Estimated Cost to Develop = $ 278,044
(average salary = $56,286/year, overhead = 2.40).
| Annuaire | Sloc | Par la langue |
|---|---|---|
| code | 5 136 | php = 5136 |
| test | 3 363 | php = 3193, sh = 170 |
| bac | 603 | php = 423, sh = 180 |
| démo | 71 | php = 71 |
| Inc | 37 | php = 37 |
| Langue | Sloc | Pourcentage |
|---|---|---|
| php | 8 860 | (96,20%) |
| shot | 350 | (3,80%) |
Ce code doit fonctionner sur PHP 7,4 ou plus. Si vous essayez d'exécuter ce code sur une ancienne version de PHP, il essaiera de journaliser un message d'erreur, puis de quitter votre processus.
Ce code vérifiera qu'il fonctionne sur une plate-forme 64 bits. Si ce n'est pas le cas, il se plaindra et sortira.
Si vous chargez le module de sodium, la bibliothèque garantira que la bibliothèque de sodium est réellement disponible. Si ce n'est pas le cas, le processus se plaindra et sortira.
Si vous chargez le module OpenSSL, la bibliothèque s'assurera que la bibliothèque OpenSSL est réellement disponible. Si ce n'est pas le cas, le processus se plaindra et sortira.
Je crois que ce code devrait fonctionner sur n'importe quel système d'exploitation, mais je ne l'ai testé que sur Linux. Si vous avez réussi sur MacOS ou Windows, je serais heureux d'en entendre parler.
Les scripts shell sont écrits pour bash. Si vous n'avez pas de bash, vous devrez peut-être porter.
Ce code prend en charge deux cas d'utilisation spécifiques:
Les clés sont gérées séparément et différemment pour chaque cas d'utilisation.
Les détails de la prise en charge de chaque cas d'utilisation sont documentés ci-dessous.
L'utilisation de cette bibliothèque pour le chiffrement en repos est généralement un risque plus important et un engagement plus important que de l'utiliser simplement pour le chiffrement aller-retour. Si vous perdez vos clés de cryptage aller-retour ou si vous êtes obligé de les faire pivoter d'urgence, ce sera probablement moins un problème que si quelque chose de similaire se produisait avec vos clés en repos.
Le principal cas d'utilisation pour lequel cette bibliothèque a été développée a été de soutenir le ralentissement de quelques kilobytes de données contenant des numéros de version de ligne légèrement sensibles mais pas à la mission pour un contrôle de concurrence optimiste. Par rapport à l'alternative (pas de cryptage ou de non-étanche des données optimistes de contrôle de concurrence), l'utilisation de cette bibliothèque est une amélioration. Que ce soit vraiment approprié dans d'autres applications est une question ouverte, je ne suis pas sûr. Vous ne devriez certainement pas utiliser cette bibliothèque si elle ne fournit pas le niveau de sécurité dont vous avez besoin.
La manière préférée et prise en charge de nommer des secrets dans les fichiers de configuration est comme constantes à l'aide de la fonction php Define (). Le problème avec l'utilisation de champs de classe / d'instance ou de variables globales est que les valeurs peuvent facilement couler dans le code de débogage et de journalisation, cela est moins probable (bien que toujours possible) pour les constantes. De même, si vous avez besoin de mettre en cache des données globales / statiques (telles que la lecture du fichier de configuration), la meilleure façon de le faire est avec une variable statique locale dans une fonction, si possible, car l'utilisation des champs d'instance, des champs de classe ou des globaux peut entraîner plus facilement des fuites secrètes.
Pour vous donner un exemple, créons un fichier de test appelé double-define.php comme ceci:
<?php
define( 'TEST', 123 );
define( 'TEST', 456 );
Ensuite, lorsque nous exécutons le code, quelque chose comme ça se produit:
$ php double-define.php
PHP Warning: Constant TEST already defined in ./double-define.php on line 4
PHP Stack trace:
PHP 1. {main}() ./double-define.php:0
PHP 2. define($constant_name = 'TEST', $value = 456) ./double-define.php:4
Si cette valeur constante contenait votre clé secrète, vous avez juste eu une très mauvaise journée.
Le moyen le plus sûr de définir une constante en PHP est de vérifier qu'il n'est pas déjà défini en premier, car la tentative de définir une constante déjà définie entraînera une erreur. Si vous trouvez une constante déjà définie, vous pouvez soit abandonner avec un message d'erreur (si vous ne fournissez pas trop de détails car le Web public peut le voir) ou simplement conserver la valeur existante et n'essayez pas de la redéfinir. Le générateur de fichiers de configuration bin / gen-demo-config.php prend la première approche et appelle la fonction php die() si un double est détecté. Vous pouvez voir ce qui se passe en incluant deux fois le fichier config.php généré, comme:
require __DIR__ . '/config.php';
require __DIR__ . '/config.php';
Vous pouvez trouver un exemple de ce qui se passe si vous incluez le double de la config.php dans config-die.php.
Par conséquent, comme pour la plupart des fichiers source PHP, il est préférable d'utiliser require_once lors de l'inclusion du fichier config.php :
require_once __DIR__ . '/config.php';
Lorsque je nomme des choses qui sont secrètes, je m'assure que le nom contient la chaîne "passer" (comme dans "Mot de passe", "passwd" et "PassPhrase", ou même, à un étirement, "passeport") ou "secret". Dans mes installations de journalisation générale (qui ne sont pas incluses dans cette bibliothèque), je froisse et révèle tout ce qui correspond à un nom qui correspond (insensible au cas) avant de journaliser les données de diagnostic. Je vous encourage à adopter cette pratique.
Dans cette bibliothèque, si une variable ou une constante peut contenir des données sensibles, elle sera nommée avec "passer" ou "secret" en tant que sous-chaîne dans le nom.
N'écrivez pas de données sensibles dans les journaux.
Mettez «passer» ou «secret» au nom de variables sensibles, de champs ou de constantes.
Ici, j'explique ce que ces termes de sondage similaires signifient réellement dans le contexte de cette bibliothèque.
Si vous utilisez les modules par défaut, le format de données est soit "KA0" pour le module OpenSSL ou "KAS0" pour le module de sodium.
Si vous héritez du cadre de base et définissez votre propre module de crypto, le format de données par défaut est "XKA0" pour un module basé sur l'implémentation OpenSSL ou "xkas0" pour un module basé sur l'implémentation de sodium, sinon votre implémentation de do_get_const_data_format() détermine ce que le format de données sera connu car vous pouvez faire quelque chose si longtemps que cela ne commence pas avec la chaîne " implémentations.
Vous devez utiliser le bon module pour le format de données afin de décrypter avec succès un texte chiffré.
Le codage des données est soit JSON, sérialisation PHP ou texte. En supposant que vous disposez du bon module pour le format de données (ci-dessus), et avec une mise en garde discutée ci-dessous, vous pouvez décrypter n'importe quoi indépendamment de la codage des données utilisée. Le chiffrement sera effectué en utilisant le codage des données configurées, voir config_encryption_data_encoding, il peut être l'un des:
Notez que vous ne pourrez pas utiliser le codage PHPS à moins que vous ne définissiez également config_encryption_phps_enable, c'est parce que la désérialisation PHP peut être dangereuse afin qu'elle soit désactivée par défaut. Honnêtement, c'est un peu la main. Je viens d'entendre des rumeurs selon lesquelles PHP unserialize() peut conduire à l'injection de code, mais je ne sais pas si c'est vrai ou ce que cela signifie exactement. J'ai implémenté la sérialisation et la désérialisation PHP et je lui ai donné un peu de test, mais je ne sais pas si c'est vraiment peu sûr ou non. Je suis à peu près sûr que le codage des données JSON et du texte devrait être sûr.
En plus de hériter de KickassCrypto et de remplacer une fonctionnalité particulière, beaucoup de configuration est disponible via les constantes de configuration. Recherchez CONFIG_SODIUM pour trouver ce qui est disponible pour le sodium et CONFIG_OPENSSL pour trouver ce qui est disponible pour OpenSSL.
Veuillez noter qu'à l'heure actuelle, ce code est configuré directement dans le fichier config.php .
À l'avenir, le fichier config.php comprendra des fichiers de configuration gérés séparément, étant:
Il y aura des scripts de gestion pour la rotation automatique et l'approvisionnement des clés dans ces fichiers.
Les utilisateurs de Linux expérimentés savent que vous ne modifiez pas directement /etc/sudoers , vous le modifiez avec visudo afin que vous puissiez vérifier que vous n'avez pas accidentellement introduit une erreur de syntaxe et arrosé votre système.
J'ai l'intention de fournir des scripts similaires pour l'édition et la gestion config.php et d'autres fichiers de configuration. Donc, veille pour ces mises à jour. En attendant ... soyez très prudent .
Une chose que vous devriez être très prudente que vous ne faites pas est de gérer vos clés dans autre chose qu'un fichier PHP avec une extension de fichier ".php". Si vous mettez vos clés dans un fichier ".ini" ou quelque chose comme ça , ils pourraient très bien être servis par votre serveur Web . Alors ne faites pas ça. Faites également attention à ne pas introduire des erreurs de syntaxe dans votre fichier de configuration ou d'autres fichiers source en production, car les détails peuvent fuir avec le potentiel des messages d'erreur résultants.
Comme mentionné dans la section précédente, une bonne quantité de configurabilité est fournie par la prise en charge des constantes de configuration nommées.
En plus des constantes de configuration, vous pouvez faire beaucoup si vous héritez de la classe de base KickassCrypto et remplacez ses méthodes.
En tant qu'alternative aux constantes de configuration (qui ne peuvent être définies qu'une fois par processus et par la suite ne peuvent pas être modifiées), il existe des méthodes d'instance comme get_config_...() pour les options de configuration et get_const_...() pour une évaluation constante. Les constantes et les options de configuration les plus importantes sont lues indirectement via ces accessoires, vous devriez donc pouvoir les remplacer de manière fiable.
La plupart des appels aux fonctions intégrés PHP sont effectués par des emballages minces via des fonctions protégées sur KickassCrypto . Ceux-ci sont définis dans le trait KICKASS_WRAPPER_PHP . Cette indirection permet à certaines invocations de la fonction PHP d'être interceptées et potentiellement modifiées. Cela a été fait principalement pour soutenir l'injection de défauts lors des tests unitaires, mais vous pouvez utiliser à d'autres fins pour modifier les détails de l'implémentation.
Les choses qui sont considérées comme sensibles dans KickassCrypto sont définies comme privées ou finales . Si ce n'est pas privé et que ce n'est pas définitif, c'est un jeu juste pour l'emporter (sauf si j'ai fait une erreur). En particulier, les méthodes d'instance qui commencent par do_ ont été spécifiquement conçues pour être remplacées ou interceptées par des implémenteurs.
Cette bibliothèque fournit deux fonctions de localisateur de service qui gèrent chacune une instance de la bibliothèque crypto, ce sont:
kickass_round_trip()kickass_at_rest()Vous pouvez remplacer l'instance du service fournie par la fonction de localisateur de service en appelant la fonction et en passant la nouvelle instance comme paramètre unique, comme ceci:
class MyKickassCrypto extends KickassCryptoKickassCrypto {
protected function do_is_valid_config( &$problem = null ) { return TODO; }
protected function do_get_passphrase_list() { return TODO; }
// ... other function overrides ...
}
kickass_round_trip( new MyKickassCrypto );
Idéalement, cette bibliothèque répondra à vos exigences hors de la boîte (ou avec une certaine configuration) et vous n'aurez pas besoin de remplacer les instances fournies par les localisateurs de service par défaut.
Un localisateur de service créera une nouvelle instance par défaut pour vous lors du premier appel au localisateur de service s'il n'a pas déjà d'instance. Que l'implémentation par défaut soit le module de sodium ou le module OpenSSL dépend de l'ordre que vous avez inclus les fichiers inc/sodium.php et inc/openssl.php ; Si vous avez inclus la bibliothèque entière avec inc/library.php le module de sodium aura la priorité.
Que vous ayez chargé les localisateurs de service pour le module de sodium ou le module OpenSSL, vous pourrez remplacer l'instance par défaut en appelant le localisateur de service avec une nouvelle instance comme argument.
Le processus de cryptage est à peu près:
Notez que la bibliothèque de sodium utilise un nonce au lieu d'un vecteur d'initialisation (à un effet similaire) et que le sodium gère sa propre balise d'authentification.
Lorsque cette bibliothèque code pour son texte chiffré, il comprend un préfixe de format de données de "KAS0 /" pour l'implémentation de sodium et "KA0 /" pour l'implémentation OpenSSL.
Le zéro ("0") dans le préfixe de format de données est destiné à la version Zero , qui est destiné à impliquer que l'interface est instable et peut changer .
Les versions futures de cette bibliothèque pourraient implémenter un nouveau préfixe de format de données pour un format de données stable.
Lorsque cette bibliothèque décode son texte chiffré, il vérifie le préfixe de format de données. À l'heure actuelle, seul "KAS0 /" ou "KA0 /" est pris en charge.
Le format de données de la version zéro, mentionné ci-dessus, implique actuellement les éléments suivants:
Une fois le codage de données (JSON par défaut, discuté dans la section suivante), le rembourrage est effectué et la longueur des données est préfixée. Avant le cryptage, le message est formaté, comme ceci:
$message = $encoded_data_length . '|json|' . $encoded_data . $this->get_padding( $pad_length );
La longueur de données JSON est formatée en valeur hexadécimale de 8 caractères. La taille de 8 caractères est constante et ne varie pas en fonction de l'ampleur de la longueur des données JSON.
La raison du rembourrage est d'obscurcir la taille réelle des données. Le rembourrage se fait dans jusqu'à 4 limites de kib (2 12 octets), que nous appelons des morceaux. La taille du morceau est configurable et la valeur par défaut peut changer à l'avenir.
Ensuite, si nous chiffrons avec du sodium, le message est chiffré avec sodium_crypto_secretbox() , puis nonce et le texte chiffré sont concaténés ensemble, comme ceci:
$nonce . $ciphertext
Sinon, si nous chiffrons avec OpenSSL, le message est chiffré avec AES-256-GCM et le vecteur d'initialisation, le texte chiffré et la balise d'authentification sont concaténés ensemble, comme ceci:
$iv . $ciphertext . $tag
Ensuite, tout est codé Base64 avec la fonction PHP Base64_Encode () et le préfixe de format de données est ajouté.
Pour le sodium qui est fait comme ceci:
"KAS0/" . base64_encode( $nonce . $ciphertext )
Et pour OpenSSL, c'est fait comme ceci:
"KA0/" . base64_encode( $iv . $ciphertext . $tag )
Le processus de décryptage prévoit de trouver le nonce de 24 octets et le texte chiffré pour le format de données "KAS0" et le vecteur d'initialisation de 12 octets, le texte chiffré et la balise d'authentification de 16 octets pour le format de données KA0.
Après avoir décryé le texte chiffré, la bibliothèque prévoit de trouver la taille des données JSON en tant que chaîne ASCII représentant un indicateur de codage de données de 8 caractères (`` JSON '' ou `` PHPS ''), suivi d'un seul caractère de tuyau, suivi de la JSON (ou des données sérialisées PHP), puis du padding. La bibliothèque peut ensuite extraire les données JSON / sérialisées de son rembourrage et s'occuper du reste du décodage.
Avant que les données d'entrée de chiffrement soient codées sous forme de JSON à l'aide de la fonction php json_encode (). Initialement, cette bibliothèque a utilisé la fonction PHP Serialize (), mais apparemment, cela peut conduire à certains scénarios d'exécution de code (je ne suis pas sûr sur les détails), il a donc été décidé que le codage JSON était plus sûr. Ainsi, maintenant, nous utilisons à la place le codage JSON.
L'utilisation de JSON comme format de codage de données a quelques implications mineures concernant les valeurs que nous pouvons prendre en charge. En particulier, nous ne pouvons pas encoder des instances d'objets qui peuvent ensuite être décodées à des instances d'objet (si les objets implémentent l'interface jsonSerializable, ils peuvent être sérialisés sous forme de données, mais ceux-ci ne seront décodés qu'aux tableaux PHP, pas les objets PHP à partir desquels ils sont provenant); Certaines valeurs de points flottants impairs ne peuvent pas être représentés (c'est-à-dire Nan, POS Inf, Neg Info et Neg Zero); Et les cordes binaires ne peuvent pas être représentées dans JSON.
Par défaut, ces options sont utilisées pour le codage JSON:
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
Mais ces options ne affecteront pas la capacité d'une implémentation à décoder le JSON. Les implémentations peuvent affiner le codage et le décodage JSON si nécessaire en remplacement des méthodes data_encode () et data_decode (). Alternativement, vous pouvez nommer les options de codage et de décodage JSON dans votre fichier config.php avec le CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS et CONFIG_ENCRYPTION_JSON_DECODE_OPTIONS Constantes, par exemple:
define( 'CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
define( 'CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS', JSON_THROW_ON_ERROR );
Cette bibliothèque doit fonctionner, que JSON_THROW_ON_ERROR soit spécifiée ou non.
Si vous spécifiez JSON_PARTIAL_OUTPUT_ON_ERROR dans vos options de codage JSON, vos données peuvent devenir silencieusement invalides, alors faites-le à vos propres risques. Peut-être contre-intuitivement, j'ai découvert que l'activation JSON_PARTIAL_OUTPUT_ON_ERROR est la moindre stratégie car au moins dans ce cas, vous obtenez quelque chose . Si vous n'activez pas JSON_PARTIAL_OUTPUT_ON_ERROR si une partie de votre entrée ne peut pas être codée (par exemple lorsque vous avez des chaînes binaires qui ne sont pas dans un codage valide tel que UTF-8), alors l'ensemble des données est supprimé. Avec JSON_PARTIAL_OUTPUT_ON_ERROR seule la partie imprésentable est omise. Pour le moment, JSON_PARTIAL_OUTPUT_ON_ERROR n'est pas automatiquement spécifié, mais c'est quelque chose que je pourrais revoir à l'avenir.
Si vous utilisez l'une de ces options de codage / décodage JSON, vous pourriez très bien finir par passer un mauvais moment:
JSON_NUMERIC_CHECKJSON_INVALID_UTF8_IGNOREJSON_INVALID_UTF8_SUBSTITUTE Lorsque cette bibliothèque crypte ses données, elle remporte sa sortie jusqu'à une taille de morceau configurable.
La constante de configuration pour la taille du morceau est CONFIG_ENCRYPTION_CHUNK_SIZE .
La taille du morceau par défaut est de 4 096 (2 12 ).
Si vous vouliez augmenter la taille du morceau à 8 192, vous pouvez le faire dans votre fichier config.php comme ceci:
define( 'CONFIG_ENCRYPTION_CHUNK_SIZE', 8912 );
Vous pouvez modifier la taille du morceau défini et il commencera à s'appliquer à de nouvelles données, et les anciennes données cryptées avec une taille de morceau différente pourront toujours être déchiffrées.
Tant que les limites de taille des données sont observées (celles-ci sont discutées ensuite), cette bibliothèque peut crypter tout ce qui peut être codé en JSON par PHP.
Cela comprend une variété de choses, comme:
Des choses qui ne peuvent pas être soutenues avec JSON:
Notez que la valeur booléenne false ne peut pas être cryptée. Ce n'est pas parce que nous ne pouvions pas le crypter, c'est parce que nous le renvoyons lorsque le décryptage échoue. Nous refusons donc de crypter faux afin qu'il ne puisse pas être confondu avec une erreur lors du décryptage.
Si vous avez besoin de crypter la valeur booléenne, envisagez de le mettre dans un tableau, comme ceci:
$input = [ 'value' => false ];
Ou encodant comme JSON, comme ceci:
$input = json_encode( false );
Si vous faites l'une de ces choses, vous pourrez crypter votre valeur.
Il convient de souligner que dans PHP, les "chaînes" sont essentiellement des tableaux d'octets, ce qui signifie qu'ils peuvent contenir des données essentiellement "binaires". Ces données binaires ne peuvent cependant pas être représentées comme JSON. Si vous devez gérer les données binaires, la meilleure façon est probablement de les coder sous le nom de base64 avec Base64_Encode () ou hexaDecimal avec bin2hex (), puis crypter cela.
À l'avenir, la possibilité de travailler avec des données qui ne sont pas toujours encodées par JSON pourraient être ajoutées à cette bibliothèque. Faites-moi savoir si c'est une fonctionnalité que vous voulez avoir.
Remarque: l'utilisation de la sérialisation PHP au lieu de l'encodage JSON est désormais une option; Cette documentation doit être mise à jour pour expliquer comment elle fonctionne et comment l'utiliser. L'avantage de la sérialisation PHP est qu'il prend en charge plus de types de données et de formats que JSON.
Une fois les données encodées en JSON, elles sont limitées à une longueur maximale configurable.
La constante de configuration pour la longueur de codage JSON maximale est CONFIG_ENCRYPTION_DATA_LENGTH_MAX .
La limite de codage de données par défaut est de 67 108 864 (2 ^ 26 ) octets, soit environ 67 Mo ou 64 MIB.
Il est possible de configurer cette limite d'encodage de données, si vous devez la rendre plus grande ou plus petite. Sachez simplement que si vous rendez la limite trop grande, vous vous retrouverez avec des problèmes de mémoire et que votre processus pourrait être terminé.
Si vous vouliez diminuer la limite d'encodage des données, vous pouvez le faire dans votre fichier config.php comme ceci:
define( 'CONFIG_ENCRYPTION_DATA_LENGTH_MAX', pow( 2, 25 ) );
Cette bibliothèque ne compresse pas les données d'entrée, car la compression peut introduire des faiblesses cryptographiques, comme dans l'attaque du crime SSL / TLS.
Le problème est que si l'attaquant peut modifier une partie du texte brut, il peut découvrir si les données, elles entrent dans d'autres parties du texte brut, car s'ils mettent une valeur et le résultat est plus petit, c'est parce qu'il existe dans la partie du texte brut qu'ils ne savaient pas, mais faites maintenant!
Il est très important de ne pas compresser les données qu'un attaquant peut fournir d'autres données secrètes. Il est préférable de ne pas du tout compresser.
Si une erreur est rencontrée pendant le cryptage ou le décryptage, un délai compris entre 1 milliseconde (1 ms) et 10 secondes (10 s) est introduit. Il s'agit d'une atténuation contre les attaques de synchronisation potentielles. Voir S2N et Lucky 13 pour la discussion.
Note that avoiding timing attacks is hard. A malicious guest on your VPS host (or a malicious person listening to your server's fans! ?) could figure out that your process is sleeping rather than doing actual work.
This library includes a method called delay() , and this method is called automatically on the first instance of an error. The delay() method does what is says on the tin: it injects a random delay into the process. The delay() method is public and you can call it yourself if you feel the need. Each time delay() is called it will sleep for a random amount of time between 1 millisecond and 10 seconds.
The programmer using this library has the opportunity to override the do_delay() method and provide their own delay logic.
If that do_delay() override throws an exception it will be handled and an emergency delay will be injected.
If you do override do_delay() but don't actually delay for at least the minimum duration (which is 1 ms) then the library will inject the emergency delay.
The main reason for allowing the implementer to customize the delay logic is so that unit tests can delay for a minimum amount of time. Ordinarily there shouldn't be any reason to meddle with the delay logic and it might be less safe to do so.
When an instance of one of of the following is created the configuration settings are validated.
KickassSodiumRoundTripKickassSodiumAtRestKickassOpenSSLRoundTripKickassOpenSSLAtRestIf the configuration settings are not valid the constructor will throw an exception. If the constructor succeeds then encryption and decryption later on should also (usually) succeed. If there are any configuration problems that will mean encryption or decryption won't be able to succeed (such as secret keys not having been provided) the constructor should throw.
This library defines its own exception class called KickassException . This works like a normal Exception except that it adds a method getData() which can return any data associated with the exception. A KickassException doesn't always have associated data.
Of course not all problems will be able to be diagnosed in advance. If the library can't complete an encryption or decryption operation after a successful construction it will signal the error by returning the boolean value false. Returning false on error is a PHP idiom, and we use this idiom rather than raising an exception to limit the possibility of an exception being thrown while an encryption secret or passphrase is on the call stack.
The problem with having sensitive data on the call stack when an exception is raised is that the data can be copied into stack traces, which can get saved, serialized, displayed to users, logged, etc. We don't want that so we try very hard not to raise exceptions while sensitive data might be on the stack.
If false is returned on error, one or more error messages will be added to an internal list of errors. The caller can get the latest error by calling the method get_error . If you want the full list of errors, call get_error_list .
If there were any errors registered by the OpenSSL library functions (which the OpenSSL module calls to do the heavy lifting), then the last such error is available if you call the get_openssl_error() . You can clear the current error list (and OpenSSL error message) by calling the method clear_error() .
For the PHP Sodium implementation the function we use is sodium_crypto_secretbox(). That's XSalsa20 stream cipher encryption with Poly1305 MAC authentication and integrity checking.
For the PHP OpenSSL implementation the cipher suite we use is AES-256-GCM. That's Advanced Encryption Standard encryption with Galois/Counter Mode authentication and integrity checking.
Secret keys are the secret values you keep in your config.php file which will be processed and turned into passphrases for use by the Sodium and OpenSSL library functions. This library automatically handles converting secret keys into passphrases so your only responsibility is to nominate the secret keys.
The secret keys used vary based on the use case and the module. There are two default use cases, known as round-trip and at-rest.
The "256" in AES-256-GCM means that this cipher suite expects 256-bit (32 byte) passphrases. The Sodium library sodium_crypto_secretbox() function also expects a 256-bit (32 byte) passphrase.
We use a hash algorithm to convert our secret keys into 256-bit binary strings which can be used as the passphrases the cipher algorithms expect.
The minimum secret key length required is 88 bytes. When these keys are generated by this library they are generated with 66 bytes of random data which is then base64 encoded.
The secret key hashing algorithm we use is SHA512/256. That's 256-bits worth of data taken from the SHA512 hash of the secret key. When this hash code is applied with raw binary output from an 88 byte base64 encoded input you should be getting about 32 bytes of randomness for your keys.
The Sodium library expects to be provided with a nonce, in lieu of an initialization vector.
To understand what problem the nonce mitigates, think about what would happen if you were encrypting people's birthday. If you had two users with the same birthday and you encrypted those birthdays with the same key, then both users would have the same ciphertext for their birthdays. When this happens you can see who has the same birthday, even when you might not know exactly when it is. The initialization vector avoids this potential problem.
Our AES-256-GCM cipher suite supports the use of a 12 byte initialization vector, which we provide. The initialization vector ensures that even if you encrypt the same values with the same passphrase the resultant ciphertext still varies.
This mitigates the same problem as the Sodium nonce.
Our AES-256-GCM cipher suite supports the validation of a 16 byte authentication tag.
The "GCM" in AES-256-GCM stands for Galois/Counter Mode. The GCM is a Message Authentication Code (MAC) similar to a Hash-based Message Authentication Code (HMAC) which you may have heard of before. The goal of the GCM authentication tag is to make your encrypted data tamperproof.
The Sodium library also uses an authentication tag but it takes care of that by itself, it's not something we have to manage. When you parse_binary() in the Sodium module the tag is set to false.
This library requires secure random data inputs for various purposes:
There are two main options for generating suitable random data in PHP, those are:
Both are reasonable choices but this library uses random_bytes().
If the random_bytes() function is unable to generate secure random data it will throw an exception. See the documentation for details.
We also use the PHP random_int() function to generate a random delay for use in timing attack mitigation.
The round-trip use case is for when you want to send data to the client in hidden HTML form <input> elements and have it POSTed back later.
This use case is supported with two types of secret key.
The first key is called the current key and it is required.
The second key is called the previous key and it is optional.
Data is always encrypted with the current key.
Data is decrypted with the current key, and if that fails it is decrypted with the previous key. If decryption with the previous key also fails then the data cannot be decrypted, in that case the boolean value false will be returned to signal the error.
When you rotate your round-trip secret keys you copy the current key into the previous key, replacing the old previous key, and then you generate a new current key.
The config setting for the current key for the Sodium module is: CONFIG_SODIUM_SECRET_CURR .
The config setting for the current key for the OpenSSL module is: CONFIG_OPENSSL_SECRET_CURR .
The config setting for the previous key for the Sodium module is: CONFIG_SODIUM_SECRET_PREV .
The config setting for the previous key for the OpenSSL module is: CONFIG_OPENSSL_SECRET_PREV .
To encrypt round-trip data:
$ciphertext = kickass_round_trip()->encrypt( 'secret data' );
To decrypt round-trip data:
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
The at-rest use case if for when you want to encrypt data for storage in a database or elsewhere.
This use case is supported with an arbitrarily long list of secret keys.
The list must include at least one value. The first value in the list is used for encryption. For decryption each secret key in the list is tried until one is found that works. If none work the data cannot be decrypted and the boolean value false is returned to signal the error.
When you rotate your at-rest secret keys you add a new master key as the first item in the list. You need to keep at least one extra key, and you can keep as many in addition to that as suits your purposes.
After you rotate your at-rest secret keys you should consider re-encrypting all your existing at-rest data so that it is using the latest key. After you have re-encrypted your at-rest data, you can remove the older key.
The config setting for the key list for the Sodium module is: CONFIG_SODIUM_SECRET_LIST .
The config setting for the key list for the OpenSSL module is: CONFIG_OPENSSL_SECRET_LIST .
Please be aware: if you restore an old backup of your database, you will also need to restore your old keys.
Be very careful that you don't lose your at-rest secret keys. If you lose these keys you won't be able to decrypt your at-rest data.
To encrypt at-rest data:
$ciphertext = kickass_at_rest()->encrypt( 'secret data' );
To decrypt at-test data:
$plaintext = kickass_at_rest()->decrypt( $ciphertext );
It has been noted that key management is the hardest part of cybersecurity. This library can't help you with that.
Your encrypted data is only as secure as the secret keys.
If someone gets a copy of your secret keys, they will be able to decrypt your data.
If someone gets a copy of your encrypted data now, they can keep it and decrypt it if they get a copy of your secret keys in the future. So your keys don't have to be only secret now, but they have to be secret for all time.
If you lose your secret keys, you won't be able to decrypt your data.
Your round-trip data is probably less essential than your at-rest data.
It's a very good idea to make sure you have backups of the secret keys for your essential round-trip or at-rest data. You can consider:
When doing key management it is important to make sure your config files are edited in a secure way. A syntax error in a config file could lead to a secret key being exposed to the public web. If this happened you would have to rotate all of your keys immediately and then destroy the old compromised keys, even then it might be too late .
It would be a good idea to stand ready to do a key rotation in an automated and tested fashion immediately in case of emergency.
When you rotate your round-trip and at-rest keys you need to make sure they are synchronized across all of your web servers.
I intend to implement some facilities to help with key deployment and config file editing but those facilities are not done yet.
This library supports encrypted data at-rest, and encrypted data round-trips. Another consideration is data in motion. Data in motion is also sometimes called data in transit.
Data is in motion when it moves between your web servers and your database server. Data is also in motion when it moves between your web servers and the clients that access them. You should use asymmetric encryption for your data in motion. Use SSL encryption support when you connect to your database, and use HTTPS for your web clients.
This library is a server-side component. We don't support encrypting data client-side in web browsers.
This library collects some basic telemetry:
Call KickassCrypto::GetTelemetry() to get the telemetry and KickassCrypto::ReportTelemetry() to report it.
The unit tests are in the src/test/ directory, numbered sequentially.
There's some test runners in bin/dev/, as you can see. Read the scripts for the gory details but in brief:
There are also some silly tests, but we won't talk about those. They are not ordinarily run. And they're silly.
If you want to add a normal/fast test create the unit test directory as src/test/test-XXX , then add either fast.php or fast.sh . If you create both then fast.sh will have precedence and fast.php will be ignored.
If you want to add a slow test create the unit test directory as src/test/test-XXX , then add either slow.php or slow.sh . If you create both then slow.sh will have precedence and slow.php will be ignored.
You usually only need to supply a shell script if your unit tests require multiple processes to work. That can happen when you need to test different constant definitions. As you can't redefine constants in PHP you have to restart your process if you want to run with different values.
See existing unit tests for examples of how to use the simple unit test host.
I have heard of and used PHPUnit (although I haven't used it for a long while). I don't use it in this project because I don't feel I need it or that it adds much value. Tests are a shell script, if that's missing they're a PHP script. If I need to make assertions I call assert(). Facile.
Here are some notes about the various idioms and approaches taken in this library.
In the code you will see things like this:
protected final function is_valid_settings( int $setting_a, string $setting_b ) : bool {
if ( strlen( $setting_b ) > 20 ) { return false; }
return $this->do_is_valid_settings( $setting_a, $setting_b );
}
protected function do_is_valid_settings( $setting_a, $setting_b ) {
if ( $setting_a < 100 ) { return false; }
if ( strlen( $setting_b ) > 10 ) { return false; }
return 1;
}
There are several things to note about this idiom.
In talking about the above code we will call the first function is_valid_settings() the "final wrapper" (or sometimes the "main function') and we call the second function do_is_valid_settings() the "default implementation".
The first thing to note is that the final wrapper is_valid_settings() is declared final and thus cannot be overridden by implementations; and the second thing to note is that the final wrapper declares the data types on its interface.
In contrast the default implementation do_is_valid_settings() is not marked as final, and it does not declare the types on its interface.
This is an example of Postel's Law, which is also known as the Robustness Principle. The final wrapper is liberal in what it accepts, such as with the return value one ( 1 ) from the default implementation; and conservative in what it does, such as always returning a properly typed boolean value and always providing values of the correct type to the default implementation.
Not needing to write out and declare the types on the interface of the default implementation also makes implementation and debugging easier, as there's less code to write. (Also I find the syntax for return types a bit ugly and have a preference for avoiding it when possible, but that's a trivial matter.)
Ordinarily users of this code will only call the main function is_valid_settings() , and anyone implementing new code only needs to override do_is_valid_settings() .
In general you should always wrap any non-final methods (except for private ones) with a final method per this idiom, so that you can have callers override functionality as they may want to do but retain the ability to maintain standards as you may want to do.
If you're refactoring a private method to make it public or protected be sure to introduce the associated final wrapper.
One last thing: if your component has a public function, it should probably be a final wrapper and just defer to a default implementation.
Default implementations should pretty much always be protected, certainly not public, and maybe private if you're not ready to expose the implementation yet.
Having types on the interface of the final method is_valid_settings() confers three main advantages.
The first is that the interface is strongly typed, which means your callers can know what to expect and PHP can take care of fixing up some of the smaller details for us.
The second advantage of this approach is that our final wrapper function is marked as final. This means that the implementer can maintain particular standards within the library and be assured that those standards haven't been elided, accidentally or otherwise.
Having code that you rely on marked as final helps you to reason about the possible states of your component. In the example given above the requirement that $setting_b is less than or equal to 20 bytes in length is a requirement that cannot be changed by implementations; implementations can only make the requirements stronger, such as is done in the default implementation given in the example, where the maximum length is reduced further to 10 bytes.
Another advantage of the typed interface is that it provides extra information which can be automatically added into the documentation. The typed interface communicates intent to the PHP run-time but also to other programmers reading, using, or maintaining the code.
Not having types on the interface of the default implementation do_is_valid_settings() confers four main advantages.
The first is that it's easier to type out and maintain the overriding function as you don't need to worry about writing out the types.
Also, in future, the is_valid_settings() might declare a new interface and change its types. If this happens it can maintain support for both old and new do_is_valid_settings() implementations without implementers necessarily needing to update their code.
The third advantage of an untyped interface for the do_is_valid_settings() function is that it allows for the injection of "impossible" values. These are values which will never be able to make it past the types declared on the main function is_valid_settings() and into the do_is_valid_settings() function, and being able to inject such "impossible" values can make unit testing of particular situations easier, as you can pass in a value that could never possibly occur in production in order to signal something from the test in question.
The fourth and perhaps most important implication of the approach to the default implementation is that it is not marked as final which means that programmers inheriting from your class can provide a new implementation, thereby replacing, or augmenting, the default implementation.
One way a programmer can go wrong is to infinitely recurse. For example like this:
class InfiniteRecursion extends KickassCryptoOpenSslKickassOpenSslRoundTrip {
protected function do_encrypt( $input ) {
return $this->encrypt( $input );
}
}
If the do_encrypt() function calls the encrypt() function, the encrypt() function will call the do_encrypt() function, and then off we go to infinity.
If you do this and you have Xdebug installed and enabled that will limit the call depth to 256 by default. If you don't have Xdebug installed and enabled PHP will just start recurring and will continue to do so until it hits its memory limit or runs out of RAM.
Since there's pretty much nothing this library can do to stop programmers from accidentally writing code like the above what we do is to detect when it's probably happened by tracking how deep our calls are nested using an enter/leave discipline, like this:
try {
$this->enter( __FUNCTION__ );
// 2023-04-07 jj5 - do work...
return $result;
}
catch ( AssertionError $ex ) {
throw $ex;
}
catch ( Throwable $ex ) {
try {
$this->handle( $ex, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) {
try {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) { ; }
}
}
finally {
try { $this->leave( __FUNCTION__ ); } catch ( Throwable $ignore ) { ; }
}
The leave() function has no business throwing an exception, but we wrap it in a try-catch block just in case.
The example code above is shown with typical catch blocks included, but the key point is that the very first thing we do is register the function entry with the call to enter() and then in our finally block we register the function exit with the call to leave() .
If a function enters more than the number of times allowed by KICKASS_CRYPTO_RECURSION_LIMIT without leaving then an exception is thrown in order to break the recursion. At the time of writing KICKASS_CRYPTO_RECURSION_LIMIT is defined as 100, which is less than the Xdebug limit of 256, which means we should always be able to break our own recursive loops.
And for all the trouble we've gone to if the inheritor calls themselves and recurs directly there is nothing to be done:
class EpicFail extends KickassCryptoOpenSslKickassOpenSslRoundTrip {
protected function do_encrypt( $input ) {
return $this->do_encrypt( $input );
}
}
As mentioned above and elaborated on in the following section this library won't usually throw exceptions from the methods on its public interface because we don't want to leak secrets from our call stack if there's a problem.
Instead of throwing exceptions the methods on the classes in this library will usually return false instead, or some other invalid value such as null or [] .
The avoidance of exceptions is only a firm rule for sensitive function calls which handle secret keys, passphrases, unencrypted content, or any other sensitive data. At the time of writing it's possible for the public get_error_list() function to throw an exception if the implementer has returned an invalid value from do_get_error_list() , apart from in that specific and hopefully unlikely situation everything else should be exception safe and use the boolean value false (or another appropriate sentinel value) to communicate errors to the caller.
Sometimes because of the nature of a typed interface it's not possible to return the boolean value false and in some circumstances the empty string ( '' ), an empty array ( [] ), null ( null ), the floating-point value zero ( 0.0 ), or the integer zero ( 0 ) or minus one ( -1 ) may be returned instead; however, returning false is definitely preferred if it's possible.
Aside: in some cases minus one ( -1 ) can be used as the sentinel value to signal an error, such as when you want to indicate an invalid array index or an invalid count, but unlike in some other languages in PHP minus one isn't necessarily an invalid array index, and returning false is still preferred. This library does use minus one in some cases, if there's a problem with managing the telemetry counters.
The fact that an error has occurred can be registered with your component by a call to error() so that if the callers get a false return value they can interrogate your component with a call to get_error() or get_error_list() to get the recent errors (the caller can clear these errors with clear_error() too).
In our library the function for registering that an error has occurred is the error() function defined in the KickassCrypto class.
In some error situations the best and safest thing to do is swallow the error and return a sensible and safe and uncontroversial default value as a fallback.
Here's a quick run-down:
get_error_list() you get an exception with no errorget_error() you get null and an errorclear_error() it's void but with an errorhandle() you get a log entry, no errornotify() it will be handled then ignored, no errorignore() you get a log entry, no errorthrow() it will throw anywayerror() your error may not be properly registered, it always returns falsecount_*() counter you get -1 and no errorincrement_counter() you get -1 and no errorget_const_data_format() you get an empty string and no errorget_const_*() constant accessor you get the value defined by the default constant and no errorget_config_*() config accessor you get the value defined by the default constant (or false if there is no such thing) and no errorget_const() you get the default value and no errorget_passphrase_list() you get an empty array and an errorget_encryption_passphrase() you get null and no erroris_*() method you will get false and no errorget_data_encoding() you will get an empty string and no errorget_data_format() you will get false and no errorconvert_secret_to_passphrase() you will get false and no errorget_padding() you will get false and no errorget_delay() you will get false and no error (an emergency delay will be injected)delay() you will get void and no error (an emergency delay will be injected)log_error() you will get false and no error (but we try to be forgiving)This library is very particular about exception handling and error reporting.
If you have sensitive data on your call stack you must not throw exceptions. Sensitive data includes:
If you encounter a situation from which you cannot continue processing of the typical and expected program logic the way to register this problem is by calling the error() function with a string identifying and describing the problem and then returning false to indicate failure.
As the error() function always returns the boolean value false you can usually register the error and return false on the same like, like this:
return $this->error( __FUNCTION__, 'something bad happened.' );
When I nominate error strings I usually start them with a lowercase letter and end them with a period.
Note that it's okay to intercept and rethrow PHP AssertionError exceptions. These should only ever occur during development and not in production. If you're calling code you don't trust you might not wish to rethrow AssertionError exceptions, but if you're calling code you don't trust you've probably got bigger problems in life.
If you have a strong opinion regarding AssertionError exceptions and think I should not rethrow them I would be happy to hear from you to understand your concern and potentially address the issue.
Following is some example code showing how to handle exceptions and manage errors.
protected final function do_work_with_secret( $secret ) {
try {
$result = str_repeat( $secret, 2 );
$this->call_some_function_you_might_not_control( $result );
return $result;
}
catch ( AssertionError $ex ) {
throw $ex;
}
catch ( Throwable $ex ) {
try {
$this->handle( $ex, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) {
try {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) { ; }
}
}
try {
return $this->error( __FUNCTION__, 'error working with string.' );
}
catch ( Throwable $ignore ) {
try {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) { ; }
}
return false;
}
In actual code you would define an error constant for use instead of the string literal 'error working with string.' . In this library the names of error constants begin with "KICKASS_CRYPTO_ERROR_" and they are defined in the src/code/global/constant/framework.php file.
Note that we don't even assume it's safe to call handle() , ignore() , or error() ; we wrap all such calls in try-catch handlers too. There are some edge case situations where even these functions which are supposed to be thread safe can lead to exceptions, such as when there's infinite recursion which gets aborted by the run-time. If you're an expert on such matters the code might do with a review from you.
Now I will agree that the above code is kind of insane, it's just that it seems to me like there's no avoiding it if we want to be safe. We have to explicitly allow the AssertionError exception every single time in every single method just so that assertions remain useful to us as a development tool, and then when we handle other exceptions we want to make some noise about them so we call handle() , but the thing is that handle() will defer to do_handle() which can be overridden by implementers, which means it can throw... so if handle() throws we don't want to just do nothing, we want to give the programmer a last chance to learn of their errant code, so we notify that we're going to ignore the exception with a call to ignore() , but that will defer to do_ignore() , which the programmer could override, and throw from... but if that happens we will just silently ignore such a problem.
And then if we get through all of that and our function hasn't returned then that's an error situation so we want to notify the error, but error() defers to do_error() and that could be overridden and throw, so we wrap in a try-catch block and then do the exception ignore dance again.
I mean it's all over the top and excessive but it should at least be safe and it meets two requirements:
In the usual happy code path none of the exception handling code even runs.
There are a bunch of functions for testing boolean conditions, and they begin with "is_" and return a boolean. These functions should only do the test and return true or false, they should not register errors using the error() function, if that's necessary the caller will do that.
The is_() functions can be implemented using the typed final wrapper idiom documented above.
Following is a good example from the code.
protected final function is_valid_secret( $secret ) : bool {
try {
$is_valid = $this->do_is_valid_secret( $secret );
// ...
assert( is_bool( $is_valid ) );
return $is_valid;
}
catch ( AssertionError $ex ) {
throw $ex;
}
catch ( Throwable $ex ) {
try {
$this->handle( $ex, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
}
return false;
}
Note that do_is_valid_secret() also has a secret on the call stack, so it should be implemented as exception safe in the same way (in case it is called directly from some other part of the code).
Note too that it's okay to just rethrow assertion violations, these should never happen in production and they make testing the code easier.
The approach to unit-testing taken by this library is simple and powerful. There are three types of test which can be defined for each unit test:
Each script will be either a shell script with the same name, eg fast.sh , or if that's missing a PHP script with the same name, eg fast.php . The test runner just finds these scripts and runs them. This is easy to do and provides all the power we need to run our tests, including support for the various situations where each test instance needs to run in its own process and be isolated from other testing environments.
If you have flakey and unreliable tests you can stick them in as silly tests. The fast and slow tests are the important ones, and you shouldn't put slow tests in the fast test scripts. The fast tests are for day to day programming and testing and the slow scripts are for running prior to a version release.
Here are some notes regarding notable components:
config.php file for the demoSome countries have banned the import or use of strong cryptography, such as 256 bit AES.
Please be advised that this library does not contain cryptographic functions, they are provided by your PHP implementation.
Copyright (c) 2023 John Elliot V.
This code is licensed under the MIT License.
See the contributors file.
I should probably be more disciplined with my commit messages... if this library matures and gets widely used I will try to be more careful with my commits.
The Kickass Crypto ASCII banner is in the Graffiti font courtesy of TAAG.
The string "kickass" appears in the source code 1,506 times (including the ASCII banners).
SLOC and file count reports generated using David A. Wheeler's 'SLOCCount'.
J'adorerais avoir de vos nouvelles! Hit me up at [email protected]. Put "Kickass Crypto" in the subject line to make it past my mail filters.