C++17: Diseñando un verificador variádico de contraseñas

Introducción


El objetivo de este post es el de diseñar un verificador de contraseñas que permita combinar distintas políticas de validación para las claves. Como caso base, nuestro verificador comprobará que la longitud de una contraseña pertenezca a un rango permitido. En situaciones de mayor interés, podremos también incluir reglas de validación adicionales que comprueben la presencia de letras minúsculas y mayúsculas, dígitos y/o símbolos no-alfanuméricos. Para ello, haremos uso de expresiones regulares (regex) y plantillas variádicas (variadic templates [1]) introducidas en el estándar C++11, así como de los denominados fold expressions proporcionados por C++17 [2]. Emplearemos, asimismo, la biblioteca {fmt}lib --en proceso de estandarización para C++20-- con el fin de facilitar el formato de texto [3].

Deseamos que el programador pueda generar verificadores con funcionalidad variable. Entre algunos de los casos a contemplar, podemos señalar:

  • Comprobación simple de longitud (en el ejemplo, entre 8 y 16 caracteres):
auto const check = Password_validation{8, 16}; cout << check("edmonddantes"); // output: 1 (true)

  • Comprobación de longitud y existencia de, al menos, un dígito (número entre 0 y 9):
auto const check = Password_validation<Digit>{8, 16}; cout << check("edmonddantes34"); // output: 1 (true)

  • Comprobación de longitud y presencia de, al menos, un dígito, una letra en minúscula y otra en mayúscula:
auto const check = Password_validation<Digit, Lower_upper_case>{8, 16}; cout << check("EdmondDantes34"); // output: 1 (true)


Expresiones regulares


Como podemos observar, la funcionalidad común a todos los ejemplos anteriores reside en el verificador del rango de longitud. En este primer caso simple, y para una extensión de la clave en el intervalo [min_sz, max_sz], la expresión regular de validación viene dada simplemente por:

^.{min_sz,max_sz}$

La expresión regular más general que consideraremos en este post tomará, sin embargo, la forma extendida siguiente:

^(?=.*?[0-9])(?=.*?[a-z])(?=.*?[A-Z])(?=.*?[$!?#@%&*^-]).{min_sz,max_sz}$

En ella distinguiremos los siguientes mecanismos de validación adicionales, indicados junto a sus aserciones de búsqueda anticipada (lookahead) asociadas [4]:

  • Digit (?=.*?[0-9]): Existencia de, al menos, un dígito.
  • Lower_upper_case (?=.*?[a-z])(?=.*?[A-Z]): Existencia de, al menos, una letra minúscula y otra mayúscula.
  • Special_character (?=.*?[$!?#@%&*^-]): Existencia de, al menos, un símbolo no-alfanumérico.

El programador podrá optar por incluir o no estas aserciones en el verificador, a su entera conveniencia y en tiempo de compilación.


Implementación


En primer lugar, registremos las aserciones DigitLower_upper_case y Special_character a través de estructuras simples:

   #include <cstdlib>    #include <fmt/format.h>    #include <iostream>    #include <regex>    #include <string>    using namespace std;    struct Digit {       static constexpr auto lookahead = "(?=.*?[0-9])";    };    struct Lower_upper_case {       static constexpr auto lookahead = "(?=.*?[a-z])(?=.*?[A-Z])";    };    struct Special_character {       static constexpr auto lookahead = "(?=.*?[$!?#@%&*^-])";    };

Observemos que, en todos los casos anteriores, la variable miembro estática constexpr lookahead es inferida como un puntero clásico char const* propio de C. Su conversión a un string estándar puede realizarse de forma automática.

El verificador de contraseñas aceptará los mecanismos de validación anteriores como argumentos Check_policies de una plantilla de clase variádica de nombre Password_validation. La elipsis ...Check_policies indica, en este caso, la longitud variable (posiblemente nula) del conjunto de tales políticas (parameter pack [1]):

   template<typename ...Check_policies>    class Password_validation {       regex const validation_rgx_;    public:       Password_validation(size_t min_sz, size_t max_sz)          : validation_rgx_{                fmt::format(R"~(^{0}.{{{1},{2}}}$)~",                            (string{} + ... + Check_policies::lookahead), min_sz, max_sz)          }       { }       explicit Password_validation(size_t sz) : Password_validation{sz, sz} { }       auto operator()(string const& pwd) const -> bool       {          return regex_match(pwd, validation_rgx_);       }    };

En la plantilla de clase anterior, el primer constructor recibe como argumento el rango de longitud permitido para las claves. Dicho constructor inicializa un objeto privado std::regex a través de un string generado por la función fmt::format(). Dicho string contiene la expresión regular (con formato adaptado para la biblioteca {fmt}lib):

^{0}.{{{1},{2}}}$

donde el placeholder {0} se rellena con la serie de aserciones opcionales de tipo DigitLower_upper_case y/o Special_character, de existir alguna. Esto se consigue a través de un fold expression que añade a un string vacío las cadenas de caracteres estáticas lookahead de las políticas seleccionadas:

(string{} + ... + Check_policies::lookahead)

Por su parte, los placeholders {1} y {2} se rellenan con las longitudes mínima y máxima permitidas para la clave, respectivamente.

El segundo constructor, que delega su ejecución en el primero, se reserva para la creación de verificadores de contraseñas de longitud única (min_sz coincidente con max_sz).

Por su parte, la sobrecarga del operador de llamada a función operator() --encargada de validar cualquier clave proporcionada por el usuario-- permite trabajar con instancias de la clase como si de funciones predicado se tratara.


Ejemplo de uso


La siguiente función principal permite validar contraseñas según su longitud, la presencia de al menos un dígito, un carácter en minúscula y otro en mayúscula:

   // pon el código anterior aquí y prueba a compilar    auto main() -> int    {       auto const check = Password_validation<Digit, Lower_upper_case>{816};       auto pwd = string{};       cout << "Insert a password: ";       getline(cin, pwd);       cout << fmt::format("Password '{}' is {}", pwd, check(pwd)? "OK" : "invalid");    }

El problema de validación analizado en este post se inspira en el desafío 67 de la referencia [5]. La solución propuesta por dicho título descansa en el empleo de polimorfismo dinámico, combinando de forma elegante el patrón de diseño "Decorator" (el cual permite añadir comportamiento a un objeto sin necesidad de afectar a otros objetos del mismo tipo [6]) y el uso de punteros inteligentes de tipo std::unique_ptr<>. Nuestra solución, a diferencia de aquélla, descansa en el empleo de expresiones regulares y, muy particularmente, en la elección de las políticas de validación en tiempo de compilación, lo que evita el sobrecoste asociado a las tablas virtuales y los grados adicionales de indirección, si bien renunciando a la posibilidad de seleccionar dichas políticas en tiempo de ejecución. El empleo de una solución dinámica o estática dependerá, por supuesto, de las necesidades del programador.


Referencias bibliográficas:
  1. https://en.cppreference.com/w/cpp/language/parameter_pack
  2. https://en.cppreference.com/w/cpp/language/fold
  3. https://github.com/fmtlib/fmt
  4. https://stackoverflow.com/questions/19605150/regex-for-password-must-contain-at-least-eight-characters-at-least-one-number-a
  5. Bancila M., 'The Modern C++ Challenge: Become an expert programmer by solving real-world problems'. Packt Publishing (2018).
  6. Gamma E. et al., 'Design Patterns: Elements of Reusable Object-Oriented Software'. Addison-Wesley Professional Computing Series (1994).

No hay comentarios:

Publicar un comentario