JavaScript: ¿Cómo generar Rfc2898DeriveBytes como C #?

EDITAR: por discusión en los comentarios, permítanme aclarar que esto sucederá en el lado del server, detrás de SSL. No pretendo exponer la contraseña hash o el esquema de hashing al cliente.

Supongamos que tenemos una database de identidad asp.net existente con las tablas pnetworkingeterminadas (aspnet_Users, aspnet_Roles, etc.). Según lo que entiendo, el algorithm hashing de contraseña usa sha256 y almacena el salt + (contraseña hash) como una cadena codificada en base64. EDITAR: Esta suposition es incorrecta, ver respuesta a continuación.

Me gustaría replicar la function de la function VerifyHashedPassword de Microsoft.AspNet.Identity.Crypto class con una versión de JavaScript.

Digamos que una contraseña es bienvenida1 y su contraseña hash asp.net es ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA / LoVy0E4XCyUHIfJ7dfSY0Id + uJ20DTtG + A ==

Hasta ahora he podido reproducir las partes del método que obtienen la sal y la subkey almacenada.

Donde la implementación de C # hace más o less esto:

var salt = new byte[SaltSize]; Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, SaltSize); var stonetworkingSubkey = new byte[PBKDF2SubkeyLength]; Buffer.BlockCopy(hashedPasswordBytes, 1 + SaltSize, stonetworkingSubkey, 0, PBKDF2SubkeyLength); 

Tengo lo siguiente en JavaScript (no elegante por ningún tramo):

 var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A=="; var hashedPasswordBytes = new Buffer(hashedPwd, 'base64'); var saltbytes = []; var stonetworkingSubKeyBytes = []; for(var i=1;i<hashedPasswordBytes.length;i++) { if(i > 0 && i <= 16) { saltbytes.push(hashedPasswordBytes[i]); } if(i > 0 && i >16) { stonetworkingSubKeyBytes.push(hashedPasswordBytes[i]); } } 

De nuevo, no es bonito, pero después de ejecutar este fragment, saltbytes y stonetworkingSubKeyBytes coinciden con el byte de bytes que veo en el depurador de C # para salt ySubkey almacenado.

Finalmente, en C #, se usa una instancia de Rfc2898DeriveBytes para generar una nueva subkey basada en la sal y la contraseña proporcionadas, así:

 byte[] generatedSubkey; using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, PBKDF2IterCount)) { generatedSubkey = deriveBytes.GetBytes(PBKDF2SubkeyLength); } 

Aquí es donde estoy atascado. He intentado con otras soluciones como esta , he utilizado las bibliotecas CryptoJS y CryptoJS de Google y Node, respectivamente, y mi salida nunca genera nada parecido a la versión C #.

(Ejemplo:

 var output = crypto.pbkdf2Sync(new Buffer('welcome1', 'utf16le'), new Buffer(parsedSaltString), 1000, 32, 'sha256'); console.log(output.toString('base64')) 

genera "LSJvaDM9u7pXRfIS7QDFnmBPvsaN2z7FMXURGHIuqdY =")

Muchos de los indicadores que he encontrado en línea indican problemas que involucran desajustes de encoding (NodeJS / UTF-8 vs. .NET / UTF-16LE), así que he intentado codificar usando el formatting de encoding .NET pnetworkingeterminado pero fue en vano.

O podría estar completamente equivocado sobre lo que supongo que están haciendo estas bibliotecas. Pero cualquier puntero en la dirección correcta sería muy apreciado.

Ok, creo que este problema terminó siendo un poco más simple de lo que lo estaba haciendo (¿no es así?). Después de realizar una operación RTFM en la especificación pbkdf2 , realicé algunas testings lado a lado con Node crypto y .NET crypto, y he progresado bastante en una solución.

El siguiente código JavaScript analiza correctamente la sal y la subkey almacenadas, luego verifica la contraseña dada al mezclarla con la sal almacenada. Sin duda hay ajustes mejores / más limpios / más seguros, por lo que los comentarios son bienvenidos.

 // NodeJS implementation of crypto, I'm sure google's // cryptoJS would work equally well. var crypto = require('crypto'); // The value stonetworking in [dbo].[AspNetUsers].[PasswordHash] var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A=="; var hashedPasswordBytes = new Buffer(hashedPwd, 'base64'); var hexChar = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]; var saltString = ""; var stonetworkingSubKeyString = ""; // build strings of octets for the salt and the stonetworking key for (var i = 1; i < hashedPasswordBytes.length; i++) { if (i > 0 && i <= 16) { saltString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f] } if (i > 0 && i > 16) { stonetworkingSubKeyString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f]; } } // password provided by the user var password = 'welcome1'; // TODO remove debug - logging passwords in prod is considenetworking // tasteless for some odd reason console.log('cleartext: ' + password); console.log('saltString: ' + saltString); console.log('stonetworkingSubKeyString: ' + stonetworkingSubKeyString); // This is where the magic happens. // If you are doing your own hashing, you can (and maybe should) // perform more iterations of applying the salt and perhaps // use a stronger hash than sha1, but if you want it to work // with the [as of 2015] Microsoft Identity framework, keep // these settings. var nodeCrypto = crypto.pbkdf2Sync(new Buffer(password), new Buffer(saltString, 'hex'), 1000, 256, 'sha1'); // get a hex string of the derived bytes var derivedKeyOctets = nodeCrypto.toString('hex').toUpperCase(); console.log("hex of derived key octets: " + derivedKeyOctets); // The first 64 bytes of the derived key should // match the stonetworking sub key if (derivedKeyOctets.indexOf(stonetworkingSubKeyString) === 0) { console.info("passwords match!"); } else { console.warn("passwords DO NOT match!"); } 
    Intereting Posts