Validación de usuarios vía email con CakePHP

Hoy vamos a ver como desarrollar de forma sencilla un sistema que nos permita validar de forma segura que un usuario de nuestra aplicación web efectivamente tiene el email que ha indicado en el formulario de registro. El mismo sistema podría servir también para evitar cambios fraudulentos en los perfiles de usuario, para recuperar cuentas cuando se olvidan claves o para solicitar la eliminación de la cuenta de usuario sin temor a gamberradas ;) .

La idea básica consiste en generar una clave aleatoria durante el proceso de registro, guardarla en la tabla de usuarios (para ello usaremos un campo específico), enviar un email al usuario con un enlace a un método de validación del controlador de usuarios (pasando como parámetros algún identificador del usuario más la clave generada anteriormente), y finalmente, comparar en el mencionado método de validación si la clave pasada a través del enlace corresponde con la clave almacenada en la base de datos. Si dichas claves son iguales entonces se cambia el estado del usuario a "activado" modificando algún campo específico de la tabla de usuarios.

Ahora, antes de pasar a la acción todavía, pensemos en como refinar ligeramente el método para que sea más útil. Si nos fijamos en el último paso, puede parecer que hace falta una columna específica que indique si el usuario ha sido validado o no, pero podremos ahorrárnoslo, y ganar mucho por el camino. Mi idea (y seguro que al de muchos otros) es usar el propio campo dedicado al código de validación. Hay varias formas de hacerlo, pero usaré como ejemplo mi propio procedimiento. Veamos el código y luego analizaremos el proceso más detenidamente.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app_controller.php ( AppController )
	// Generador de claves que usaremos para nuestro sistema de seguridad
	protected function genPass ($len, $uppercase = true, $lowercase = true, $numbers = true, $sym1 = true) {
		$up_dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
		$lo_dict = 'abcdefghijklmnopqrstuvwxyz';
		$nu_dict = '0123456789';
		$s1_dict = '.-';
 
		$pass_dict = (($uppercase) ? $up_dict : '') .
			(($lowercase) ? $lo_dict : '') .
			(($numbers) ? $nu_dict : '') .
			(($sym1) ? $s1_dict : '');
 
		$dict_size = strlen ($pass_dict);
 
		$pass = '';
		for ($i=0; $i<$len; $i++) {
			$pass .= $pass_dict[rand (0, $dict_size-1)];
		}
 
		return $pass;
	}

Nota al canto: he hecho que el generador de claves trabaje solo con un conjunto de 64 tipos de caracteres, esta restricción no tiene mucho sentido, pero me parece un número bonito y lo suficientemente grande. Ahora bien, hay que destacar que ciertos caracteres pueden conllevar problemas, como por ejemplo "+", o ":".

1
2
3
4
5
6
7
8
9
// Users controller -> Register method (before calling the save method of the User model)
 
$vcode = $this->genPass (62);
$this->data['User']['validate'] = 'v:' . $vcode;
 
// Here you should put the code to save the data into the database
// and to send an email to the user, with a link like:
// http://www.yourapp.com/users/validate_email/username/validatecode
// (It's preferible that the user don't know it's user internal id)

La única modificación sustancial que deberéis hacer en vuestro método de registro es crear la clave antes de guardar los datos de usuario en la base de datos (evidentemente la tabla de usuarios deberá tener un campo "validate"), y evidentemente, no olvidar enviar un email al usuario con el enlace de validación.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Users Controller
	public function validate_email () {
		if (count ($this->params) < 2) {
			$this->redirect ('/users/dashboard'); // Or wherever you want
		}
 
		$username = Sanitize::paranoid (
			$this->params['pass'][0],
			array ('_')
		);
		$code = Sanitize::paranoid (
			$this->params['pass'][1],
			array ('-', '.')
		);
 
		$db_code = $this->User->field (
			'validate',
			array (
			    'username' => $username,
			)
		);
 
		if ($db_code == 'v:'.$code) {		
			$this->User->updateAll (
				array (
				    'validate' => '\'' . $this->genPass (64) . '\''
				),
				array (
				    'username' => $username
				)
			);
 
			$this->set ('validated', true);
		} else {
			$this->set ('validated', false);
		}
	}

Supongamos que tenemos una clave de validación de 64 caracteres, difícilmente podríamos romperla, y aunque nos quedáramos solo con 62 caracteres, seguiríamos con un nivel de seguridad parecido. Aprovechando este detalle, en vez de generar claves de 64 caracteres, las construiremos solo con 62, y añadiremos al principio de estas la cadena "v:" (2 sin contar las comillas). Cuando nuestra aplicación detecte que los primeros dos caracteres de la clave de validación almacenada en la base de datos sean "v:" entonces considerará que el usuario aun no ha sido validado. Una consecuencia de ello es que durante el proceso de validación, una buena forma de dar por validado al usuario sería generar una nueva clave (esta vez de 64 caracteres, y sin el caracter ":").

La nueva clave generada para dar por validado al usuario puede usarse, a su vez, cuando el usuario olvide su clave y la aplicación tenga que enviarle un email, para poder verificar que el método de cambio de clave es llamado desde el email del usuario afectado, y no por una persona ajena a su cuenta (De igual forma, se podría usar para verificar eliminación de cuentas, etc.). Eso sí, nunca debe olvidarse crear una nueva clave cada vez que el sistema de verificación es usado para evitar que la seguridad decaiga.

Espero que os haya resultado útil :) , saludos.

Validación de NIFs, NIEs, DNIs y CIFs en PHP

Hará unas dos semanas estuve lidiando con cierta porción de código dedicada a la validación de NIFs, NIEs, DNIs y CIFs para dificultar fraudes y "suciedad" en la base de datos que usa cierta aplicación que estoy desarrollando para la Facultad de Economía y Empresa de la Universidad de Barcelona. Tengo que decir que lo más difícil no fue programar, sino encontrar la información, que se haya en gran medida de forma dispersa a través de la red, y peor aun, en muchos casos es incorrecta.

Conviene destacar que gran parte de la dificultad de encontrar esa información está ligada a los sucesivos cambios legislativos (el último fue en 2008!), el poco interés que despierta en la "clase programadora", y la escasa reflexión de los primeros diseñadores del sistema de numeración del DNI.

Bien, después de pelearme largo y tendido con el problema, acabé encontrando todo lo que necesitaba, y aquí podéis ver el resultado, para que no tengáis que perder el tiempo buscando en mil y un lugares diferentes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
	// Función auxiliar usada para CIFs y NIFs especiales
	private function getCifSum ($cif) {
		$sum = $cif[2] + $cif[4] + $cif[6];
 
		for ($i = 1; $i<8; $i += 2) {
			$tmp = (string) (2 * $cif[$i]);
 
			$tmp = $tmp[0] + ((strlen ($tmp) == 2) ?  $tmp[1] : 0);
 
			$sum += $tmp;
		}
 
		return $sum;
	}
 
	// Valida CIFs
	// El código comentado es para usar en modelos de CakePHP
	protected function validateCif ($cif /*$check*/) {
		$cif_codes = 'JABCDEFGHI';
 
		// $cif = array_pop ($check);
 
		$sum = (string) $this->getCifSum ($cif);
		$n = (10 - substr ($sum, -1)) % 10;
 
		if (preg_match ('/^[ABCDEFGHJNPQRSUVW]{1}/', $cif)) {
			if (in_array ($cif[0], array ('A', 'B', 'E', 'H'))) {
				// Numerico
				return ($cif[8] == $n);
			} elseif (in_array ($cif[0], array ('K', 'P', 'Q', 'S'))) {
				// Letras
				return ($cif[8] == $cif_codes[$n]);
			} else {
				// Alfanumérico
				if (is_numeric ($cif[8])) {
					return ($cif[8] == $n);
				} else {
					return ($cif[8] == $cif_codes[$n]);
				}
			}
		}
 
		return false;
	}
 
	// Valida NIFs (DNIs y NIFs especiales)
	// El código comentado es para usar en modelos CakePHP
	protected function validateNif ($nif /*$check*/) {
		$nif_codes = 'TRWAGMYFPDXBNJZSQVHLCKE';
 
		// $nif = strtoupper (array_pop ($check));
 
		$sum = (string) $this->getCifSum ($nif);
		$n = 10 - substr($sum, -1);
 
		if (preg_match ('/^[0-9]{8}[A-Z]{1}$/', $nif)) {
			// DNIs
			$num = substr($nif, 0, 8);
 
			return ($nif[8] == $nif_codes[$num % 23]);
		} elseif (preg_match ('/^[XYZ][0-9]{7}[A-Z]{1}$/', $nif)) {
			// NIEs normales
			$tmp = substr ($nif, 1, 7);
			$tmp = strtr(substr ($nif, 0, 1), 'XYZ', '012') . $tmp;
 
			return ($nif[8] == $nif_codes[$tmp % 23]);
		} elseif (preg_match ('/^[KLM]{1}/', $nif)) {
			// NIFs especiales
			return ($nif[8] == chr($n + 64));
		} elseif (preg_match ('/^[T]{1}[A-Z0-9]{8}$/', $nif)) {
			// NIE extraño
			return true;
		}
 
		return false;
	}

Además, aquí tenéis la lista de fuentes en las que me he basado. En primer lugar las no oficiales, comentadas para indicar las que no me parecen de fiar, porque eso también puede llevar quebraderos de cabeza.

  1. http://es.wikipedia.org/wiki/Número_de_identificación_fiscal (información incompleta)
  2. http://es.wikipedia.org/wiki/Código_de_identificación_fiscal (información incompleta)
  3. http://compartecodigo.com/javascript/validar-nif-cif-nie-segun-ley-vigente-31.html (a mi entender, tiene fallos)
  4. http://sourcecookbook.com/en/recipes/36/validacion-automatica-de-cif-nif-y-nie-segun-la-ultima-legislacion-j-query (otro con bastantes fallos)
  5. http://menudoproblema.es/blog/entries/2011/01/05/como-realizar-la-validacion-de-un-cif-en-python/ (incompleto, pero inspirador)
Sobre fuentes oficiales, una serie de artículos del BOE que dan cuenta de la evolución de la legislación referente a esos códigos identificativos:
  1. http://boe.es/boe/dias/1975/10/22/pdfs/A22177-22178.pdf
  2. http://boe.es/boe/dias/1987/12/24/pdfs/A37785-37839.pdf (artículo 113)
  3. http://boe.es/boe/dias/1990/03/14/pdfs/A07256-07259.pdf
  4. http://boe.es/boe/dias/2007/09/05/pdfs/A36512-36594.pdf
  5. http://boe.es/boe/dias/2008/02/26/pdfs/A11374-11376.pdf
Los dos últimos son los más interesantes por ser los más nuevos, aun así, la lista de documentos oficiales relativos al asunto es mucho más extensa, y se podría completar a través de las referencias hechas a los que faltan en los que yo he enlazado.
Bien, espero que esto pueda resultar de ayuda a alguien.

Ubuntu 9.10, desencanto total

Después del episodio que sufrí con el software científico vienen más problemas, Ubuntu últimamente no da pie con bola, ¿qué pasa en Canonical? Por un lado he notado que el sistema anda algo más lento que antes, no es que vaya lento, pues tengo un buen equipo, pero puedo asegurar que el rendimiento es menor. Por otro lado estoy sufriendo problemas algo más importantes y que me inquietan un poco, ciertos atajos de teclado no funcionan (aunque están configurados correctamente), por ejemplo, si quiero bloquear la pantalla con la combinación Ctrl+Alt+L me resulta imposible.

A eso debo añadir que aunque ha mejorado en el aspecto de la hibernación (antes no podía y ahora sí), ha empeorado en cuestiones de seguridad. Me explico, cuando suspendo o hiberno el sistema no necesito introducir mi clave una vez vuelvo a trabajar con él... supongo que se podrá hacer un apaño para arreglarlo, pero la configuración que han dejado por defecto no me gusta en absoluto. ¿Qué pasa si me roban el portátil en un despiste mientras lo tengo en suspenso? Pues que se puede acceder a toda mi información sin ningún esfuerzo, independientemente de que tenga cifrado el disco con una clave de tropecientos mil bits, seguridad zero.

En cuanto tenga algo de tiempo libre me paso a otra distro, las que estoy considerando: Debian (que ya la sé manejar y tiene un equipo más profesional que Ubuntu trabajando en ella, además siempre puedo usar el repo sid ;) ), Fedora (Los chicos de Redhat siempre mantienen a la última sus distros con un montón de novedades intersantes) y Arch (ésta última supone mucho trabajo... pero también supone mucho aprendizaje). Descarto Gentoo por falta de tiempo. A ver qué acabo usando.

El numerito

09-f9-11-02-9d-74-e3-5b-d8-41-56-c5-63-56-88-c0

El número mágico que servirá para descifrar los discos HD DVD y saltarse la protección DRM impulsada por companías tales como IBM, Intel, Microsoft, Panasonic, Sony, Toshiba, Walt Disney y Warner Bros.

Una pequeña explicación de la importancia de éste numero está en:
http://kriptopolis.org/el-numerito

Otra explicación más técnica y detallada la podemos encontrar en:
Advanced Access Content System (AACS): Introduction and Common Cryptographic Elements [PDF, 82 páginas, 540 KB].

o en:
Understanding AACS

Además, por lo visto se han conseguido otros métodos para saltarse dicha protección sin el famoso número:
http://www.microsiervos.com/archivo/hackers/nuevo-crack-aacs-sin-09f91102.html

Éste tema me parece bastante interesante, hay que reconocer que el golpe ha sido duro para los defensores del DRM, puesto que el descubrimiento de la clave obligará a establecer otra nueva clave (lo que podría inutlizar mucha de la maquinaria producida, además de las películas que puedan estar a la venta). En caso de que no cambiaran la clave, el sistema quedaría anulado y listo, adios al DRM. :D