(Solucionado) Duda implementando "HMAC-based One-time Password Algorithm"

Iniciado por AlbertoBSD, 21 Diciembre 2019, 00:02 AM

0 Miembros y 1 Visitante están viendo este tema.

AlbertoBSD

Bueno, lo pongo aqui por que no se si corresponde mas a seguridad o PHP, pero su implementación ayuda mucho en el desarrollo WEB. Que algun moderador lo mueva a donde sea mas conveniente.

Estoy implementando el algoritmo para HOTP (HMAC-based One-time Password Algorithm) En su modalidad basada en el tiempo de UNIX

Actualemente tengo lo siguiente:

Código (php) [Seleccionar]

/*
dd if=/dev/urandom of=./key.dat bs=1024 count=1
*/
$key = file_get_contents("./key.dat");
$ct = floor(time()/30);
echo hash_hmac("sha1",$ct,$key)."\n";


El código actualmente genera un hash distinto cada 30 segundos exactos mi duda es como llegar a los 6 u 8 dígitos que muestra la aplicaciones de Authenticator como la de google.

Nota para los que me digan que no utilize sha1, lo utilize por que wikipedia indica que es el default, pero espero que pueda ser configurable para utilizar sha256 y alguno de la misma familia.

https://en.wikipedia.org/wiki/HMAC-based_One-time_Password_algorithm

Saludos.




Solucionado gracias a las observaciones de MinusFour.

La funcion quedo de la siguiente manera:

Código (php) [Seleccionar]
function hotp($key,$c) {
$mask = ['f' => '7','e' => '6','d' => '5','c' => '4','b' => '3','a' => '2','9' => '1','8' => '0'];
$hash_value_str = hash_hmac("sha1",pack("H*",sprintf("%016x",$c)),$key);
$offset = intval($hash_value_str[39],16)*2;
$extract = substr($hash_value_str,$offset,8);
if(isset($mask[$extract[0]])) {
$extract[0] = $mask[$extract[0]];
}
$value = intval($extract,16);
return ($value % 1000000);
}


Y para utilizarlo solo basta llamarla:

Código (php) [Seleccionar]
$ct = floor(time()/30);
$key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
printf("%06d\n",hotp($key,$ct));


Esto arroja los 6 dígitos que deben de coincidir con la aplicación de Autenticador que tengamos instalada en el móvil.

Para este ejemplo se puede configurar manualmente como se muestra en una las imágenes.

Donaciones
1Coffee1jV4gB5gaXfHgSHDz9xx9QSECVW

MinusFour

Necesitas truncar el HMAC, como dice el RFC. Necesitas extraer 31 bits del HMAC. Lo que dice el RFC es que primero obtengas el offset del cual vas a agarrar los 31 bits. Este offset, son los últimos 4 bits del HMAC. El número que obtengas lo multiplicas por 8 para obtener el inicio del offset. Para obtener el final del offset, solo le sumas 32. De ese offset, solo tomas los 31 bits menos significativos (no tomas el bit más significativo). Lo último es tomar los últimos d digitos de la representación numérica de esos 31 bits. Es un número entre 0 y 2147483648.

Este es un ejemplo HMAC sacado del RFC:


   -------------------------------------------------------------
   | Byte Number                                               |
   -------------------------------------------------------------
   |00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|
   -------------------------------------------------------------
   | Byte Value                                                |
   -------------------------------------------------------------
   |1f|86|98|69|0e|02|ca|16|61|85|50|ef|7f|19|da|8e|94|5b|55|5a|
   -------------------------------***********----------------++|


Los últimos 4 bits son: "A"(10). Entonces, tomas los bits de 80 a 112, que son:


50 EF 7F 19


El primer bit es 0 así que no importa, pero aquí le aplicas una mascara al bit más significativo (50 & 7F = 50). Este es tu número de 31 bits:


1357872921


En el RFC usan de ejemplo que solo toman 6 digitos. Usan modulo con una potencia de 10 para truncar el número.


We then take this number modulo 1,000,000 (10^6) to generate the 6-
   digit HOTP value 872921 decimal.


Y ya esta, ese es tu HOTP:

https://tools.ietf.org/html/rfc4226

AlbertoBSD

 ;-) ;-) ;-) Muchas gracias.

He hecho una implementacion algo vaga (Ahorita no esta optimizada ni nada) y aunque llego al mismo resultado del ejemplo que muestras si meto el hash indicado del ejemplo, y si, produce el Valor de 6 dígitos del ejemplo.

$hash_value_str = "1f8698690e02ca16618550ef7f19da8e945b555a";

Código (php) [Seleccionar]

<?php
/* dd if=/dev/urandom of=./key.dat bs=1024 count=1*/
include_once("Base2n.php");
$base32 = new Base2n(5'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'FALSETRUETRUE);
$mask = ['f' => '7',
'e' => '6',
'd' => '5',
'c' => '4',
'b' => '3',
'a' => '2',
'9' => '1',
'8' => '0'];
//$key = file_get_contents("./key.dat");
$key "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
$ct floor(time()/30);
$hash_value_str hash_hmac("sha1",$ct,$key);
//$hash_value_str = "1f8698690e02ca16618550ef7f19da8e945b555a";
echo "$hash_value_str\n";
echo 
strlen($hash_value_str)."\n";
echo 
$hash_value_str[39]."\n";
$offset intval($hash_value_str[39],16)*2;
echo 
"offset: $offset\n";
$extract substr($hash_value_str,$offset,8);
echo 
"extract: $extract\n";
if(isset(
$mask[$extract[0]])) {
$extract[0] = $mask[$extract[0]];
}
echo 
"extract: $extract\n";
$value intval($extract,16);
echo 
"Valor: $value\n";
echo 
"Digtos: ". ($value 1000000) ."\n";
echo 
$base32->encode($key) ."\n";
?>



El detalle es que paso la salida en Base32 del valor $key a la aplicación del Google Autenticador y los números que ahí produce no coinciden con los que arroja el programa cuando metod los valores de $key y el valor $ct en  base a time()/30.

Voy a ver que puedo estar haciendo mal. Tome una implementacion de base32 que se encuentra en el siguiente link, por si alguien lo quiere probar.

https://github.com/ademarre/binary-to-text-php/blob/master/Base2n.php

Saludos!

Donaciones
1Coffee1jV4gB5gaXfHgSHDz9xx9QSECVW

MinusFour

La llave que se hashea me imagino que debe ser en su forma base32. El secreto que se comparte debe ser el mismo.

AlbertoBSD

#4
Te refieres a algo como utilizar el valor en formato base32 para realizar el hasheo y demas?

He modificado la funcion para poder testeaar varios valores al mismo tiempo:

Código (php) [Seleccionar]

function hotp($key,$c) {
$mask = ['f' => '7','e' => '6','d' => '5','c' => '4','b' => '3','a' => '2','9' => '1','8' => '0'];
echo "key: $key\n";
echo "c: $c\n";
$hash_value_str = hash_hmac("sha1",$c,$key);
echo "hash: $hash_value_str\n";
echo "4bits (hex): ".$hash_value_str[39]."\n";
$offset = intval($hash_value_str[39],16)*2;
$extract = substr($hash_value_str,$offset,8);
echo "extract: $extract\n";
if(isset($mask[$extract[0]])) {
echo $extract[0]."& 7 =";
$extract[0] = $mask[$extract[0]];
echo $extract[0]."\n";
echo "new extract: $extract\n";
}
$value = intval($extract,16);
echo "Value: $value\n";
return ($value % 1000000);
}


Y ahora solo tengo que hacer:

Código (php) [Seleccionar]

$base32 = new Base2n(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', FALSE, TRUE, TRUE);
$ct = floor(time()/30);
$key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
echo "Key in base32: ".$base32->encode($key)."\n";

printf("%06d\n",hotp($key,$ct));
printf("%06d\n",hotp($base32->encode($key),$ct));


Tiene muchos Echo la funcion que se pueden comentar, estan ahi para de momento validar que la funcion esta haciendo bien  los pasos del algoritmo.

Alguna salida es:


Key in base32: IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAQ====
key: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
c: 52566234
hash: 428fc537d16c867ee7e1b4b3cc432c7e288d5769
4bits (hex): 9
extract: e1b4b3cc
e& 7 =6
new extract: 61b4b3cc
Value: 1639232460
232460
key: IFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAUCQKBIFAQ====
c: 52566234
hash: a49ac9470db4a542a36b42d28b7e089e11123f8a
4bits (hex): a
extract: 42d28b7e
Value: 1121094526
094526


Ahora como comentas he probado la llave en su formas de AAAAA y en sus forma en base32.

El google Authenticator lo configuro de la siguiente manera:

Primero agrego la llave en su formato base32


Y obtienes los valores



Pero no coincide con lo que arroja el programa, tengo la duda de saber si el valor $C es correcto, lo obtengo del time/30:

Código (php) [Seleccionar]
$ct = floor(time()/30);

Saludos!

MOD: Imagenes adaptadas a lo permitido.
Donaciones
1Coffee1jV4gB5gaXfHgSHDz9xx9QSECVW

animanegra

#5
Yo lo implementé ya en php y en html/javascript para después con cordoba portarlo a android. La version apk, lee los QRs y obtiene el TOTP de forma standar con md5 sha1 sha3 etc....
En el caso de google authenticartor, en el qr no viene el method pero es sha1. Yo en mi cliente tengo puesto el md5 por defecto.
La parte del servidor pues eso, genera el TOTP para depsués utilizarlo.

Creo que igual te puede ayudar, no lo se. Si tienes problemas echa un vistazo...

https://github.com/4nimanegra/OTPanimanegra

Está aun a medias (no es producto, es muy feo y tal), pero funcional. Testeado por ejemplo con protonmail. Algun dia, lo terminare porque no le queda nada, o no. ^^

Ahora dudo si la ultima version estaba en el github... :\ Le hecho un vistazo y si no la subo a mas tardar para mañana.

42
No contesto mensajes por privado, si tienes alguna pregunta, consulta o petición plantéala en el foro para que se aproveche toda la comunidad.

AlbertoBSD

#6
Que tal gracias por responder, acabo de ver el link en tu pagina de github y el código que muestras solo extrae los primeros 6 caracteres del hash devuelto por hash_hmac sin importar si son letras o no.

Creo que no esta actualizado.

Sobre lo que comentas del tipo de hash pense que era solo con sha1 pero me da gusto que sea configurable por aquello de las colisiones en sha1 y en md5 yo prefiero usar sha256, pero realmente con el limite de 30 segundos la mayoría de los hash son validos.

Saludos
Donaciones
1Coffee1jV4gB5gaXfHgSHDz9xx9QSECVW

MinusFour

#7
Creo que el culpable aquí es diferentes metodos de hash_hmac. Tu contador parece estar correcto, lo único que pudiera ser en ese aspecto es que los relojes no estén sincronizados.

Ahora, yo creo que el formato es el verdadero problema. En el RFC de TOTP ellos usan bytes para el secreto y el mensaje. Por ejemplo, si el secreto en ASCII es '12345678901234567890', el hexadecimal de este es:


31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 37 38 39 30


Y por ejemplo, si el contador fuera 1, lo que ellos hacen es hacer 0 padding hasta tener 8 bytes.


00 00 00 00 00 00 00 01


En el ejemplo que ponen en su RFC, ellos usan Java y convierten el string hexadecimal a bytes. Para este preciso ejemplo, con Sha-1 y el secreto que mencione, ellos obtienen:



 +-------------+--------------+------------------+----------+--------+
 |  Time (sec) |   UTC Time   | Value of T (hex) |   TOTP   |  Mode  |
 +-------------+--------------+------------------+----------+--------+
 |      59     |  1970-01-01  | 0000000000000001 | 94287082 |  SHA1  |
 |             |   00:00:59   |                  |          |        |
 +-------------+--------------+------------------+----------+--------+


Y simplemente, no doy con ese TOTP sin importar que valores use en el hmac. Intente con strings en hexadecimal y ASCII (con pack). Simplemente, no obtengo esos números. Lo voy a probar con node, porque la libreria crypto en node si me deja usar buffers.

El RFC de TOTP por si te sirve:

https://tools.ietf.org/html/rfc6238

Edit: Ok, vale... pude hacerlo funcionar ya. Tanto el secreto como el contador parece que hash_hmac los toma como ASCII. No puedes enviar un entero como mensaje. Me imagino que PHP simplemente lo convierte a ASCII (1 vendría siendo 31 en hex). El contador también tiene que tener padding hasta que tengas 8 bytes. Un string en hexadecimal necesitaría tener 16 caracteres. Tomas el contador, lo conviertes a un string en hexadecimal y le agregas 0 hasta tener 16 caracteres.

Ejemplo:

3167928 es 3056B8 en hexadecimal. Le necesitas agregar otros 5 bytes (10 caracteres).


00000000003056B8


Y usas pack a ese string:

Código (php) [Seleccionar]

pack('H*', '00000000003056B8');


Los 0s son importantes...

En cuanto a tu secreto, es necesario que conviertas de Base32 a ASCII.

AlbertoBSD

Excelente ya funciono, fijate que es las pocas cosas que no me gusta de PHP cuando quieres que un valor se mantenga en su forma (int), se toma las libertades de usalo como string y viceversa.

Muchas gracia por tomarte la molestia y hecharle un ojo.  ;-) ;-) ;-)

La función quedo de la siguiente manera:

Código (php) [Seleccionar]
function hotp($key,$c) {
$mask = ['f' => '7','e' => '6','d' => '5','c' => '4','b' => '3','a' => '2','9' => '1','8' => '0'];
$hash_value_str = hash_hmac("sha1",pack("H*",sprintf("%016x",$c)),$key);
$offset = intval($hash_value_str[39],16)*2;
$extract = substr($hash_value_str,$offset,8);
if(isset($mask[$extract[0]])) {
$extract[0] = $mask[$extract[0]];
}
$value = intval($extract,16);
return ($value % 1000000);
}


Y los valores ya coinciden con los que arrojan las aplicaciones de authenticator.

Saludos
Donaciones
1Coffee1jV4gB5gaXfHgSHDz9xx9QSECVW