domingo, 7 de septiembre de 2008

Internacionalización en PHP usando gettext

En algunas oportunidades se hace necesario que nuestro sitio web tenga versiones en múltiples idiomas.

El concepto de internacionalización o i18n en sitios web consiste no sólo en mostrar los contenidos en el idioma del usuario, sino que también implica ajustarse a sus configuraciones regionales tal como: formatos de fechas, formatos de números y reglas de ordenamiento alfabético. En la actualidad la mayoría de las base de datos y lenguajes de programación modernos tienen soporte para i18n.

En este artículo me enfocaré en el problema de mostrar el contenido de un sitio web en múltiples idiomas, para ello les presentaré mi solución favorita basada en gettext, que es la biblioteca GNU para internacionalización. Los ejemplos que aquí daré están basados en PHP, pero son totalmente aplicables a otros lenguajes de programación como por ejemplo C.


Codificación
Como primera medida en el proceso de internacionalización, nuestros sistemas debieran ser desarrollados utilizando codificación UTF-8 en vez de la tradicional ISO-8859-1 también conocida como LATIN1.

La codificación UTF-8 prácticamente soporta la mayoría de los simbolos (y caracteres) de escritura a nivel mundial. La codificación LATIN1 es mucho más reducida en la cantidad de simbolos (o caracteres) de escritura, pero es suficiente en caso que queramos mantener nuestro sitio en español e inglés.

En caso que optemos por desarrollar un sistema con codificación LATIN1 o si tenemos que dar soporte multi-idioma a sistemas heredados que ya fueron desarrollados con LATIN1, se deberán realizar algunos pasos adicionales que iré comentando en los ejemplos.


Módulo gettext
Cuando generamos contenidos hacia el web, usualmente extraemos la información de dos fuentes: base de datos y textos estáticos.

La traducción del contenido de la base de datos se debe realizar caso a caso de acuerdo a las características de cada sistema y no es cuerto en este artículo.

La traducción de los textos estáticos se realizará usando gettext que usualmente ya está habilitada en sistemas LINUX. En Windows basta con editar el archivo de configuración de PHP y descomentar (o escribir) la siguiente linea:
extension=php_gettext.dll

Adicionalmente, para generar las traducciones es necesario tener instalado gettext, el cual normalmente ya viene instalado en LINUX y puede ser descargado para Windows en la página del proyecto GNU para Windows.



Manos a la obra
Usualmente en nuestro código tenemos instrucciones como las siguientes:
echo "Hola mundo";
print "Hola mundo";
printf("Hola %s", $nombre);

Todas esas instrucciones deberán ser modificadas para ser procesadas por la función gettext() o mejor aún por un alias de esa función llamado simplemente _(), con lo que tendremos los siguientes reemplazos:
echo _("Hola mundo");
print _("Hola mundo");
printf(_("Hola %s"), $nombre);

Esta función no realiza ningún cambio en los textos. Deberemos aplicar estos cambios a todo el contenido de nuestro sistema lo que va a implicar cambiar el estilo de programación, como por ejemplo en los siguientes ejemplos:
echo "Bienvenido $nombre";

echo "
<h1>Página principal</h1>
<p>Bienvenido $nombre</p>
";

hacemos uso de una de las características más amigables de PHP que consiste en incrustar variables directamente en un string. Para trabajar con gettext, deberemos reescribir este trozo de código. Como recomendación, les suguiero utilizar las funciones printf() y sprintf():
printf(_("Bienvenido %s"), $nombre);

printf("<h1>%s</h1><p>%s $nombre</p>",
_("Página principal"), _("Bienvenido"));



Generar traducciones
Una vez que se hayan intervenido todos los textos del sistema, se deberá ejecutar un programa que escanea todo el código fuente buscando las intervenciones que realizamos, esto crea un archivo de traducciones donde se consolidan todos los textos encontrados, dicho archivo de texto deberá ser traducido (existen programas que ayudan a hacerlo) y luego se generará un archivo binario que contiene todas las traducciones.

El primer paso es escanear todos los archivos involcados, por ejemplo para buscar todos los archivos con extensión .php y .inc se ejecuta el siguiente comando:
find . -name \*.php -o -name \*.inc > messages.txt

A continuación realizamos el proceso de escaneo de textos a traducir mediante el comando xgettext:
xgettext --language=PHP --from-code=ISO-8859-1 -o messages.po -f messages.txt

Por defecto, este comando trabaja en UTF-8, por lo que si nuestro código está en LATIN1 (ISO-8859-1) debemos utilizar el parámetro --from-code=ISO-8859-1.

La salida de este comando nos generará un archivo de texto que deberá ser traducido, por ejemplo al inglés. La edición puede ser realizada manualmente o por medio de algún programa como por ejemplo Poedit.

Una vez que tengamos nuestro archivo de traducción, deberemos convertirlo (compilarlo) a formato .mo mediante el comando msgfmt:
msgfmt messages.po -o messages.mo

Y con esto ya hemos generado nuestro archivo de traducción. Eventualmente podemos generar muchos archivos de idiomas. Ahora sólo nos falta decirle a PHP que reconozca estos archivos de idioma.


Habilitar traducciones
Debemos hacerle saber al sistem dónde encontrar nuestro archivo .mo. Los sistemas operativos tienen directorios predefinidos para estos fines, sin embargo, para evitar problemas de permisos y privilegios, prefiero configurar el sistema para indicarle manualmente dónde buscarlos.

Como en este ejemplo hemos traducido los textos al idioma inglés, pondremos el archivo de traducción en la siguiente ubicación:
/tmp/locale/en_US/LC_MESSAGES/messages.mo

Para habilitar la traducción de los textos utilizaremos el siguiente código donde la variable $locale define el idioma que será utilizado que en este caso será en_US:
$locale = 'en_US';
if (!defined('LC_MESSAGES')) define('LC_MESSAGES', 6);
setlocale(LC_MESSAGES, $locale);
bindtextdomain('facturanet', '/tmp/locale');
textdomain('messages');

En caso de que $locale indique un idioma no definido, el sistema mostrará los textos en su idioma original. Por supuesto que deberás cambiar el directorio /tmp/locale por uno más apropiado.

Los archivos binarios .mo son guardados en cache, por lo que luego de actualizar el archivo se recomienda reiniciar el servidor web.


Selección de idioma
Debido a que la variable $locale es la que determina el idioma que se presentará al usuario, les recomiendo que sea una variable de sesión de manera que el sistema recuerde el idioma del usuario entre cada página. Se debería crear una página PHP en que el usuario pueda cambiar de idioma.

Para mejorar la experiencia del usuario, es posible autodetectar el idioma del usuario consultando la variable del servidor $_SERVER['HTTP_ACCEPT_LANGUAGE'].



Otras características
Hay otras características que no alcanzaré a revisar ahora y que permiten un mejor trabajo con la boblioteca gettext:

  • En caso de que modifiquemos nuestro sistema y tengamos que realizar nuevas traducciones, no es necesario repetir todo el proceso, gettext provee comandos para incorporar nuevas palabras a un archivo ya existente mediante msgmerge.
  • Además de la función _() existe otras funciones gettext para realizar traducciones más complejas, como por ejemplo diferenciación de singuales y plurales.

jueves, 19 de junio de 2008

El performance de Google App Engine

Hace unos días estuve probando la nueva plataforma Google App Engine la cual permite hostear en los servidores de Google aplicaciones WEB desarrolladas en Python.

El modelo de negocio de Google es similar al ofrecido por Amazon, en el cual se cobra de acuerdo al tráfico, almacenamiento de datos y tiempo de CPU efectivamente utilizados. Sin embargo, Google va más allá (o provee un servicio más acotado, según el punto de vista) al proveer de una plataforma de desarrollo basada en Python.

Además de la estabilidad y disponibidad de las aplicaciones desarrolladas sobre Google App Engine, uno de las características importantes es la escalabilidad de su plataforma.

Construí una aplicación básica que hace un par de consultas a la base de datos y que genera un documento HTML de aproximadamente 1KB de información. Luego, con la herramienta Apache Benchmark (ab) realicé pruebas de carga utilizando un enlace a Internet dedicado. La prueba consistió en cargar dicha página de manera concurrente durante 5 segundos. En cada prueba modifiqué el nivel de concurrencia desde 1 hasta 100 requests en paralelo.

En promedio el tiempo de carga de la página completa demoraba 500 milisegundos, lo cual es bastante tiempo para una página liviana y que no hace más de 10 consultas SQL. Sin embargo, las pruebas de concurrencia me dejaron impresionado ya que el nivel de servicio prácticamente no se vió afectado aunque haya aumentado a 100 conexiones en paralelo, tal como lo muestro en el siguiente gráfico.


Sin concurrencia se alcanzó un nivel de 2.33 requests por segundo, al utilizar una concurrencia de 10 conexiones simultaneas se logró un nivel de 23.98 requests por segundo, finalmente, al alcanzar un nivel de 100 conexiones simultaneas se logró un nivel del 192.38 requests por segundo, es decir el servicio proveyó de un escalamiento lineal prácticamente de 1:1. Hasta ahora, disponer de enlace y hardware capaces de brindar dicho nivel de escalabilidad era extremadamente caro.

Sin duda, este nuevo producto de Google será un gran producto para los desarrolladores de software, brindando un gran nivel de escalabilidad a un costo bastante bajo.

miércoles, 11 de junio de 2008

Cool-php-captcha

En vista que había dedicado varias horas a implementar un sistema de captcha, aproveché de invertir un par de horas más y crear un proyecto que llamé cool-php-captcha publicado en Google Code bajo licencia GPLv3.

Los invito a visitar la página del proyecto http://code.google.com/p/cool-php-captcha y darme sus opiniones.

lunes, 2 de junio de 2008

Crear un CAPTCHA como el de Google

Esta es la segunda parte de mi artículo sobre la creación de CAPTCHAS. En el artículo anterior vimos el procedimiento para crear un CAPTCHA básico al cual le agregamos algunas mejoras para aumentar su complejidad y así disminuir su vulnerabilidad.

En esta segunda parte vamos a realizar algunas mejoras en la generación de imágenes, incorporaremos nuevas tipografías (la mayoría son OpenSource o Freeware) y optimizaremos el código.

Con estos cambios obtendremos un CAPTCHA de muy buena calidad sin recurrir al uso de "artefactos" o imágenes de fondo que conviertan el CAPTCHA en algo horrible, como los siguientes que son utilizados por algunos de los grandes sitios de internet: Delicious, Facebook, Fotolog, Live Journal y Microsoft Live:


Más bien, el resultado final que vamos a obtener va a ser un CAPTCHA muy parecido a los generados por Google:
Si bien este tipo de imagen no es de las más seguros, es bastante mejor que la mayoría de los captchas que están disponibles en la red.



Texto a utilizar en el CAPTCHA
En el ejemplo del artículo anterior utilizabamos un generador de textos aleatorios que intercalaba vocales y consonantes con el fin de generar palabras medianamente pronunciables.

En esta versión final inicialmente desarrollé una mejora de dicha función ya que la original ponía las vocales siempre en las mismas posiciones lo cual hacía más fácil la decodificación del captcha.

Esta nueva función genera textos aleatorios alternando consonantes y vocales al azar:

function getCaptchaText() {
$length = rand(5,7);
$consonants = "abcdefghijlmnopqrstvwyz";
$vocals = "aeiou";
$text = "";
$vocal = rand(0,1);
for ($i=0; $i<$length; $i++) {
if ($vocal) {
$text .= substr($vocals, mt_rand(0, 4), 1);
} else {
$text .= substr($consonants, mt_rand(0, 22), 1);
}
$vocal = !$vocal;
}
return $text;
}


Palabras de diccionario
Sin embargo, al realizar algunas pruebas me dí cuenta que al utilizar palabras de diccionario disminuye considerablemente el tiempo de ingreso y la tasa de error de usuarios. Además, el hecho de utilizar palabras "conocidas por el usuario" permite generar caracteres con mayor deformación.

Para la generación de palabras de diccionario, descargué un archivo de palabras en español (también puede ser en inglés u otro idioma según el público objetivo) desde http://www.word-list.com. Solamente mantuve las palabras entre 5 y 7 caracteres de longitud lo cual me dejó con solo 20.645 palabras diferentes.

No debemos olvidar quitar del diccionario palabras que podrían ser ofensivas para nuestros usuarios como por ejemplo: "zorra", "tonto", "mierda", "violador", "suicida" o "asesino".

En principio, para escoger la palabra de diccionario a utilizar, se debiera escoger un número al azar y luego recorrer el archivo de palabras línea por línea mediante fgets() hasta llegar a la línea buscada. Dicho procesamiento es altamente ineficiente por lo que hice algunos ajustes para hacer uso de fseek(): Cada una de las lineas del archivo de palabras las rellené con espacios en blanco hasta totalizar 7 caracteres más el caracter de salto de linea (\n), con lo cual quedan 8 bytes por cada palabra. Por lo tanto, si quisiera obtener la palabra de la posición 388, se debería hacer fseek(387*8) para luego leer la linea fgets().

function getDictionaryCaptchaText() {
$fp = fopen("words-es.txt", "r");
$linea = rand(0, (filesize("words-es.txt")/8)-1);
fseek($fp, 8*$linea);
$text = trim(fgets($fp));
fclose($fp);
return $text;
}


La desventaja de utilizar textos de diccionario es que el universo de palabras a utilizar disminuye de millones (entre 2 y 40 millones al utilizar hasta 7 caracteres) a miles (20 mil al utilizar palabras entre 5 y 7 caracteres).

Como una opción para solucionar este problema introduje una modificación que cambia al azar algunas de las vocales de una palabra, por ejemplo la palabra "caviar" puede cambiar a "cuvier", "covoar", "cavour", etc.

function getDictionaryCaptchaText($extended = true) {
$fp = fopen("words-es.txt", "r");
$linea = rand(0, (filesize("words-es.txt")/8)-1);
fseek($fp, 8*$linea);
$text = trim(fgets($fp));
fclose($fp);


// Cambio vocales al azar
if ($extended) {
$text = str_split($text, 1);
$vocals = array('a','e','i','o','u');
foreach ($text as $i => $char) {
if (mt_rand(0,1) && in_array($char, $vocals)) {
$text[$i] = $vocals[mt_rand(0,4)];
}
}
$text = implode('', $text);
}
return $text;
}

Como opción personal voy a utilizar simplemente las palabras de diccionario. Además, como mejora podría modificarse el script para que utilice como diccionario palabras en el idioma del usuario. Esto se puede realizar determinando el país del usuario de acuerdo a su dirección IP o mediante el parámetro HTTP HTTP_ACCEPT_LANGUAGE el cual indica el idioma de preferencia del usuario.




Generación de la imagen
En la generación de la imagen se van a aplicar las siguientes transformaciones:
  1. Disminuir el espaciado entre caracteres, incluso traslapando algunos caracteres.
  2. Variación de tamaños de caracteres.
  3. Diversidad de tipografías (pero utilizando una sola tipografía por cada imágen).
  4. Alternación de color de textos.
  5. Ondulación del texto en los ejes X e Y.

1. Disminuir el espaciado entre caracteres
Las tipografías están construidas para mantener una sepación específica entre cada caracter para aumentar la legibilidad. Sin embargo, esto es precisamente lo que no se debe hacer al generar un captcha. Al juntar los caracteres y permitir su traslape se dificulta el proceso de separación caractar a caracter, esto es muy importante porque la extracción de cada uno de los caracteres es uno de los pasos escenciales en el proceso de decodificación de captchas.

En las siguientes imagenes vemos cómo el texto de la derecha tiene sus caracteres muy juntos.
Para lograr disminuir el espaciado entre cada caracter tuve que imprimir en la imagen caracter por caracter con el siguiente código:

// Texto a imprimir
$text = getDictionaryCaptchaText($extended);
// Definiciones de latipografía a utilizar
// - font: archivo TTF
// - condensation: Cantidad de pixeles que se quitará entre cada caracter
$fontcfg = $fonts[mt_rand(0, sizeof($fonts)-1)];
$fontsize = 32;
$x = 20;
for ($i=0; $i<=6; $i++) {
$coords = imagettftext($im, $fontsize, 0,
$x, 47, $fg_color, 'fonts/'.$fontcfg['font'], substr($text, $i, 1));
$x += ($coords[2]-$x)-$fontcfg['condensation'];
}





2. Variación de tamaños de caracteres
La primera alteración a realizar es el cambio del tamaño de cada caracter como se muestra a continuación:

// Texto a imprimir
$text = getDictionaryCaptchaText($extended);

// Definiciones de latipografía a utilizar
// - font: archivo TTF
// - minSize: tamaño de fuente mínimo a utilizar
// - maxSize: tamaño de fuente máximo a utilizar
// - condensation: Cantidad de pixeles que se quitará entre cada caracter
$fontcfg = $fonts[mt_rand(0, sizeof($fonts)-1)];

$x = 20;
for ($i=0; $i<=6; $i++) {
$coords = imagettftext($im, rand($fontcfg['minSize'],$fontcfg['maxSize']), 0,
$x, 47, $fg_color, 'fonts/'.$fontcfg['font'], substr($text, $i, 1));
$x += ($coords[2]-$x)-$fontcfg['condensation'];
}


3. Diversidad de tipografías
Escogí un conjunto de tipografías que alterno entre cada generación. En principio quise alternar la tipografía al generar cada caracter, pero el resultado no era muy vistoso, por lo tanto ahora utilizo la misma tipografía para todos los caracteres:

No debemos olvidar que la mayoría de las tipografías que están instaladas en nuestros computadores están protegidas por copyright, por lo que buscando en internet descargué varias tipografías OpenSource o Freeware desde el sitio http://www.urbanfonts.com. Con un poco de paciencia se pueden encontrar tipografías que permiten generar captchas más originales:



4. Alternación de color de textos
Principalmente por un tema estético introduje alternación entre 3 colores posibles para el captcha. Algunos podrían tentarse de poner cada caracter en diferente color, pero esto sería un error porque facilitaría al decodificador separar cada caracter.


Para definir el color, defino un arreglo con los colores RGB y luego obtengo el color escogido al azar:

$colors = array(
array(27,78,181), // azul
array(22,163,35), // verde
array(214,36,7), // rojo
);

$color = $colors[mt_rand(0, sizeof($colors)-1)];
$fg_color = imagecolorallocate($im, $color[0], $color[1], $color[2]);



5. Ondulación del texto en los ejes X e Y Finalmente aplicamos una transformación al texto para en base a una función senoidal. Esto lo hacemos en el eje X e Y. Como una imágen vale más que mil palabras, en el siguiente ejemplo muestro el proceso de transformación: se comienza con una imagen plana (en color rojo) a la cual le aplico un filtro "wave" en el eje X (color verde) y en el eje Y (color azul). Una vez que apliquemos este filtro en ambos ejes obtenemos como resultado la imagen de la derecha (en color verde):

La generación de ondulación se realiza mediante la función imagecopy() que va desplazando linea por linea la imagen de acuerdo a una función senoidal. Esto se realiza en el eje X y luego en el eje Y. Se define una variable aleaoria $k para definir la "fase" o ángulo inicial de la ondulación. La forma de la onda senoidal se define por la amplitud (valor mas alto y bajo de la onda) y el período que define la "frecuencia":

// Genero ondas verticales (eje X)
$period = $scale*$periodoX;
$amplitude = $scale*$amplitudX;
$k = rand(0,100);
for ($i = 0;$i < ($width*$scale);$i++) {
imagecopy($im,$im,
$i-1, sin($k+$i/$period) * $amplitude,
$i,0,
1,$height*$scale);
}

// Genero ondas horizontales (eje Y)
$period = $scale*$periodoY;
$amplitude = $scale*$amplitudY;
$k = rand(0,100);
for ($i = 0;$i < ($height*$scale);$i++) {
imagecopy($im,$im,
sin($k+$i/$period) * $amplitude, $i-1,
0,$i,
$width*$scale,1);
}








Juntando las piezas

Una vez que se junten todos estos pasos y parametrizando algunas cosas obtenemos el siguiente resultado:


Como se pueden dar cuenta -por pura coincidencia- se generaron varias palabras potencialmente ofensivas que idealmente no debieran presentarse al usuario.


El programa final es el siguiente:
(recuerda descargar los archivos de tipografía desde algún sitio como por ejemplo http://www.urbanfonts.com)


<?php
/**
* Script para la generación de CAPTCHAS
*
* Autor: José Rodríguez jose.rodriguez at exec.cl
* http://joserodriguez.cl
* http://www.exec.cl
*
* En caso que hagas uso de este código o una variación,
* te pido que me lo hagas saber.
*
* Esta obra está licenciada bajo Creative Commons
* Reconocimiento - No comercial - Compartir bajo la misma licencia 3.0 Unported License.
* http://creativecommons.org/licenses/by-nc-sa/3.0/
*
*/



// Alto y ancho de la imagen
$width = 200;
$height = 66;

// Nombre de la variable de sesion
$session_var = "captcha";

// Colores
$colors = array(
array(27,78,181), // azul
array(22,163,35), // verde
array(214,36,7), // rojo
);

/**
* Configuración de tipografías
* - font: archivo TTF
* - condensation: cantidad de pixeles que se juntará cada caracter
* - minSize: tamaño minimo del texto
* - maxSize: tamaño máximo del texto
*/
$fonts = array(
array('font' => 'Danoisemedium.ttf','condensation' => 2, 'minSize' => 28, 'maxSize' => 40),
array('font' => 'HEINEKEN.TTF', 'condensation' => 2.5, 'minSize' => 24, 'maxSize' => 40),
array('font' => 'VeraSeBd.ttf', 'condensation' => 3.5, 'minSize' => 20, 'maxSize' => 33),
array('font' => 'VeraSe.ttf', 'condensation' => 4, 'minSize' => 26, 'maxSize' => 40),
array('font' => 'CrazyHarold.ttf', 'condensation' => 2, 'minSize' => 20, 'maxSize' => 28),
array('font' => 'Duality.ttf', 'condensation' => 2, 'minSize' => 28, 'maxSize' => 48),
/*
// Otras tipografías
array('font' => 'BeyondWonderland.ttf', 'condensation' => 3, 'minSize' => 28, 'maxSize' => 39),
array('font' => 'BennyBlanco.ttf', 'condensation' => 1, 'minSize' => 24, 'maxSize' => 30),
array('font' => 'freak.ttf', 'condensation' => 2, 'minSize' => 32, 'maxSize' => 54),
*/
);

// Configuración de ondulacion del texto
// Periodo y amplitud en ejes X e Y
$periodoY = 15;
$amplitudY = 16;
$periodoX = 12;
$amplitudX = 4;

/**
* Factor de resolución con que se trabajará internamente
* Se prefiere manipular la imagen al doble de su tamaño
* para evitar pérdida de calidad al aplicar filtro wave.
* Valores posibles: 1, 2 o 3.
*/
$scale = 2;

// Utilizar palabras inexistentes?
$extended = false;


// Permite habilitar depurado
$debug = false;
$ini = microtime(true);












session_start();



// Creo la imagen
$im = imagecreatetruecolor($width*$scale, $height*$scale);
$bg_color = imagecolorallocate($im, 255, 255, 255);
$color = $colors[mt_rand(0, sizeof($colors)-1)];
$fg_color = imagecolorallocate($im, $color[0], $color[1], $color[2]);
imagefilledrectangle($im, 0, 0, $width*$scale, $height*$scale, $bg_color);




// Genero el texto, caracter por caracter
$text = getDictionaryCaptchaText($extended);
$fontcfg = $fonts[mt_rand(0, sizeof($fonts)-1)];
$x = 20*$scale;
for ($i=0; $i<=6; $i++) {
$coords = imagettftext($im, rand($fontcfg['minSize'],$fontcfg['maxSize'])*$scale, 0,
$x, 47*$scale, $fg_color, 'fonts/'.$fontcfg['font'], substr($text, $i, 1));
$x += ($coords[2]-$x)-$fontcfg['condensation']*$scale;
}




// Genero ondas verticales (eje X)
$period = $scale*$periodoX;
$amplitude = $scale*$amplitudX;
$k = rand(0,100);
for ($i = 0;$i < ($width*$scale);$i++) {
imagecopy($im,$im,
$i-1, sin($k+$i/$period) * $amplitude,
$i,0,
1,$height*$scale);
}

// Genero ondas horizontales (eje Y)
$period = $scale*$periodoY;
$amplitude = $scale*$amplitudY;
$k = rand(0,100);
for ($i = 0;$i < ($height*$scale);$i++) {
imagecopy($im,$im,
sin($k+$i/$period) * $amplitude, $i-1,
0,$i,
$width*$scale,1);
}




// Reduzco el tamaño de la imagen
$imResampled = imagecreatetruecolor($width, $height);
imagecopyresampled ($imResampled,$im,0,0,0,0,$width, $height,$width*$scale,$height*$scale);
imagedestroy($im);


// Guardo el texto en sesión
$_SESSION[$session_var] = $text;


if ($debug) {
imagestring($imResampled, 1, 1, $height-8, "$text k:$k ".$fontcfg['font'].' '.round((microtime(true)-$ini)*1000), $fg_color);
}


header("Content-type: image/jpeg");
imagejpeg($imResampled, null, 80);

// Limpieza
imagedestroy($imResampled);





/**
* Retorna un texto de diccionario aleatorio
*/
function getDictionaryCaptchaText($extended = false) {
$fp = fopen("words-es.txt", "r");
$linea = rand(0, (filesize("words-es.txt")/8)-1);
fseek($fp, 8*$linea);
$text = trim(fgets($fp));
fclose($fp);


// Cambio vocales al azar
if ($extended) {
$text = str_split($text, 1);
$vocals = array('a','e','i','o','u');
foreach ($text as $i => $char) {
if (mt_rand(0,1) && in_array($char, $vocals)) {
$text[$i] = $vocals[mt_rand(0,4)];
}
}
$text = implode('', $text);
}

return $text;
}







/**
* Retorna un texto aleatorio
*/
function getCaptchaText() {
$length = rand(5,7);
$consonants = "abcdefghijlmnopqrstvwyz";
$vocals = "aeiou";
$text = "";
$vocal = rand(0,1);
for ($i=0; $i<$length; $i++) {
if ($vocal) {
$text .= substr($vocals, mt_rand(0, 4), 1);
} else {
$text .= substr($consonants, mt_rand(0, 22), 1);
}
$vocal = !$vocal;
}
return $text;
}



?>

miércoles, 28 de mayo de 2008

Creación de un CAPTCHA (o test de turing)

A esta altura, la mayoría de nosotros nos hemos encontrado en situaciones que cuando ingresamos información en un formulario para registrarnos en algún servicio -como por ejemplo crear una casilla de correo o descargar un archivo de servicios como rapidshare- el sitio nos pide que escribamos el texto de una imagen que vemos en pantalla.


Estos mecanismos de validación permiten -en teoría- distinguir entre humanos y computadores, y así prevenir abusos como por ejemplo la publicación de spams en los comentarios de blogs y la creación masiva de casillas de correo para spamming.

En los últimos años han proliferado diversos mecanismos de CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart), como por ejemplo resolver un problema matemático, indicar el animal que ves es una fotografía o -el más popular- identificar el texto que ves en una imágen.

Una de las desventajas de los CAPTCHAs es que tienen serias deficiencias de accesibilidad. Además existen algunos casos extremadamente molestos para el usuario como el siguiente ejemplo utilizado en Rapidshare:

Debido a que la mayoria de los desarrollos de software en que he participado son sistemas cerrados o que no son un potencial blanco de ataques de spamming, no me he visto en la necesidad de implementar mecanismos de CAPTCHA, sin embargo, haciendo algunos experimentos he llegado a implementar un sistema sencillo en PHP que les describo a continuación.


Implementación de CAPTCHA en PHP
En este ejercicio voy a generar un CAPTCHA que le presente al usuario una imagen con letras que el usuario deberá escribir en un formulario. Debido al uso de imágenes, me apoyaré en la biblioteca GD, por lo que para estos ejemplos PHP debe tener soporte para GD.

Mi objetivo es generar un captcha que nos brinde un nivel de seguridad mínima y en ningún caso desarrollar un sistema infalible. Detrás de la generación (y decodificación) de los captchas existen teorías de procesamiento de imágenes y reconocimiento de caracteres que no son parte de este artículo.

El modelo general consiste en construir un script PHP que retorne una imagen con un texto aleatorio. El texto original será almacenado en una variable de sesión. Al procesar el formulario se deberá comparar el texto ingresado por el usuario con el texto almacenado en la variable de sesión.


Versión 1
Mi primera versión consiste en una implementación básica sólo para desarrollar la funcionalidad mínima del sistema:
<?php

// Alto y ancho de la imagen
$width = 250;
$height = 60;
// Tipografia
$font = "arial.ttf";
// Nombre de la variable de sesion
$session_var = "captcha";




// Genero la imagen
$im = imagecreatetruecolor($width, $height);
$bg_color = imagecolorallocate($im, 255, 255, 255);
$fg_color = imagecolorallocate($im, 0, 0, 0);
imagefilledrectangle($im, 0, 0, $width, $height, $bg_color);

// Genero el texto
$text = "secret";
imagettftext($im, 20, 0, 11, 21, $fg_color, $font, $text);

// Guardo el texto en sesión
session_start();
$_SESSION[$session_var] = $text;


// Genero la imagen
header("Content-type: image/png");
imagepng($im);

// Limpieza
imagedestroy($im);

?>

Este script genera una imagen de 250x60 pixeles, con color de fondo blango y texto arial negro. El texto utilizado en la imagen se guarda en la variable de sesión captcha. El resultado de este script es la siguiente imagen:

En el script que procesa el formulario, basta comparar el valor de $_SESSION['captcha'] con el texto ingresado en el formulario para confirmar que el captcha haya sido ingresado satisfactoriamente. Para reducir la tasa de error, es posible quitar los eventuales espacios en blanco y convertir el texto a minúculas para hacer una comparación case-insensitive.

Ahora que contamos con el código base podemos continuar mejorando nuestro script.


Versión 2
Las mejoras que introduciremos en esta versión son las siguientes:
  1. Generación de un texto aleatorio.
  2. Generar texto con distintas tipografías.
  3. Agregar transformaciones simples al texto.
  4. Aumentar el tamaño y darle un color más amigable.

La generación de un texto aleatorio puede ser algo tan sencillo como extraer los primeros 6 caracteres de un valor MD5 como el siguiente: substr(md5(uniqid(),0,6) el cual genera un string con números y letras entre la "a" y la "f".

Sin embargo construiremos algo más elaborado donde podamos controlar los caracteres a generar e intentaremos que el texto sea medianamente pronunciable con el fin de ser más amigable con el usuario. Una manera simple de generar texto pronunciable es asegurándonos de tener suficientes vocales y quitando algunas letras poco usadas en el español como la "x" y la "k". Definí arbitrariamente la longitud del texto a 6 caracteres lo cual minimiza la generación de palabras mal vistas como "pene", "puta" o "malo" y da una cantidad de combinaciones de letras apropiada.

Para ello elaboré una función que retorna textos aleatorios intercalando una consonante y una vocal:
<?php

function getCaptchaText($length = 6) {
$consonants = "bcdfghjlmnpqrstvwyz";
$vocals = "aeiou";

$text = "";
$imax = $length/2;
for ($i=0; $i<$imax; $i++) {
$text .= substr($consonants, mt_rand(0, 18), 1);
$text .= substr($vocals, mt_rand(0, 4), 1);
}
return substr($text, 0, $length);
}

?>
Por lo tanto, en el script original reemplazamos:
$text = "secret";
por:
$text = getCaptchaText(6);
Con este cambio ahora generamos captchas como los siguientes:



Para hacer el captcha más complejo vamos a generar el texto con distintas tipografías, agregarle rotación independiente a cada letra y juntar (o condensar) las letras para que se produzca un traslape que hará más dificil su decodificación.

Extraje algunos archivos .ttf desde mi directorio C:\WINDOWS\Fonts y luego reemplacé:
$font = "arial.ttf";
por:
$fonts = array('timesbi.ttf', 'calibriz.ttf', 'cambriaz.ttf');
$font = $fonts[mt_rand(0, sizeof($fonts)-1)];
Aumentamos el tamaño, le agregamos color y ahora obtenemos las siguientes imagenes donde podemos apreciar el cambio de tipografía en cada ejecución:

Hasta este punto hemos generado un captcha básico que es extremadamente fácil de decodificar.

Para hacer la imágen mas compleja vamos a generar letra por letra. Cada letra tendrá una rotación distinta y acercaremos las letras para lograr un traslape.

Modificamos el código:
imagettftext($im, 20, 0, 11, 21, $fg_color, $font, $text);
por:
$x = 3;
for ($i=0; $i<=6; $i++) {
$angulo = rand(-12, 12);
$coords = imagettftext($im, 38, $angulo, $x, 47, $fg_color, $font, substr($text, $i, 1));
$x += ($coords[2]-$x) - 4;
}

Este código va generando letra por letra y en cada iteración va aumentando la coordenada X restandole algunos pixeles para lograr que el texto se traslape. La rotación de cada letra tendrá un valor comprendido entre el intervalo [-12,12].

Se deberá buscar el valor exacto de traslape con el fin de hacer difícil su decodificación, pero permitiendo que el texto sea legible. Un refinamiento de la generación de estos textos podría ser que cada letra se genere con una tipografía distinta.

Al aplicar estos cambios obtenemos las siguientes imagenes:


Una vez que juntemos todos estos cambios obtendremos el siguiente código:

<php


// Alto y ancho de la imagen
$width = 250;
$height = 65;
// Tipografia
$fonts = array('timesbi.ttf', 'calibriz.ttf', 'cambriaz.ttf');
// Nombre de la variable de sesion
$session_var = "captcha";
// Condensacion entre cada letra
$condensacion = 4;


// Genero la imagen
$im = imagecreatetruecolor($width, $height);
$bg_color = imagecolorallocate($im, 255, 255, 255);
$fg_color = imagecolorallocate($im, 33, 67, 165);
imagefilledrectangle($im, 0, 0, $width, $height, $bg_color);

// Genero el texto
$font = $fonts[mt_rand(0, sizeof($fonts)-1)];
$text = getCaptchaText(6);
$x = 3;
for ($i=0; $i<=6; $i++) {
$angulo = rand(-12, 12);
$coords = imagettftext($im, 38, $angulo, $x, 47, $fg_color, $font, substr($text, $i, 1));
$x += ($coords[2]-$x)-$condensacion;
}

// Guardo el texto en sesión
session_start();
$_SESSION[$session_var] = $text;

// Genero la imagen
header("Content-type: image/png");
imagepng($im);

// Limpieza
imagedestroy($im);

/**
* Retorna un texto aleatorio
*
* @param int $length Longitud del texto
* @return string Texto aleatorio
*/
function getCaptchaText($length = 6) {
$consonants = "bcdfghjlmnpqrstvwyz";
$vocals = "aeiou";
$text = "";
$imax = $length/2;
for ($i=0; $i<$imax; $i++) {
$text .= substr($consonants, mt_rand(0, 18), 1);
$text .= substr($vocals, mt_rand(0, 4), 1);
}
return substr($text, 0, $length);
}

?>



Versión 3
Para dificultar aún más la decodificación le aplicaremos a la imagen un filtro "wave" que genere ondulaciones al texto.

Adicionalmente para aumentar el "ruido" de la imagen la generaré como JPEG en vez de PNG.

Existen otras técnicas bastante eficientes para la mayoría de los casos que consiste en introducir imágenes de fondo y ruido a la imágen como por ejemplo líneas aleatorias o pintar pixeles al alzar. Sin embargo dichas técnicas disminuyen en gran medida la estética del captcha por lo que las descartaré para estos ejemplos.

Para la generación de ondas en la imágen, existen excelentes biblitecas gráficas que permiten hacer esto como por ejemplo ImageMagick. Sin embargo, para continuar con la simplicidad del script utilizaremos una combinación de funciones de GD y funciones trigonométricas. La función de a continuación está basada en un ejemplo publicado en el manual de PHP:

/**
* Filtro wave
*
* Obtenido desde ejemplo publicado en el manual de PHP:
* http://www.php.net/manual/en/function.imagecopy.php#72393
*
*/
function wave_region($img, $x, $y, $width, $height,$amplitude = 4.5,$period = 30) {
// Make a copy of the image twice the size
$mult = 2;
$img2 = imagecreatetruecolor($width * $mult, $height * $mult);
imagecopyresampled ($img2,$img,0,0,$x,$y,$width * $mult,$height * $mult,$width, $height);

// Wave it
$k = rand(-60,60);
for ($i = 0;$i < ($width * $mult);$i += 2) {
imagecopy($img2,$img2,
$x + $i - 2,$y + sin($k+$i / $period) * $amplitude,
$x + $i,$y,
2,($height * $mult));
}

// Resample it down again
imagecopyresampled ($img,$img2,$x,$y,0,0,$width, $height,$width * $mult,$height * $mult);
imagedestroy($img2);
}


Esta función la llamaremos inmediatamente después de generar el texto con los siguientes argumentos:
// Aplico filtros
wave_region($im, 0, 0, $width, $height, mt_rand(7, 22), mt_rand(25,40));
Una vez que apliquemos este cambio se generarán las siguientes imágenes de referencia:


Al juntar estos últimos cambios y ajustar el tamaño definitivo de la imágen obtendremos el siguiente código:

<php



// Alto y ancho de la imagen
$width = 180;
$height = 65;
// Tipografia
$fonts = array('timesbi.ttf', 'calibriz.ttf', 'cambriaz.ttf');
// Nombre de la variable de sesion
$session_var = "captcha";
// Condensacion entre cada letra
$condensacion = 4;



// Genero la imagen
$im = imagecreatetruecolor($width, $height);
$bg_color = imagecolorallocate($im, 255, 255, 255);
$fg_color = imagecolorallocate($im, 33, 67, 165);
imagefilledrectangle($im, 0, 0, $width, $height, $bg_color);

// Genero el texto
$font = $fonts[mt_rand(0, sizeof($fonts)-1)];
$text = getCaptchaText(6);

//$text = "captcha"; $font="cambriaz.ttf"; $angulo = 0;

$x = 3;
for ($i=0; $i<=6; $i++) {
$angulo = rand(-12, 12);
$coords = imagettftext($im, 38, $angulo, $x, 47, $fg_color, $font, substr($text, $i, 1));
$x += ($coords[2]-$x)-$condensacion;
}

// Aplico filtros
wave_region($im, 0, 0, $width, $height, mt_rand(7, 22), mt_rand(25,40));

// Guardo el texto en sesión
session_start();
$_SESSION[$session_var] = $text;

// Genero la imagen
header("Content-type: image/jpeg");
imagejpeg($im);

// Limpieza
imagedestroy($im);

/**
* Retorna un texto aleatorio
*
* @param int $length Longitud del texto
* @return string Texto aleatorio
*/
function getCaptchaText($length = 6) {
$consonants = "bcdfghjlmnpqrstvwyz";
$vocals = "aeiou";
$text = "";
$imax = $length/2;
for ($i=0; $i<$imax; $i++) {
$text .= substr($consonants, mt_rand(0, 18), 1);
$text .= substr($vocals, mt_rand(0, 4), 1);
}
return substr($text, 0, $length);
}

/**
* Filtro wave
*
* Obtenido desde ejemplo publicado en el manual de PHP:
* http://www.php.net/manual/en/function.imagecopy.php#72393
*
*/
function wave_region($img, $x, $y, $width, $height,$amplitude = 4.5,$period = 30) {
// Make a copy of the image twice the size
$mult = 2;
$img2 = imagecreatetruecolor($width * $mult, $height * $mult);
imagecopyresampled ($img2,$img,0,0,$x,$y,$width * $mult,$height * $mult,$width, $height);

// Wave it
$k = rand(-60,60);
for ($i = 0;$i < ($width * $mult);$i += 2) {
imagecopy($img2,$img2,
$x + $i - 2,$y + sin($k+$i / $period) * $amplitude,
$x + $i,$y,
2,($height * $mult));
}

// Resample it down again
imagecopyresampled ($img,$img2,$x,$y,0,0,$width, $height,$width * $mult,$height * $mult);
imagedestroy($img2);
}

?>

La evolución del captcha construido fue la siguiente:

Como mejoras a este captcha se podría realizar una seleccón mas exuastiva de las tipografías utilizadas, se le podría añadir una imagen de fondo aleatoria, lineas aleatorias o ruido.

En el siguiente artículo vamos a optimizar el código y aumentar la complejidad del captcha sin perder su estética (es decir no le incorporaré lineas aleatorias, imágenes de fondo, etc.) de una manera similar a los captchas utilizados por Google.

Actualizado el día domingo 1 de junio de 2008.

miércoles, 21 de mayo de 2008

Protección básica contra Cross-site scripting (XSS)

Los ataques Cross-site scripting (XSS) o de HTML injection pueden ser prevenidos de manera bastante simple tomando algunas medidas de seguridad básicas que debemos tener siempre presentes.

Este tipo de ataque consiste en incrustar código HTML en un sitio web con el objetivo de poder ejecutar código Javascript arbitraro o incluir código HTML con el fin de -por ejemplo- realizar ataques de phishing.

La preveción de este tipo de ataque se realiza filtrando la infomación que se ingresa y publica en nuestro sitio, por lo que tenemos dos puntos de control:
  • Filtrar datos ingresados
  • Escapar datos publicados en la WEB

Filtrar datos ingresados
La media de prevención más sencilla es impedir que los usuarios del sitio puedan publicar información, como por ejemplo cuando se hacen comentarios en un blog o en formularos de contacto.

En caso que el sitio deba permitir el ingreso de información, se debe definir si se aceptará código HTML. En caso que se opte por no aceptar código HTML será necesario filtrar todo el contenido recibido desde formularios para quitar eventuales tags HTML que ingrese el usuario. Esto se puede realizar con la siguiente función:
$comentario = strip_tags($comentario);
La función strip_tags() no realiza validación de que el código HTML esté bien formado, por lo que potencialmente se podría perder información -mal- ingresada por el usuario. Se puede realizar una validación más apropiada por medio de tidy.

En caso que sea necesario permitir el ingreso de código HTML, se deberán restringir los tags HTML que serán permitidos. Usualmente se permiten los tags de texto básico como <p>, <br>, <i>, <b>, <strong>, <em>, <blockquote> y tags de listas como <ul>, <ol> y <li>. Especialmente se debe evitar la inclusión de tags HTML que permitan la inclusión de contenidos externos como por ejemplo <script>, <frame>, <iframe>, <object> y <a>.

El filtrado de los tags HTML permitidos se puede realizar con el siguiente comando:
$comentario = strip_tags($comentario, "<p><br><strong><em>");
Sin embargo, nuevamente la función strip_tags() puede presentar problemas porque los tags admitidos podrían contener atributos peligrosos como onload y onmouseover. Por lo que se debería recurrir a una combinación de tidy y expresiones regulares.

Las tareas de filtrado se pueden enfrentar de dos maneras: Manteniendo una lista negra de tags y atributos no permitidos o manteniendo una lista blanca con los tags y atributos permitidos. En vista de evitar problemas de seguridad, es más recomendable mantener una lista blanca que contenga los elementos admitidos ya que es mucho menos grave restringir el ingreso de un dato inofensivo (falso positivo) que permitir el ingreso de un dato potencialmente peligroso.


Escapar datos ingresados
Como segunda medida de contención se puede agregar validación antes de publicar información en la WEB.

Se debe considerar que la información que ingresa a nuestras bases de datos (y archivos de caché) es introducida por diversos medios, como por ejemplo WebServices, migraciones de datos o SQL Injection, por lo tanto aunque se mantenga absoluta seguridad en nuestros formularios WEB, siempre existe la posibilidad de contar con datos peligrosos. Debido a esto, seimpre es recomendable filtrar la información que se publica en nuestro sitio.

En primer lugar debemos distinguir si la información que publicamos puede contener HTML o no.

En general la gran mayoría de la información que se publica en la web -como por ejemplo nombres, fechas, títulos- no contiene código HTML. En estos casos la información a imprimir en una página web debe ser escapada para reemplazar los caracteres especiales por sus entidades respectivas, esto se realiza con el siguiente comando:
echo htmlspecialchars($nombre);
Esta función tiene la ventaja que imprime correctamente textos como "Barnes & Noble" y "A < B < C" que en caso de no ser escapados no se visualizarían correctamente.

En el caso de publicar contenidos con HTML, se debe aplicar la misma función de filtrado que se utiliza al momento de ingresar datos.

Con estas simples medidas se aumenta en gran medida el nivel de seguridad ante ataques XSS.

lunes, 5 de mayo de 2008

Javascript dentro de HTML

Para incrustar código Javascript dentro de HTML se debe utilizar la siguiente etiqueta HTML:
<script type="text/javascript">
// Aquí va el código...
</script>
Sin embargo, en la actualidad es mejor no utilizar Javascript incrustado sino que más bien utilizar archivos Javascript externos. Esto permite separar el contenido de la programación y además permite una navegación más rápida ya que se pueden guardar en cache los archivos .js externos. El código para referenciar archivos Javascript es el siguiente:
<script type="text/javascript" src="filename.js"></script>
El sentido común nos dice que las referencias a archivos Javascript debieran realizarse en el encabezado del documeno HTML, es decir, dentro de los tags HEAD.

Es mejor hacer lo contrario: por un tema de performance se recomienda -en la medida de lo posible- poner dichas referencias al final del documento HTML ya que esto acelera el proceso de rendering del documento HTML.

Graceful degradation y Unobtrusive Javascript
Cuando se construyen sitios que utilicen Javascript es importante tener en mente mantener la semántica del sitio web y ser conscientes que no todos nuestros visitantes tendrán Javascript habilitado, por ejemplo bots de los motores de búsqeda, usuarios que han deshabilitado javascript por motivos de seguridad, usuarios de algunos dispositivos móviles, microformatos, herramientas semánticas como Yahoo pipes, clientes en modo de sólo texto o -el problema más común- usuarios que visitan nuestro sitio con un navegador que es pacialmente incompatible con nuestro sitio ya sea porque es un navegador antiguo o una versión más moderna. Todos estos usuarios debieran poder navegar por nuestro sitio sin mayores problemas.

El concepto de Unobtrusive Javascript consiste en seguir las buenas prácticas en la programación de Javascript y poder separar el comportamiento (código javascript) del contenido del sitio, es decir que el código Javascript sea lo menos invasivo posible.

El concepto de Graceful degradation consiste en que un sitio web tenga la capacidad de continuar operando con clientes incompatibles proveyendo un nivel de servicios reducido en vez de que este no pueda operar.

Por ejemplo, a continuación se muestran algunos links que hacen uso de javascript de manera incorrecta:
<a href="javascript:link('products')">Productos</a>

<a href="javascript:submitForm('form1')">Enviar formulario</a>

<a href="products.html" onclick="check()">Productos</a>
El primer ejemplo utiliza un llamado a la función link(). Este código no funcionará en navegadores sin soporte a Javascript y hace que sea muy dificil que un robot pueda navegar por el sitio para indexarlo, además atenta contra la usabilidad como por ejemplo hacer clic con el botón central del mouse para abrir el link en una nueva pestaña no funcionará. Este es el caso de Falabella donde no puedo abrir las fichas de productos en distintas pestañas para poder compararlos.

El segundo ejemplo realiza el envío de un formulario mediante código Javascript cuando lo semánticamente correcto habría sido utilizar el elemento HTML <input type="submit"> y en caso de requerir validación del formulario agregar el evento onsubmit al formulario.

El tercer ejemplo ejecuta una función de validación al momento de hacer clic en el link mediante el atributo onclick.

El código correcto para estos 3 ejemplos sería:
<a href="/show/products">Productos</a>

<input type="submmit" id="submitForm" value="Enviar formulario"/>
<a href="products.html" id="linkProducts">Productos</a>
En el primer ejemplo simplemente se reemplazó el código javascript por un link. Se pueden utilizar técnicas como mod_rewrite de Apache para mantener links amigables. En el segundo y tercer caso se utiliza HTML estándar y se referencian los elementos mediante el atributo id mediante el cual podrán ser manipulados por javascript.

Finalmente, agregamos los event handler mediante el siguiente código Javascript:
document.getElementById('linkProducts').onclick = check;
document.getElementById('form1').onsubmit = checkForm;
(NOTA: estos son ejemplos Javascript simplificados que contienen errores de malas práctias como por ejemplo definir directamente los eventos en vez de utilizar una función para agregar eventos en cascada)

sábado, 3 de mayo de 2008

SSH Tunneling

Un problema común
Uno de los grandes problema con que lidio lidiaba en los servidores de producción, es que comúnmente tienen reglas de firewall estrictas que sólo permiten el tráfico TCP/IP por el puerto del servicio que están ejecutando (servidor web, base de datos, FTP, etc.) y el puerto de SSH (22) con fines de administración.

Además en la mayoría de los casos el acceso SSH está limitado sólo desde algunos de los equipos de la red local y en ningún caso permiten el acceso SSH desde Internet.

Esta situación hace muy difícil los procesos de administración, actualización y solución de contingencias ya que la única manera de acceder al servidor era ir físicamente al datacenter o intermediar telefónicamente con un operador que con suerte sabe ejecutar los comandos de shell cd y ls.

Otro problema que se nos puede presentar es la necesidad de encriptar conexiones de protocolos inseguros, como por ejemplo una conexión FTP.


La solución
Una solución para poder acceder a sistemas remotos es estableciendo un Tunnleing SSH, el cual consiste en redirigir (forward) las conexiones TCP dirigidas hacia un puerto específico hacia otro host por medio de la conexión encriptada de SSH. No soporta redirección de paquetes UDP.

En los siguientes ejemplos utilizaremos como ejemplo las siguientes direcciones IP:
  • Servidor (con acceso restringido): 1.1.1.1
  • Equipo de administración (tiene acceso al servidor): 1.1.1.2
  • Equipo local (sin acceso al servidor): 1.1.1.3
El tunneling SSH tiene dos modalidades de redirección: remote port forwarding que permite redirigir conexiones TCP desde el equipo remoto y local port forwarding que permite redirigir las conexiones TCP desde el equipo local.

En shell, la sintaxis tradicional para establecer una conexión SSH es la siguiente:
ssh user@host
La sintaxis para establecer un tunneling consiste en indicar adicionalmente:
  1. El tipo de redirección: "R" para remote port forwarding y "L" para local port forwarding.
  2. El puerto que se redireccionará.
  3. El host y puerto hacia el cual se redigirán los paquetes TCP
Quedando en:
ssh user@host -R port:host:hostport
ssh user@host -L port:host:hostport
En los siguientes ejemplos se utilizará para redirigir conecciones SSH, pero esta técnica sirve para redireccionar cualquier servicio basado en TCP.


Local port forwarding
Si quisieramos acceder por SSH al servidor 1.1.1.1 desde nuestro equipo local 1.1.1.3, el cual no tiene acceso directo, podríamos establecer inicialmente una conexión hacia el equipo de administración 1.1.1.2 y desde ahí establecer un port forwarding hacia el servidor 1.1.1.1 tal como se muestra en el siguiente esquema:


Para eso, desde nuestro equipo local 1.1.1.1 ejecutamos el siguiente comando:
ssh user@1.1.1.2 -L 2222:1.1.1.1:22
Una vez conectado al equipo remoto 1.1.1.2 todas las conecciones desde nuestro equipo local al puerto 2222 serán redirigidas al puerto 22 del servidor 1.1.1.1, con lo cual hemos logrado tener acceso al servidor remoto mediante el siguiente comando:
ssh user@localhost -p 2222
Incluso desde otro equipo que tenga acceso a 1.1.1.3 podrá conectarse al servidor 1.1.1.1 mediante SSH:
ssh user@1.1.1.3 -p 2222


Remote port forwarding
Otra modalidad para acceder al servidor 1.1.1.1 es establecer la conexión opuesta desde el equipo de administración 1.1.1.2 hacia nuestro equipo local 1.1.1.3 como se ilustra a continuación:


En este caso desde el equipo 1.1.1.2 se establecerá un remote port forwarding mediante el siguiente comando:
ssh user@1.1.1.3 -R 3333:1.1.1.1:22
Una vez conectado al nuestro equipo 1.1.1.3 todas las conecciones desde nuestro equipo local al puerto 3333 serán redirigidas al puerto 22 del servidor 1.1.1.1, con lo cual hemos logrado tener acceso al servidor remoto mediante el siguiente comando ejecutado desde 1.1.1.3:
ssh user@localhost -p 3333


SSH desde Windows
Las técnicas de tunneling también están disponibles para Windows, por ejemplo se puede utilizar el programa gratuito putty. La configuración de tunneling se realiza en la siguiente ventana de configuración:




Aplicaciones
Esta técnica nos permite acceder a casi cualquier servicio remoto, como por ejemplo la base de datos del backend o a un sitio web disponible sólo para la intranet.

Además como la conexión se realiza sobre SSH, todas las comunicaciones se realizan mediante un canal encriptado, lo que nos permite dar una mayor seguridad a conexiones vía FTP o a una base de datos.

La técnica de port forwarding es bastante flexible y poderosa, por ejemplo en algunas situcaciones he llegado a realizar hasta 3 tunnelings anidados para lograr alcanzar servidores que están extremadamente resguardados.

jueves, 24 de abril de 2008

UNICODE

Este es un interesante artículo publicado en el blog oficial de Google para desarrolladores en español sobre la historia de la codificación UNICODE hasta llegar a UTF-8.
El mayor problema con el que os vais a encontrar es la falta de conocimientos sobre internacionalización. Es increíble como muchos programadores, por lo demás perfectamente capacitados, parecen convertirse en Paris Hilton cuando hablan de internacionalización. Eso si llegan a hablar... Vamos a empezar sentando las bases de lo que es un juego de carácteres (charset) y una codificación (encoding). El juego de carácteres es la tabla que traduce de un número a un "carácter" (o para ser más precisos un "codepoint"), y la codificación es el algoritmo que hemos seguido para guardar ese número.
Los invito a seguir leyendo internacionalización I - Unicode en el blog Programa con Google.

domingo, 20 de abril de 2008

Hashing en PHP

Una de las técnicas más básicas y simples relativas a la seguridad de información es el hashing o codificación de datos sensibles (ej.: contraseñas).

Los algoritmos de hashing permiten generar un hash (o "huella") a partir de un string y tienen la característica principal de que no es posible obtener el string original a partir del hash generado.

La longitud del hash es fija para cada algoritmo por lo que no importa si la entrada tiene una longitud de 1 byte o de varios gigas, el hash tendá siempre la misma longitud. Una de las características básicas de todo hash es que el menor cambio en el string de entrada implica un gran cambio en el hash generado.

Los algoritmos de hash más populares son: MD5, SHA-1 y SHA-2. En la siguiente tabla muestro la longitud de algunos hash y un ejemplo al cifrar el texto "123":

AlgoritmoBitsEjemplo
CRC3232-2008521774 (representado como int32)
MD4128c58cda49f00748a3bc0fcfa511d516cb
MD5128202cb962ac59075b964b07152d234b70
SHA-119240bd001563085fc35165329ea1ff5c5ecbdbbeef
SHA-256 (SHA-2)256a665a45920422f9d417e4867efdc4fb8 a04a1f3fff1fa07e998e86f7f7a27ae3
SHA-512 (SHA-2)5123c9909afec25354d551dae21590bb26e 38d53f2173b8d3dc3eee4c047e7ab1c1 eb8b85103e3be7ba613b31bb5c9c3621 4dc9f14a42fd7a2fdb84856bca5c44c2
RIPEMD-160160e3431a8e0adbf96fd140103dc6f63a3f8fa343ab
HAVAL-256256e3891cb6fd1a883a1ae723f13ba336f5 86fa8c10506c4799c209d10113675bc1
TIGER-128128fe14a796bb0768a83398e6935842229b
TIGER-160160fe14a796bb0768a83398e6935842229bbef2eeb0

Como información adicional puedes ver el siguiente sitio que permite generar hashes para múltiples algotirmos.

Debido a que la longitud del hash es de tamaño fijo, existe la posibilidad de que ocurran colisiones, las cuales son extremadamente improblables pero no imposibles, por ejemplo en MD5 teóricamente se pueden generar 3.4 × 1038 (2128) valores diferentes de hash y en SHA-256 son casi 1.2 × 1077 (2256) valores diferentes.


Aplicaciones

Entre las aplicaciones más comunes de hash se pueden contar las siguientes:
  • Almacenamiento de contraseñas.
  • Generación de tokens (ej: para sesión de usuarios, cookies).
  • Implementación de arreglos asociativos.
  • Checksums (ej: al transmitir información como un torrent, un instalador o un archivo .ISO).
  • Digest para firma electrónica (ej: xmlsignature).
A continuación me referiré al almacenamiento de contraseñas.



Almacenamiento de contraseñas mediante HASH
Al guardar las contraseñas codificadas mediante un hash, se imposibilita conocer la contraseña original tanto para un eventual atacante como también para un administrador.

Otro alcance de esta técnica es para que -por políticas de seguridad, cumplimiento de estándares de certificación o por cumplimiento de los términos de uso- la organización no maneje las contraseñas originales de sus usuarios.

El mecanismo más común para autenticar usuarios es por medio de un login y password, lo que implica que en la base de datos exista una tabla que llamaremos usuarios que contiene al menos las columnas usua_login y usua_password, dando como resultado la siguiente tabla de usuarios:

usua_loginusua_password
jessicap7q,f2
pedrobonsai
rodrigoro123

Como podemos darnos cuenta, cualquiera que tenga acceso a la base de datos podrá conocer la contraseña de los usuarios. Existen muchas técnicas para lograr tener acceso a la base de datos, como por ejemplo errores documentados de software, SQL injection, errores en páginas de inicio de sesión, programas como PhpMyAdmin que no estén lo suficientemente resguardados, usuarios al interior de la organización o ingeniería social.

Por lo tanto el primer nivel de seguridad consiste en guardar las contraseñan cifradas con algún algoritmo de hashing. En la actualidad el algoritmo más popular es MD5, sin embargo desde hace algunos años se han reportado ataques satisfactorios a este algoritmo. Por lo tanto recomiendo utilizar algoritmos de mayor dureza como SHA-1 o SHA-256 los cuales ya están disponibles en muchas herramientas desarrollo. A continuación presento una tabla de las funciones en PHP para codificar en algunos de los algoritmos de hashing:

MD5md5($txt)
SHA-1sha1($txt)
sha-256mhash(MHASH_SHA256, $txt)
tiger-128mhash(MHASH_TIGER128, $txt)
haval-256mhash(MHASH_HAVAL256, $txt)
La función hash() requiere tener PHP con el módulo mhash.

En estos ejemplos utilizaré SHA-1, por lo que la tabla usuarios quedará con los siguientes datos:

usua_loginusua_password
jessica1f9004142372e078dbda694238d82a192760c170
pedro9562384bbab4d406ee06012638ca65f403d1bd1f
rodrigo0f8342a2ed2797cb3d19b926d5c98db79b5a8708

Al guardar los datos codificados se debe considerar que las rutinas de inicio de sesión y de gestión de usuarios deben ser intermediadas por un proceso que convierta la contraseña original en la contraseña codificada, como por ejemplo el siguiente código:
$password = "bonsai";
$hash = sha1($password);
A este nivel hemos logrado que los password se guarden cifrados en la base de datos y sea (teóricamente) imposible conocer los passwords originales. Sin embargo, un atacante que logre acceder a la base de datos aún tendrá posibilidad de conocer algunas de las contraseñas.

Si buscamos en google por la contraseña codificada de Pedro encontraremos inmediatamente que dicho hash corresponde al texto "bonasi". La contraseña de Pedro (bonsai) es conocida como una palabra de diccionario, es decir una palabra común. En internet existen muchos sitios donde tienen indexados millones de palabras con su correspondiente hash -principalmente MD5- por lo que mediante ataques por diccionario es posible obtener las contraseñas de los usuarios más descuidados.

Jessica y en menor medida Rodrigo utilizaron contraseñas que no son palabras de diccionario ("p7q,f2" y "ro123" respectivamente) por lo que es más dificil descubrir sus contraseñas originales.

Lamentablemente, muchos desarrolladores llegan sólo hasta este nivel y descuidan agregar un mayor nivel de seguridad el cual se puede realizar con un cambio muy simple que describo a continuación.


Almacenamiento de contraseñas mediante HMAC
Un algoritmo HMAC no es más que un algoritmo HASH al cual -antes de codificar el texto original- se le añade un texto adicional (key o clave secreta).

En un principio realizabamos la codificacion de la contraseña mediante el siguiente código:
$password = "bonsai";
$hash = sha1($password);
En este ejemplo vamos a escoger la clave secreta "xy45" y vamos a codificar las contraseñas utilizando la siguiente variación:
$password = "bonsai";
$key = "xy45";
$hash = sha1($key.$password);
Es importante que la clave secreta sea lo suficientemente larga y compleja. Recomiendo que la longitud sea de 8 caracteres y se utilicen combinaciones de mayusculas, minusculas, números y caracteres de puntuación. Opcionalmente, en vez de una simple concatenación, PHP provee la función hash_hmac() especialmente diseñada para estos efectos.

Gracias a esta técnica obtendremos la siguiente tabla de usuarios:

usua_loginusua_password
jessica25062347f18d9e8a85849aade94b38beaea54653
pedro742cc2861f5c611452bf7e78c8bcbc3899851a56
rodrigo44f789c9642a64513e8682a7dea072dedae85cd9

Estas contraseñas ahora son invulnerables a un ataque directo de diccionario y por lo tanto brindan un mayor nivel de seguridad. Sin embargo, aún es posible obtener las contraseñas originales por medio de un ataque de colisión.

Si el atacante conoce la clave secreta HMAC, le bastaría con crear un pequeño programa que genere las contraseñas en HMAC a partir de un diccionario de palabras para luego compararlas con las contraseñas almacendas en la base de datos. Si no conoce la clave secreta HMAC, basta con cambiar -por medio de la aplicación- varias veces la contraseña de algún usuario del sistema y luego comparar el string HMAC generado con el del resto de usuarios.

Almacenamiento de contraseñas mediante HMAC con distinta clave secreta
La solución a este tipo de ataque consiste en que la contraseña de cada usuario se codifique con una clave secreta HMAC diferente, como por ejemplo el username, la casilla de correos del usuario o agún otro dato variable. Nuestra nueva función de codificación quedaría de la siguiente manera:
$username = "pedro";
$password = "bonsai";
$key = "xy45";
$hash = sha1($key.$username.$password);
En este nuevo escenario aún es posible que un atacante obtenga algún password por colisión pero tendrá que generar un diccionario de claves usuario por usuario.

Otra ventaja -o desventaja según cómo se le mire- es que si se cambia el login del usuario, el password se invalidará. Por lo que en la eventualidad de cambiar el login, se deberá volver a generar el password.



Medidas adicionales de seguridad
Se recomienda mantener oculta la clave secreta HMAC e implementar mecanismos para limitar el registro masivo de usuarios y cambios masivos de contraseña.

También se pueden implementar -a nivel de software- políticas de seguridad a la gestión de contraseñas para no permitir que los usuarios utilicen palabras de diccionario o contraseñas demasiado cortas.

jueves, 10 de abril de 2008

Conversión de UNICODE y LATIN1 en PHP 5

La principal innovación de PHP 6 será el soporte nativo a UNICODE, sin embargo aún queda un largo camino.

Algunos módulos de PHP 5 internamente ya operan con UNICODE como es el caso de DOM que está desarrollado sobre libxml2, esta característica brinda un gran potencial pero a la vez da muchos dolores de cabeza ya que hay que hacer convivir datos codificados usualmente en ISO-8859-1 (LATIN1) y UTF-8.

Para poder realizar conversiones de codificación PHP provee de las funciones utf8_encode() y utf8_decode(). Sin embargo, es bastante común equivocarse y convertir a UTF-8 datos que ya están en dicha codificación o cometer el mismo error con datos codificados ISO-8859-1 lo cual nos corrompe algunos caracteres y puede causar errores en documentos XML y WebServices.

Este es un ejemplo de errores típicos de codificación:

En otras ocasiones, cuando hay que procesar datos de entrada (ej: leer archivos), uno no sabe de antemano si los datos a procesar vendrán codificados en UTF-8 o ISO-8859-1, o en un peor escenario podrían recibirse datos en ambas codificaciones.

Para solucionar estos problemas he construido unas funciones que detectan la codificación de un string y de esta manera realizan la conversión de codificación sólo si es necesario. Las funciones la he llamado latin1() que convierte los datos a ISO-8859-1 y utf8() que convierte a UTF-8.

Debido a que utilizan la función mb_detect_encoding(), es necesario que PHP tenga habilitado el módulo mbstring.



Función que converte un string a ISO-8859-1 (LATIN1)
function latin1($txt) {
$encoding = mb_detect_encoding($txt, 'ASCII,UTF-8,ISO-8859-1');
if ($encoding == "UTF-8") {
$txt = utf8_decode($txt);
}
return $txt;
}


Función que converte un string a UTF-8
function utf8($txt) {
$encoding = mb_detect_encoding($txt, 'ASCII,UTF-8,ISO-8859-1');
if ($encoding == "ISO-8859-1") {
$txt = utf8_encode($txt);
}
return $txt;
}

martes, 8 de abril de 2008

Criptografía en PHP

La criptografía nos permite cifrar información para que pueda ser accedida sólo por las personas adecuadamente autorizadas.

En informática la criptografía se puede dividir en dos grupos:
  • Criptografía simétrica: la encriptación y desencriptación se realiza con la misma llave.
  • Criptografía asimétrica: la encriptación se realiza con una llave y la desencriptación con otra diferente.
Muchos modelos de transmisión de información segura utilizan una combinación de ambos métodos para combinar sus ventajas.

El término "llave", "clave" o en ingles "key" se utiliza para identificar la secuencia de bits utilizada como "clave secreta" para el proceso de cifrar/descifrar la información.

Una regla estándar en la criptografía es que la seguridad debe enfocarse en proteger las llaves de encriptación y no en la ocultación del algoritmo criptográfico (o cipher). De hecho es muy usual que los algoritmos sean públicos.


Criptografía simétrica
La criptografía simétrica se basa en que la misma llave de encriptación se utiliza para desencriptar.

Es relativamente rápido, fácil de implementar e implementable en hardware de bajo costo. Presenta el problema que el emisor y receptor deben conocer previamente la llave de encriptación. Esto nos lleva al problema de cómo transmitirle al receptor la llave de encriptación y este es precisamente el principal problema de este modelo.

Antiguamente el algoritmo más popular era DES (Data Encryption Standard), sin embargo en la actualidad es considerado un algoritmo inseguro ya que la llave de encriptación tiene una longitud de sólo 56 bits, una vez que se declaró este algoritmo como inseguro, se comenzó a utilizar Triple DES que tiene una llave de 158 bits.

En la actualidad el algortimo de cifrado simétrico más popular es AES (Advanced Encryption Standard) también conocido como Rijndael (que es una combinación de lo nombre de sus autores) y la llave puede tener una longitud de 128, 196 y 256 bits.

A continuación incluyo un ejemplo de crifrado utilizando Rijndael-256, es decir utilizamos una llave de 256 bits que es equivalente a los 32 caracteres del ejemplo:
<?php

// Datos de entrada
$texto = 'frase secreta';
$key = '12345678901234567890123456789012';

// Proceso de cifrado
$iv = 'abcdefghijklmnopqrstuvwxyz012345';
$td = mcrypt_module_open('rijndael-256', '', 'ecb', '');
mcrypt_generic_init($td, $key, $iv);
$texto_cifrado = mcrypt_generic($td, $texto);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);

// Opcionalmente codificamos en base64
$texto_cifrado = base64_encode($texto_cifrado);

echo "$texto_cifrado\n";

?>
Este ejemplo requiere que PHP tenga habilitado el módulo mcrypt.

A continuación muestro un ejemplo del proceso inverso donde se desencripta la información previamente encriptada:
<?php

// Opcionalmente descodificamos en base64
$texto_cifrado = base64_decode($texto_cifrado);

// Proceso de descifrado
$td = mcrypt_module_open('rijndael-256', '', 'ecb', '');
mcrypt_generic_init($td, $key, $iv);
$texto = mdecrypt_generic($td, $texto_cifrado);
$texto = trim($texto, "\0");

echo "$texto\n";

?>

Criptografía asimétrica
La criptografía asimétrica tiene la característica que utiliza una llave para cifrar y otra para descifrar. Usualmente a una de estas llaves se le denomina clave privada y a la otra clave pública. La clave privada es conocida solo por el propietario de la misma, la clave pública puede ser conocida por todos. Gracias a este modelo se soluciona el principal problema de la criptografía simétrica.

Sin embargo tiene el inconveniente que los algoritmos criptográficos son más complejos, requieren mayor tiempo de procesamiento y el mensaje cifrado requiere de mayor longitud que el mensaje original. Además para proveer el mismo nivel de dureza que la criptografía simétrica (con respecto a ataques de fuerza bruta) requiere de llaves de mayor longitud, como por ejemplo una llave simétrica de 128 bits es aproximadamente equivalente a una llave asimétrica de 1024 bits.

Gracias a la criptografía asimétrica es posible garantizar -entre otros- lo siguiente:
  • Confidencialidad: Codificar información que solo la pueda descifrar el receptor.
  • Autenticación: Validar que la persona sea quien dice ser.
  • Integridad: Asegurar que la información no haya sido alterada (ej: firma electrónica).
  • No repudio: Asegurar quién es el autor de cierta información.
Más adelante presentaré un ejemplo de cifrado asimétrico, pero antes de eso publicaré un artículo de openssl para enseñar a generar certificados digitales y llaves.


Firma electrónica
La firma electrónica (que es derivada de la criptografía asimétrica) se ha convertido en una herramienta escencial en el desarrollo de aplicaciones y es un estándar de la industria y de gobierno electrónico (e-gov) para el tratamiento de información.

La firma electrónica está construida sobre la infraestructura de llave pública (PKI) y permite asegurar la autoría e integridad de los documentos.

Usualmente la implementación de firma electrónica se realiza en base a la especificación de XML Signature [w3c] también conocida como xmldsig.

Actualmente PHP no implementa XML Signature de manera nativa, sin embargo desde PHP 5.2.1 es posible implementarla (planeo a futuro dar algunos ejemplos de su implementación). Además Rob Richards que es miembro del equipo de desarollo de PHP (autor -entre otros- del módulo DOM) está trabajando en un módulo para PHP que ojalá algún día vea la luz.