Introduciendo contraseñas de forma secreta
PÍLDORAS DE VITAMINA C
Introduciendo contraseñas de forma secreta
2024-05-01
Por
Yvil Yenius

Cualquier organización maligna que se precie debe proteger con contraseñas absolutamente todo. Servidores, programas, acceso a bases de datos. TODO. Añadir una contraseña a tu sistema es bastante fácil, pero lo que quizás no sepas es como conseguir que esa contraseña no se vea mientras la escribes. Pues en ese caso… este es tu artículo.

Parece una tontería verdad, pero no es tan evidente. Como genia diabólica he invertido muchos años en estudiar las mejores formas de construir sistemas misteriosos y secretos y ahora, que he evolucionado mi organización hacia tecnologías más sofisticadas, puedo compartir alguna de esta valiosísima información, la cual, aunque obsoleta, puede ser útil para alguno de vosotros.

PROTEGIENDO UN PROGRAMA CON CONTRASEÑA

A modo de ejemplo vamos a comenzar con un pequeño programa de prueba con el que trabajar. Se trata del primer servicio super secreto que incluí en mi organización clandestina para mis secuaces. La verdad es que el servicio no hacía nada, pero molaba mucho y a los secuaces les encantaba. Este es el programa.

#include <stdio.h>
#include <string.h>

int main () {
  char clave[1024];
  
  puts ("Servicio Super Secreto");
  puts ("(c) Yvil Genius para IbolCorp, 2024\n");
  printf ("Contraseña ?  ");
  fflush (stdout);
  fgets (clave, 1024, stdin);
  if (strncmp (clave, "YvilRulez!", 10) != 0) {
    puts ("Acceso Denegado!");
    return 1;
  }
  puts ("Bienvenido secuaz! ");
  // Aquí empieza el servicio super secreto
  return 0;
}

El programa es muy básico, pero voy a hacer un par de comentarios para los principiantes.

Hablaremos en un momento sobre los terminales, pero por defecto debes saber que los terminales funcionan en lo que se llama Buffered Mode (realmente el modo tiene otro nombre, pero lo veremos en un rato). En este modo, los caracteres que queremos imprimir o leer no se transfieren a/desde nuestro programa inmediatamente sino que se almacenan en una memoria intermedia. Lo que los anglosajones llaman Buffer. Cuando la memoria se llena o se cumple una cierta codición, como por ejemplo que hemos enviado un caracter \n (esto es pulsar ENTER), el buffer se imprime en pantalla o se lee en nuestro programa.

Puesto que queremos que la contraseña se imprima justo al lado de el prompt ( Contraseña ?), debemos forzar la impresión del buffer con fflush. De lo contrario, dependiendo del estado del sistema, la cadena Contraseña ? puede imprimirse antes o después de introducir nuestra contraseña. Normalmente después, justo al pulsar ENTER.

El otro comentario para principiantes es que fgets lee el retorno de carro. Es decir, nuestra variable clave contendrá un caracter \n al final. Para lidiar con esto tenéis varias opciones. Podéis eliminar el caracter con algo como clave[strlen(clave) - 1] = 0 o ignorarlo como he hecho yo en este ejemplo. Simplemente comparo 10 caracteres usando strncmp en lugar de strcmp, con lo cual el retorno de carro final se deja fuera de la comparación.

TERMINALES

La forma de interactuar con un sistema UNIX es a través de un terminal. En el principio de los tiempos se utilizaba una máquina especial llamada terminal (eso no lo viste venir eh?) que se conectaba al ordenador, normalmente, a través de un puerto serie. En la actualidad, utilizamos emuladores de terminal que son, digamos, la versión software de aquellas máquinas.

Estos terminales no hacían muchas cosas, pero tampoco eran simples máquinas de escribir. Una de las cosas que permitían hacer era configurarlos. El terminal en si tenía un sencillo software con el que configurar distintas funcionalidades. También interpretaban comandos especiales que hacían cosas como borrar la pantalla o moverte a una posición concreta de la misma. Estos códigos se conocen como secuencias de escape, ya que escapaban del flujo de datos normales hacia el ordenador, y estaban dirigidas al terminal. Había distintas secuencias para los distintos tipos de terminales, pero se acabaron estandarizando y hoy en día, los emuladores de terminal siguen soportando las principales, siendo las secuencias ANSI las más populares y estándar.

En general, manejar el terminal directamente es una tarea tediosa y complicada, y los programas suelen optar por dos opciones:

  • Obviar el terminal completamente y simplemente leer y escribir datos sin más como en mi primer servicio secreto super clandestino.
  • Utilizar alguna librería que maneje el terminal por nosotros. La librería curses es la más popular y la que utilizan la mayoría de programas que quieren controlar lo que se muestra en pantalla. Esto incluye, por ejemplo, el programa de configuración del kernel.

OCULTANDO LA CONTRASEÑA

Los terminales por defecto muestran en pantalla cualquier tecla que pulsamos, siempre que no se trate de una tecla asociada a una secuencia de control como por ejemplo BACKSPACE. En ese caso, la tecla es procesada. Los código de control suelen tener asignados valores menores que 32, el cual representa la barra espaciadora. Por ejemplo, el código 8 se reserva para BACKSPACE y el 9 para el tabulador. Además de esos valores el código 127 representa la tecla DEL.

Esta función de imprimir las teclas que pulsamos se conoce como ECHO y está activa por defecto en todos los terminales. La forma más sencilla de hacer que nuestra clave no se muestre en pantalla es desactivar esta función mientras leemos la contraseña. Para ello debemos reconfigurar nuestro terminal.

La forma de reconfigurar nuestro terminal es utilizar las funciones tcgetattr y tcsetattr que nos permiten leer y escribir atributos del terminal. Para usar estas funciones debemos incluir termios.h en nuestro programa.

Veamos como quedaría el código y luego lo comentamos en detalle.

(...)
  struct termios term;
  
  tcgetattr (0, &term);
  term.c_lflag &= ~ECHO;
  tcsetattr (0, TCSANOW, &term);
  
  fgets (clave, 1024, stdin);
  
  term.c_lflag |= ECHO;
  tcsetattr (0, TCSANOW, &term);
(...)

CONFIGURANDO ECHO

La forma de interactuar con las funciones definidas en termios.h es usando la estructura termios, la cual contiene, al menos los siguientes campos:

           tcflag_t c_iflag;      /* input modes */
           tcflag_t c_oflag;      /* output modes */
           tcflag_t c_cflag;      /* control modes */
           tcflag_t c_lflag;      /* local modes */
           cc_t     c_cc[NCCS];   /* special characters */

Estos son los campos que podemos manipular, y para el ejemplo que nos ocupa, lo que nos interesa son los llamados local modes. Usando ese campo podemos activara y desactivar fácilmente la función de ECHO.

A efectos prácticos tcflag_t es un entero que contiene distintos flags. El flag que controla el echo está asignado a la constante ECHO (definida en termios,h) que se corresponde con 0x08 o el cuarto bit si lo prefieres, si bien, es recomendable utilizar la constante ECHO en lugar de el valor numérico. Así que sabiendo todo esto la forma de desactivar el ECHO del terminal es la siguiente:

  • Leer la configuración actual. Esto lo tenemos que hacer ya que no podemos modificar un solo flag del terminal. Tenemos que modificarlos todos.
  • De la configuración actual modificamos el flag que controla el echo del terminal
  • Escribimos la configuración modificada.

En el código de la sección anterior, la forma de modificar el flag de ECHO es la siguiente:

  term.c_lflag &= ~ECHO;

Para los que no tengáis mucha práctica con el manejo de bits vamos a descomponer esta expresión paso a paso. Imaginad que la constante ECHO tiene el valor 0x08 (lo cual es el caso para mi sistema actual).

XXXX XXXX XXXX XXXX   c_lflags
0000 0000 0000 1000   ECHO
1111 1111 1111 0111   ~ECHO
XXXX XXXX XXXX 0XXX   c_lflags & ~ECHO

Como podéis ver, la operación anterior borra el bit 4 de c_flags sin tocar ninguno de los otros, que es exactamente lo que queremos hacer. Una vez tenemos la nueva configuración del terminal lista, simplemente la aplicamos con tcsetattr usando la opción TCSANOW para que los cambios se apliquen inmediatamente. A partir de ese momento el ECHO del terminal estará desactivado y las teclas que pulsemos no se mostrarán en pantalla.

Para reactivarlo, procedemos de la misma forma, pero esta vez asegurándonos de que el flag que nos interesa está a 1.

   term.c_lflag |= ECHO;

En este caso solo necesitamos hacer un OR con el valor original, el cual pondrá a 1 ese bit independientemente del valor que tuviera anteriormente.

El primer parámetro que pasamos a tcgetattr y tcsetattr es el descriptor de ficheros de la entrada estándar. En general este será el valor 0.

Hemos conseguido no mostrar la contraseña en pantalla cuando la escribimos, lo cual está genial, pero podemos hacerlo mejor.

IMPRIMIENDO ESTRELLAS

Si bien, nuestro programa cumple su finalidad, la experiencia de usuario es muy mala. El usuario pulsa teclas pero parece que nada ocurre hasta que al final pulsa ENTER. Esa es la razón por la que, tradicionalmente se imprime algo cuando se introduce una clave.

En aplicaciones de consola suele ser una estrella o asterisco. En aplicaciones gráficas se suelen utilizar signos más guays. Nosotros vamos a mostrar estrellas, y para ello debemos desactivar el modo canónico del terminal. Os preguntaréis que es eso del modo canónico… bueno, ahora mismo os lo explico.

MODO CANóNICO

Los terminales también ofrecen distintos modos de operación. El modo de operación por defecto que asumen los sistemas GNU/Linux (y la mayoría de otros UNIX) se conoce como modo canónico. En este modo ocurre lo siguiente:

  • La entrada de datos está disponible linea a linea. Esto significa que hasta que pulsemos ENTER (o NL NewLine) o el caracter de fin de entrada de línea o de datos EOL o EOF los datos no se transferirán al programa. El caracter EOL suele ser \n si bien esto se puede configurar. El caracter EOF es por defecto CTRL+D. Es posible configurar un segundo caracter de fin de línea, pero eso no nos aporta nada en este momento. Consultad la documentación si tenéis curiosidad,
  • La edición de línea está habilitada. Esto significa que las teclas BACKSPACE o DEL funcionan y se pueden utilizar para modificar los datos introducidos. Por ejemplo, BACKSPACE tiene la secuencia de escape 0x08. En modo canónico, en lugar de añadir 0x08 al buffer que estamos leyendo, eliminamos el último caracter en el buffer. En el programa de ejemplo podéis comprobarlo. Veremos en un segundo como esto no se cumple en otros modos.
  • El tamaño máximo de línea es 4096. Lineas de tamaños mayores se truncan a 4095. Cualquier dato por encima de eso se descarta pero se procesa de forma que caracteres de nueva línea que lleguen más tarde indicarán el fin de la entrad… aunque una parte de esa entrada se descarte.

Así que, para poder imprimir una estrella cada vez que pulsamos una tecla debemos desactivar el modo canónico puesto que de lo contrario no podríamos hacer nada hasta que el usuario pulsara ENTER, que es lo que nos ha estado pasando hasta ahora.

DESACTIVANDO MODO CANÓNICO

Como acabamos de ver, lo primero que debemos hacer es desactivar el modo canónico. Esto lo conseguimos modificando los modos locales, igual que hicimos con la función de ECHO. En este caso, ya que vamos a modificar varios valores de la configuración, en lugar de activar y desactivar los bits que nos interesan haremos una copia de los valores originales de tal forma que podemos modificar todo lo que queramos sin preocuparnos ya que utilizaremos el valor original de configuración para restaurar el terminal.

Este es el código que debemos añadir:

    struct termios orig_term;
    struct termios new_term;
    
    tcgetattr (0, &orig_term);
    memcpy (&new_term, &orig_term, sizeof(struct termios));
    
    new_term.c_lflag &= ~(ICANON | ECHO);
    new_term.c_cc[VTIME] = 0;
    new_term.c_cc[VMIN] = 1;
    tcsetattr(0, 0, &new_term);

Como podéis ver esta vez leemos el valor original de la configuración del terminal y a continuación hacemos una copia usando memset. Desde ese punto trabajamos sobre la copia. La forma de desactivar ECHO ya sabemos como es y da la casualidad que el modo canónico no es más que otro flag en el mismo campo de la estructura termios. En ese caso combinamos las dos máscaras ICANON | ECHO y las usamos a la vez.

El valor por defecto de ICANON es 0x02 (segundo bit activado), con esto podéis intentar comprobar que el programa anterior desactiva los bits que queremos (2 y 4).

A continuación utilizamos un nuevo campo de la estructura termios. De acuerdo a la documentación man tcsetattr los valores VTIME y VMIN nos permiten controlar como se leen los valores de la entrada estándar.

  • VMIN indica el número de caracteres mínimo a leer antes de retornar el valor a la aplicación (de hecho a la llamada al sistema read). Este valor solo se aplica en modo no canónico. En este caso queremos leer un solo caracter o, dicho de otra forma, queremos que read retorne cada vez que haya un caracter disponible (sin buffer).
  • VTIME es el timeout. Nos permite indicar un tiempo tras el cual, si no hay suficientes caracteres disponibles read retornará de todas formas. En este caso, el valor 0 significa timeout infinito y por lo tanto read solo retornará cuando haya VMIN caracteres disponibles.

Según la documentación (man tcgetattr por ejemplo), esta configuración se denomina Lectura Bloqueante.

En este punto nuestro terminal esta configurado para leer un solo caracter de cada vez y con el ECHO desactivado. Escribamos el bucle para leer una contraseña.

LEYENDO LA CONTRASEÑA

Para leer la contraseña, ejecutaremos un bucle infinito en el que leeremos caracter a caracter. Ahora ya no estamos en modo canónico y nosotros tenemos que controlar cuando terminar la lectura. En nuestro caso cuando se pulse la tecla ENTER o se introduzca el caracter \n si lo preferís. Este es el código:

#define MAX_PASS 1024

    char c, indx = 0;
    do {
      read (0, &c, 1);
      if (c == '\n') break;
      clave[indx++] = c;
      write (1, "*", 1);
      if (indx > MAX_PASS) break;
    } while (1);

    tcsetattr(fileno(stdin), 0, &orig_term);

Sin sorpresas ¿verdad?. Leemos un caracter. Si es el fin de línea terminamos y sino, lo almacenamos e imprimimos un asterisco. Finalmente, si hemos alcanzado el tamaño máximo de nuestro buffer terminamos la lectura de datos para evitar un desbordamiento de buffer.

Ahora, cada vez que pulsemos una tecla se imprimirá un asterisco como queríamos.

No olvidéis incluir unistd.h ya que ahora estamos usando las funciones read y write.

SIGUIENTES PASOS

Hasta aquí lo fundamental sobre este tema, pero no queremos irnos sin dejaron un par de modificaciones que podéis intentar para practicar y mejorar la entrada de contraseñas en vuestros programas.

La primera es la de procesar los caracteres de control. Ahora, puesto que nuestro programa ya no está en modo canónico, no es posible la edición de líneas (por razones obvias), sin embargo, podéis incorporar código para procesar teclas especiales como BACKSPACE para borrar un caracter de la clave. Tal y como está el programa ahora mismo, si pulsáis BACKSPACE ese caracter será parte de la clave. Lo mismo con TAB por ejemplo, es algo que, en general, no debería permitirse en una clave.

La segunda es algo que he visto en algunos programas como por ejemplo en Lotus Notes. Cuando introduces la clave el programa muestra un número aleatorio de estrellas por cada pulsación, de forma que no puedes saber cual es el tamaño de la clave contando las estrellitas. Dependiendo del algoritmo utilizado, saber la longitud de la clave puede facilitar un poco las cosas.

Como siempre no dudéis en enviarnos vuestras modificaciones o cualquier duda que podáis tener respecto al artículo o cualquier variación del código en la que trabajéis.

No desesperéis. Según haga más sofisticadas mis herramientas de dominación del mundo iré compartiendo con vosotros técnicas más avanzadas. Estad atentos si como yo queréis desarrollar el máximo potencial de vuestra red de secuaces y haceros un nombre en la carrera por dominar el mundo. JAJAJAJAJAJA (risa diabólica)


SOBRE Yvil Yenius
Cuando se trata de dominar el mundo, Yvil es la referencia. Su privilegiada mente para el mal y su carisma para ganarse la lealtad de secuaces a diestro y siniestro, la han convertido en una de las más grandes mentes malvadas de la historia. En su tiempo libre, hornea deliciosas tartas que lanzar a sus enemigos.

 
Tu publicidad aquí :)