Una biblioteca contemporánea de criptografía PHP alrededor de 2023.
Sinopsis:
Esta biblioteca es un envoltorio alrededor de la biblioteca de sodio PHP y la biblioteca PHP OpenSSL.
El código de sodio en esta biblioteca se basa en el ejemplo dado en la documentación para la función PHP Sodium_Crypto_SecretBox ().
El código OpenSSL en esta biblioteca se basa en el ejemplo dado en la documentación para la función PHP OpenSSL_ENCRYPT ().
Esta biblioteca tiene como objetivo garantizar que los datos que cifre sean tan seguros como las claves secretas utilizadas para cifrarlo. También se toman medidas en un intento de garantizar que los datos cifrados sean a prueba de manipulaciones.
Esta biblioteca no puede resolver el problema difícil de la gestión de claves.

Esta biblioteca es un trabajo en progreso.
Estoy compartiendo este código con amigos y colegas, solicitando la mayor cantidad de críticas y comentarios como sea posible. Cuando siento que esta biblioteca es tan buena como puedo hacerlo, actualizaré esta nota de estado. Mientras tanto, los cambios de ruptura son casi ciertos y las debilidades de cifrado son bastante posibles. Si encuentra algo que cree que debería saber, ¡hágamelo saber!
Quiero ser claro contigo que esta biblioteca es grande y compleja y no ha visto mucho uso; Sin duda es cargado de errores sutiles que aún no se han descubierto. Creo que esta base de código tiene el potencial de madurar en una herramienta sólida y confiable, pero necesitamos pasar por el proceso.
Lea esta sección.
Hay muchas maneras en que puedes equivocarte con tu código criptográfico. Esta biblioteca fue escrita como un intento de reducir las pistolas criptográficas; ¡Ojalá no haya introducido ninguno!
Lo primero que debe saber sobre Crypto es que sus datos son tan seguros como sus claves. Hay más que saber sobre la gestión clave de lo que puedo decirte aquí (y de todos modos no soy un experto), pero aquí hay algunas cosas en las que pensar:
Algunas otras cosas a tener en cuenta:
get_error() después del cifrado para asegurarse de que sea nulo que indique un error).Otra cosa, que me sorprendió cuando lo aprendí, aunque es bastante obvio una vez que lo sabe, es que no debe comprimir sus datos antes de cifrarlos. Esto no siempre es un problema, pero en ciertas circunstancias puede ser, por lo que probablemente sea mejor nunca hacerlo.
El problema con la compresión es que si un atacante puede controlar algunos de los datos de entrada, puede incluir un valor particular y luego si la salida disminuye de tamaño, puede saber que la otra entrada también se incluye en el valor particular. Ay.
Esta base de código no es madura o bien probada, antes de usarla, debe leer todo el código para asegurarse de que cumpla con sus estándares de calidad. Si lo haces, me complacería saber de ti.
Si puede pensar en cualquier otra cosa que todos deben conocer y tener cuidado, ¡hágamelo saber!
¿No quiero RTFM ..? Y aquí estoy, escribiendo todas estas cosas ... Sheesh. Al menos lea las advertencias enumeradas anteriormente.
#!/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
Para obtener un poco más de elaboración, tal vez consulte el código de muestra.
O si desea el resultado final sobre cómo funciona esta biblioteca, lea el código en el marco de la biblioteca o en el otro código.
Caramba, comenzó lo suficiente, pero al final se complicó un poco.
Quería viajar redondo algunos datos relativamente confidenciales (números de versión de fila para un control de concurrencia optimista) entre mi servidor y sus clientes de manera relativamente segura, secreto y resistencia a la prueba.
Había oído que la biblioteca OpenSSL estaba disponible en PHP, así que busqué información sobre cómo usarla. Encontré el código de ejemplo en la documentación PHP para la función OpenSSL_ENCRYPT ().
Inicialmente no estaba claro para mí cómo usar este código. Particularmente fue difícil descubrir qué hacer con las tres partes: la etiqueta de autenticación, el vector de inicialización y el texto de cifrado. Finalmente descubrí que podría concatenarlos. Pero si tuviera que hacer eso, necesitaría estandarizar su longitud y colocación para poder recuperarlos más tarde ...
... y luego pensé que sería mejor enmascarar mi tamaño de datos real al acolcharlo a longitudes fijas en ciertos límites, así que lo hice ...
... y luego quería apoyar datos ricos que exigían alguna forma de serialización. Inicialmente estaba usando la función PHP Serialize () pero eso se cambió más tarde a JSON_ENCODE ().
El código de ejemplo no indicó nada sobre cómo rotar las llaves de una manera compatible. Así que se me ocurrió los dos casos de uso compatibles con esta biblioteca con diferentes enfoques para la gestión clave para escenarios de viaje de ida y vuelta y en el inicio. Esta biblioteca le permite girar en teclas nuevas mientras mantiene soporte para claves más antiguas, como es probable que no haga.
Luego capas en un enfoque cuidadoso para el manejo de excepciones e informes de errores, algunas pruebas y validación de unidades, mitigación de ataque de tiempo, localizadores de servicios, demostración de uso, límites de tamaño de datos, inicialización de frase de pases, scripts de generación de claves, telemetría y cosas así.
Básicamente, toda esta biblioteca era todo lo que sentía que tenía que hacer para poder usar la implementación de la biblioteca PHP OpenSSL incorporada.
Y luego ... la gente comenzó a contarme sobre la biblioteca de sodio y sugerir que lo use. Dado que ya había hecho un montón de trabajo para la gestión de clave y la serialización de entrada y el formato de mensajes y la codificación, etc., pensé que podría reutilizar todo eso y proporcionar un envoltorio alrededor del sodio también. Entonces eso es lo que hice.
Ahora, si usa esta biblioteca, puede decidir si desea usar la implementación de sodio o la implementación de OpenSSL. Debido a que las dos implementaciones pueden coexistir felizmente, también puede escribir código para moverse de uno a otro, si así lo desea. Las implementaciones nunca comparten la configuración clave o los formatos de datos, están completamente separadas. (Dicho esto, no es exactamente trivial cambiar los algoritmos de cifrado y probablemente tenga que desconectarse para migrar todos sus datos y, si no puede hacerlo, pasará mal, así que no planee cambiar los algoritmos, si no está seguro de comenzar con sodio y seguir con él).
No considero que esta biblioteca rodea mi propia criptografía , sino que pienso en que descubre cómo usar sodio y openssl . Si he cometido algún error, obvio o no, realmente agradecería saberlo.
Suponiendo que recuerdo actualizarlo de vez en cuando, hay un sistema de demostración aquí:
La instalación de demostración solo muestra cómo viajar de ida y vuelta datos encriptados entre el cliente y el servidor utilizando HTML y HTTP.
El código de demostración está disponible en esta biblioteca en el directorio SRC/ demo/ si desea alojarlo usted mismo.
Suponiendo que recuerdo actualizarlos de vez en cuando, los documentos de PHP están aquí:
Como se mencionó anteriormente, puede consultar el código de Git con un comando como este:
git clone https://github.com/jj5/kickass-crypto.git
Este código está inédito, no hay una versión estable.
Si desea incluir la biblioteca del cliente para su uso en su aplicación, incluya Inc/Sodium.php o el archivo Inc/openssl.php que se encargará de cargar todo lo demás; Usa algo como esto:
require_once __DIR__ . '/lib/kickass-crypto/inc/sodium.php';
Después de cargar esta biblioteca, generalmente accederá a través de los localizadores de servicio kickass_round_trip() o kickass_at_rest() que se documentan a continuación, algo así:
$ciphertext = kickass_round_trip()->encrypt( 'secret text' );
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
echo "the secret data is: $plaintext.n";
¡Tomó mucho trabajo hacer cosas tan simples!
Si desea alojar el código de demostración, necesita alojar los archivos en SRC/ demo/ e incluir un archivo config.php válido en el directorio base del proyecto (ese es el directorio que incluye este archivo readme.md). Para fines de demostración, un archivo config.php válido solo necesita definir una cadena constante para CONFIG_SODIUM_SECRET_CURR , pero debe ser una cadena larga y aleatoria, puede generar una cadena apropiada con:
php bin/gen-key.php
O simplemente puede generar un archivo config.php demostración completo con:
php bin/gen-demo-config.php > config.php
Aquí hay algunas notas sobre los archivos de software y las líneas de código.
Total Number of Files = 128
Total Number of Source Code Files = 128
| Directorio | Archivos | Por idioma |
|---|---|---|
| prueba | 63 | php = 59, sh = 4 |
| código | 35 | Php = 35 |
| papelera | 22 | php = 13, sh = 9 |
| Cª | 7 | Php = 7 |
| manifestación | 1 | Php = 1 |
| Idioma | Archivos | Porcentaje |
|---|---|---|
| php | 115 | (89.84%) |
| mierda | 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).
| Directorio | Chapoteo | Por idioma |
|---|---|---|
| código | 5,136 | Php = 5136 |
| prueba | 3.363 | php = 3193, sh = 170 |
| papelera | 603 | php = 423, sh = 180 |
| manifestación | 71 | Php = 71 |
| Cª | 37 | Php = 37 |
| Idioma | Chapoteo | Porcentaje |
|---|---|---|
| php | 8.860 | (96.20%) |
| mierda | 350 | (3.80%) |
Este código debería funcionar en PHP 7.4 o más. Si intenta ejecutar este código en una versión anterior de PHP, intentará registrar un mensaje de error y luego salir de su proceso.
Este código verificará para asegurarse de que se esté ejecutando en una plataforma de 64 bits. Si no es así, se quejará y saldrá.
Si carga el módulo de sodio, la biblioteca se asegurará de que la biblioteca de sodio esté realmente disponible. Si no es así, el proceso se quejará y saldrá.
Si carga el módulo OpenSSL, la biblioteca asegurará que la biblioteca OpenSSL esté realmente disponible. Si no es así, el proceso se quejará y saldrá.
Creo que este código debería ejecutarse en cualquier sistema operativo, pero solo lo he probado en Linux. Si ha tenido éxito en MacOS o Windows, estaría feliz de saberlo.
Los guiones de shell están escritos para Bash. Si no tiene Bash, es posible que necesite portar.
Este código admite dos casos de uso específicos:
Las claves se administran por separado y de manera diferente para cada caso de uso.
Los detalles de cómo se admite cada caso de uso se documentan a continuación.
El uso de esta biblioteca para el cifrado en Rest es generalmente un riesgo mayor y un compromiso mayor que usarla simplemente para el cifrado de ida y vuelta. Si pierde sus claves de cifrado de ida y vuelta o se ve obligado a rotarlas con urgencia, es probable que sea un problema menor que si algo similar sucediera con sus claves en el inicio.
El caso de uso principal para el cual se desarrolló esta biblioteca fue admitir un viaje redondo de unos pocos kilobytes de datos que contienen números de versión de fila ligeramente sensible pero no de misión crítica para un control de concurrencia optimista. En comparación con la alternativa (no encriptar o resistir a los datos optimistas de control de concurrencia) El uso de esta biblioteca es una mejora. Si es realmente adecuado en otras aplicaciones es una pregunta abierta, no estoy seguro. Ciertamente, no debe usar esta biblioteca si no proporciona el nivel de seguridad que necesita.
La forma preferida y compatible de nominar secretos en los archivos de configuración es como constantes utilizando la función PHP Define (). El problema con el uso de campos de clase/instancia o variables globales es que los valores pueden filtrarse fácilmente en el código de depuración y registro, esto es menos probable (aunque aún posible) para las constantes. De manera similar, si necesita almacenar en caché los datos globales/estáticos (como leer desde el archivo de configuración), la mejor manera de hacerlo es con una variable estática local en una función, si es posible, como utilizando campos de instancia, campos de clase o globales puede conducir más fácilmente a una fuga secreta.
Para darle un ejemplo, creemos un archivo de prueba llamado double-define.php como este:
<?php
define( 'TEST', 123 );
define( 'TEST', 456 );
Luego, cuando ejecutamos el código, sucede algo como esto:
$ 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 ese valor constante contenía su clave secreta, entonces ha tenido un día muy malo.
La forma más segura de definir una constante en PHP es verificar que ya no esté definida primero, porque intentar definir una constante ya definida dará como resultado un error. Si encuentra una constante ya definida, puede abortar con un mensaje de error (si no proporciona demasiados detalles porque la web pública podría verlo) o simplemente mantener el valor existente y no intente redefinirlo. El generador de archivos de configuración bin/gen-demo-config.php adopta el primer enfoque y llama a la función PHP die() si se detecta un duplicado. Puede ver qué sucede al incluir el archivo config.php generado dos veces, como:
require __DIR__ . '/config.php';
require __DIR__ . '/config.php';
Puede encontrar un ejemplo de lo que sucede si incluye Double el config.php en config-diie.php.
En consecuencia, como con la mayoría de los archivos de origen PHP, es mejor usar require_once al incluir el archivo config.php :
require_once __DIR__ . '/config.php';
Cuando nombro cosas que son secretas, me aseguro de que el nombre contenga la cadena "pase" (como en "contraseña", "passwd" y "frase de pases", o incluso, en la recta, "pasaporte") o "secreto". En mis instalaciones de registro de propósito general (que no están incluidos en esta biblioteca), frito y redacto cualquier cosa con un nombre que coincida (pertenezca a la caja) antes de registrar los datos de diagnóstico. Te animo a que adopte esta práctica.
En esta biblioteca, si una variable o constante puede contener datos confidenciales, se nombrará con "pasar" o "secreto" como subcadena en el nombre.
No escriba datos confidenciales en registros.
Ponga 'pase' o 'secreto' en nombre de variables, campos o constantes sensibles.
Aquí explico lo que realmente significan estos términos de sonido similares en el contexto de esta biblioteca.
Si usa los módulos predeterminados, el formato de datos es "KA0" para el módulo OpenSSL o "KAS0" para el módulo de sodio.
Si hereda el marco base y define su propio módulo criptográfico, el formato de datos predeterminado es "XKA0" para un módulo basado en la implementación de OpenSSL o "XKAS0" para un módulo basado en la implementación de sodio, de lo contrario, su implementación de do_get_const_data_format() implementa el formato de datos que se conocirá como puede hacer que sea algo que no se inicie, ya que no se inicie, lo que no se inicia, lo que se reserva que se reserva.
Debe usar el módulo correcto para el formato de datos para descifrar con éxito un texto cifrado.
La codificación de datos es JSON, serialización de PHP o texto. Suponiendo que tiene el módulo correcto para el formato de datos (arriba), y con una advertencia que se discute a continuación, puede descifrar cualquier cosa independientemente de los datos que codifiquen. El cifrado se realizará utilizando la codificación de datos configurados, consulte config_encryption_data_encoding, puede ser uno de:
Tenga en cuenta que no podrá usar la codificación de PHPS a menos que también define config_encryption_phps_enable, esto se debe a que la deserialización de PHP podría ser insegura, por lo que está desactivado de forma predeterminada. Honestamente, esto es un poco de mano. Acabo de escuchar rumores de que PHP unserialize() puede conducir a la inyección de código, pero no estoy seguro de si eso es cierto o qué significa exactamente. Implementé la serialización y la deserialización de PHP y le di un poco de prueba, pero no sé si es realmente inseguro o no. Estoy bastante seguro de que la codificación de datos JSON y de texto debería estar seguro.
Además de heredar de KickassCrypto y anular una funcionalidad particular, una gran cantidad de configuración está disponible a través de las constantes de configuración. Busque CONFIG_SODIUM para encontrar lo que está disponible para sodium y CONFIG_OPENSSL para encontrar lo que está disponible para OpenSSL.
Tenga en cuenta que en este momento este código está configurado directamente en el archivo config.php .
En el futuro, el archivo config.php incluirá archivos de configuración administrados por separado, siendo::
Habrá scripts de administración para girar automáticamente y aprovisionar claves en estos archivos.
Los usuarios experimentados de Linux saben que no editan /etc/sudoers directamente, lo edita con visudo para que pueda verificar que no haya introducido accidentalmente un error de sintaxis y haya sacudido su sistema.
Tengo la intención de proporcionar scripts similares para editar y administrar config.php y otros archivos de configuración. Entonces, para esas actualizaciones. Mientras tanto ... solo ten cuidado .
Una cosa que debe tener mucho cuidado que no haga es administrar sus claves en otra cosa que no sea un archivo PHP con una extensión del archivo ".php". Si coloca sus claves en un archivo ".ini" o algo así , su servidor web podría ser atendido como texto sin formato . Así que no hagas eso. También tenga cuidado de no introducir errores de sintaxis en su archivo de configuración u otros archivos de origen que se ejecutan en producción porque los detalles pueden filtrarse con los posibles mensajes de error resultantes.
Como se mencionó en la sección anterior, una buena cantidad de configurabilidad es proporcionada por soporte para constantes de configuración con nombre.
Además de las constantes de configuración, hay muchas cosas que puede hacer si heredas de la clase base KickassCrypto y anula sus métodos.
Como alternativa a las constantes de configuración (que solo se pueden definir una vez por proceso y a partir de entonces no se puede cambiar) Hay métodos de instancia como get_config_...() para opciones de configuración y get_const_...() para una evaluación constante. Las constantes y opciones de configuración más importantes se leen indirectamente a través de estos accesorios, por lo que debe poder anularlos de manera confiable.
La mayoría de las llamadas a las funciones incorporadas de PHP son realizadas por envoltorios delgados a través de funciones protegidas en KickassCrypto . Estos se definen en el rasgo KICKASS_WRAPPER_PHP . Esta indirección permite que ciertas invocaciones de la función PHP se intercepten y se modifiquen potencialmente. Esto se ha hecho principalmente para soportar la inyección de fallas durante las pruebas unitarias, pero podría usar para otros fines para cambiar los detalles de implementación.
Las cosas que se consideran sensibles en KickassCrypto se definen como privadas o finales . Si no es privado y no es final, es un juego justo para anular (a menos que haya cometido un error). Particularmente los métodos de instancia que comienzan con do_ fueron hechos específicamente para ser reemplazados o interceptados por los implementadores.
Esta biblioteca proporciona dos funciones de localización de servicios que administran una instancia de la biblioteca criptográfica cada una, esas son:
kickass_round_trip()kickass_at_rest()Puede reemplazar la instancia del servicio proporcionado por la función del localización del servicio llamando a la función y pasando la nueva instancia como el único parámetro, como este:
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 );
Idealmente, esta biblioteca cumplirá con sus requisitos fuera de la caja (o con cierta configuración) y no necesitará reemplazar las instancias proporcionadas por los localizadores de servicios de forma predeterminada.
Un localizador de servicios creará una nueva instancia predeterminada para usted en la primera llamada al localizador de servicios si aún no tiene una instancia. Si la implementación predeterminada es el módulo de sodio o el módulo OpenSSL depende del orden en que incluye los archivos inc/sodium.php e inc/openssl.php ; Si incluyó toda la biblioteca con inc/library.php el módulo de sodio tendrá precedencia.
Independientemente de si cargó los localizadores de servicios para el módulo de sodio o el módulo OpenSSL, podrá anular la instancia predeterminada llamando al localizador de servicio con una nueva instancia como argumento.
El proceso de cifrado es más o menos:
Tenga en cuenta que la biblioteca de sodio usa un nonce en lugar de un vector de inicialización (con un efecto similar) y el sodio maneja su propia etiqueta de autenticación.
Cuando esta biblioteca codifica su texto cifrado, incluye un prefijo de formato de datos de "Kas0/" para la implementación de sodio y "KA0/" para la implementación de OpenSSL.
El cero ("0") en el prefijo de formato de datos es para la versión cero , que está destinada a implicar que la interfaz es inestable y puede cambiar .
Las versiones futuras de esta biblioteca pueden implementar un nuevo prefijo de formato de datos para un formato de datos estable.
Cuando esta biblioteca decodifica su texto cifrado, verifica el prefijo de formato de datos. En la actualidad, solo es compatible con "Kas0/" o "Ka0/".
El formato de datos de la versión cero, mencionado anteriormente, actualmente implica lo siguiente:
Después de la codificación de datos (JSON de forma predeterminada, se discute en la siguiente sección) se realiza el relleno y la longitud de los datos tiene prefijo. Antes del cifrado, el mensaje está formateado, así:
$message = $encoded_data_length . '|json|' . $encoded_data . $this->get_padding( $pad_length );
La longitud de datos JSON está formateada como un valor hexadecimal de 8 caracteres. El tamaño de 8 caracteres es constante y no varía según la magnitud de la longitud de los datos JSON.
La razón del relleno es oscurecer el tamaño real de los datos. El relleno se realiza en hasta 4 límites de KIB (2 12 bytes), que llamamos trozos. El tamaño del fragmento es configurable y el valor predeterminado puede cambiar en el futuro.
Luego, si estamos encriptando con sodio, el mensaje está encriptado con sodium_crypto_secretbox() y luego Nonce y el texto cifrado se concatenan juntos, así:
$nonce . $ciphertext
De lo contrario, si estamos encriptando con OpenSSL, el mensaje está encriptado con AES-256-GCM y el vector de inicialización, el texto cifrado y la etiqueta de autenticación se concatenan juntos, así:
$iv . $ciphertext . $tag
Entonces todo está codificado con la función PHP Base64_encode () y se agrega el prefijo de formato de datos.
Para el sodio que se hace así:
"KAS0/" . base64_encode( $nonce . $ciphertext )
Y para OpenSSL que se hace así:
"KA0/" . base64_encode( $iv . $ciphertext . $tag )
El proceso de descifrado espera encontrar el Nonce de 24 bytes y el texto cifrado para el formato de datos "KAS0" y el vector de inicialización de 12 bytes, el texto cifrado y la etiqueta de autenticación de 16 bytes para el formato de datos KA0.
Después de descifrar el texto cifrado, la biblioteca espera encontrar el tamaño de los datos JSON como una cadena ASCII que representa un valor codificado por Hex Hex de 8 caracteres, seguido de un solo carácter de tubería, seguido de un indicador de codificación de datos de cuatro caracteres ('JSON' o 'PHPS'), seguido de un solo carácter de tubería, seguido por el JSON (o datos de serializados PHP), y luego el coldí. La biblioteca puede extraer los datos JSON/serializados de su relleno y cuidar el resto de la decodificación.
Antes de que los datos de entrada de cifrado se codifiquen como JSON utilizando la función PHP JSON_ENCODE (). Inicialmente, esta biblioteca utilizó la función PHP Serialize (), pero aparentemente eso puede conducir a algunos escenarios de ejecución de código (no estoy seguro de los detalles), por lo que se decidió que la codificación JSON era más segura. Por lo tanto, ahora, usamos la codificación JSON en su lugar.
El uso de JSON como formato de codificación de datos tiene algunas implicaciones menores con respecto a los valores que podemos admitir. En particular, no podemos codificar instancias de objetos que luego puedan decodificarse de nuevo a las instancias de objetos (si los objetos implementan la interfaz JSonSerializable, pueden ser serializadas como datos, pero esos solo se decodificarán a las matrices PHP, no los objetos PHP de los que vinieron); Algunos valores de punto flotante impares no se pueden representar (es decir, nan, pos inf, neg e info y neg cero); y las cuerdas binarias no se pueden representar en JSON.
Por defecto, estas opciones se utilizan para la codificación JSON:
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
Pero estas opciones no afectarán la capacidad de una implementación para decodificar el JSON. Las implementaciones pueden ajustar la codificación y decodificación de JSON si es necesario anular los métodos data_encode () y data_decode (). Alternativamente, puede nominar las opciones de codificación y decodificación de JSON en su archivo config.php con las constantes CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS y CONFIG_ENCRYPTION_JSON_DECODE_OPTIONS , por ejemplo:
define( 'CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
define( 'CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS', JSON_THROW_ON_ERROR );
Esta biblioteca debería funcionar independientemente de si JSON_THROW_ON_ERROR se especifica o no.
Si especifica JSON_PARTIAL_OUTPUT_ON_ERROR en sus opciones de codificación JSON, sus datos pueden volverse inválidos en silencio, así que hágalo bajo su propio riesgo. Quizás contraintuitivamente he descubierto que habilitar JSON_PARTIAL_OUTPUT_ON_ERROR es la mejor estrategia porque al menos en ese caso obtienes algo . Si no habilita JSON_PARTIAL_OUTPUT_ON_ERROR si no se puede codificar ninguna parte de su entrada (como cuando tiene cadenas binarias que no están en una codificación válida como UTF-8), entonces se elimina todos los datos. Con JSON_PARTIAL_OUTPUT_ON_ERROR solo se omite la porción no representable. Por el momento, JSON_PARTIAL_OUTPUT_ON_ERROR no se especifica automáticamente, pero esto es algo que podría volver a visitar en el futuro.
Si usa alguna de estas opciones de codificación/decodificación de JSON, podría terminar pasando un mal momento:
JSON_NUMERIC_CHECKJSON_INVALID_UTF8_IGNOREJSON_INVALID_UTF8_SUBSTITUTE Cuando esta biblioteca cifra sus datos, se acumula su salida a un tamaño de fragmento configurable.
La constante de configuración para el tamaño de la fragmentación es CONFIG_ENCRYPTION_CHUNK_SIZE .
El tamaño de fragmento predeterminado es 4,096 (2 12 ).
Si quisiera aumentar el tamaño de la fragmentación a 8,192, podría hacerlo en su archivo config.php como este:
define( 'CONFIG_ENCRYPTION_CHUNK_SIZE', 8912 );
Puede cambiar el tamaño de la fragmentación definida y comenzará a aplicarse a nuevos datos, y los datos antiguos encriptados con un tamaño de fragmento diferente aún podrán descifrar.
Mientras se observen los límites de tamaño de datos (estos se discuten a continuación), esta biblioteca puede cifrar cualquier cosa que pueda ser codificada como JSON por PHP.
Esto incluye una variedad de cosas, como:
Cosas que no pueden ser apoyadas con JSON:
Tenga en cuenta que el valor booleano False no se puede encriptar. No es porque no pudiéramos cifrarlo, es porque lo devolvemos cuando falla el descifrado. Por lo tanto, nos negamos a cifrar falso para que no se pueda confundir con un error al descifrarse.
Si necesita cifrar el valor booleano falso, considere ponerlo en una matriz, así:
$input = [ 'value' => false ];
O codificando como JSON, como este:
$input = json_encode( false );
Si hace alguna de esas cosas, podrá cifrar su valor.
Vale la pena señalar que en PHP las "cadenas" son esencialmente matrices de bytes, lo que significa que pueden contener datos esencialmente "binarios". Sin embargo, dichos datos binarios no pueden representarse como JSON. Si necesita manejar los datos binarios, la mejor manera es probablemente codificarlo como base64 con base64_encode () o hexadecimal con bin2hex () y luego cifre eso.
En el futuro, la capacidad de trabajar con datos que no siempre se codifica JSON podría agregarse a esta biblioteca. Avísame si esa es una característica que te importa.
Nota: El uso de la serialización de PHP en lugar de la codificación JSON ahora es una opción; Esta documentación debe actualizarse para explicar cómo funciona y cómo usarla. La ventaja de la serialización de PHP es que admite más tipos de datos y formatos que JSON.
Después de que los datos se codifiquen como JSON, se limita a una longitud máxima configurable.
La constante de configuración para la longitud máxima de codificación de JSON es CONFIG_ENCRYPTION_DATA_LENGTH_MAX .
El límite de codificación de datos predeterminado es 67,108,864 (2^ 26 ) bytes, que es aproximadamente 67 MB o 64 MIB.
Es posible configurar este límite de codificación de datos, si necesita hacerlo más grande o más pequeño. Solo tenga en cuenta que si hace el límite demasiado grande, terminará con problemas de memoria y su proceso podría finalizar.
Si desea disminuir el límite de codificación de datos, podría hacerlo en su archivo config.php como este:
define( 'CONFIG_ENCRYPTION_DATA_LENGTH_MAX', pow( 2, 25 ) );
Esta biblioteca no comprime los datos de entrada, porque la compresión puede introducir debilidades criptográficas, como en el ataque SSL/TLS del crimen.
El problema es que si el atacante puede modificar parte del texto plano, puede averiguar si los datos que ingresan existe en otras partes del texto plano, porque si ponen un valor y el resultado es más pequeño, porque existe en la parte del texto sin formato que no sabían, ¡pero ahora!
Es muy importante que no comprime los datos que un atacante puede suministrar con otros datos secretos. Es mejor no comprimir en absoluto.
Si se encuentra un error durante el cifrado o descifrado, se introduce un retraso de entre 1 milisegundo (1 ms) y 10 segundos (10 s). Esta es una mitigación contra posibles ataques de tiempo. Ver S2N y Lucky 13 para discusión.
Tenga en cuenta que evitar ataques de tiempo es difícil. 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(). Fácil.
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'.
¡Me encantaría saber de ti! Hit me up at [email protected]. Put "Kickass Crypto" in the subject line to make it past my mail filters.