Trying to decrypt AES using RNCryptor-js which uses SJCL. After logging all the steps on each end, (other end is RNCryptor-python) the keys, salts, HMAC hashes, everything matches up. But when I get to the final step:
var aes = new sjcl.cipher.aes(encryption_key);
sjcl.beware["CBC mode is dangerous because it doesn't protect message integrity."]()
var decrypted = aes.decrypt(ciphertext, iv);
I get the error:
sjcl.exception.invalid {toString: function, message: "invalid aes block size"}
Here is the full code:
PBKDF2:
this.KeyForPassword = function(password, salt) {
var hmacSHA256 = function (password) {
var hasher = new sjcl.misc.hmac(password, sjcl.hash.sha256);
this.encrypt = function () {
return hasher.encrypt.apply(hasher, arguments);
};
};
return sjcl.misc.pbkdf2(password, salt, 10000, 32 * 8, hmacSHA256);
};
Decryption (takes a hex input):
this.decrypt = function(password, message, options) {
message = sjcl.codec.hex.toBits(message);
options = options || {};
var version = sjcl.bitArray.extract(message, 0 * 8, 8);
var options = sjcl.bitArray.extract(message, 1 * 8, 8);
var encryption_salt = sjcl.bitArray.bitSlice(message, 2 * 8, 10 * 8);
var encryption_key = _this.KeyForPassword(password, encryption_salt, "decryption");
var hmac_salt = sjcl.bitArray.bitSlice(message, 10 * 8, 18 * 8);
var hmac_key = _this.KeyForPassword(password, hmac_salt, "decryption");
var iv = sjcl.bitArray.bitSlice(message, 18 * 8, 34 * 8);
var ciphertext_end = sjcl.bitArray.bitLength(message) - (32 * 8);
var ciphertext = sjcl.bitArray.bitSlice(message, 34 * 8, ciphertext_end);
var hmac = sjcl.bitArray.bitSlice(message, ciphertext_end);
var expected_hmac = new sjcl.misc.hmac(hmac_key).encrypt(sjcl.bitArray.bitSlice(message, 0, ciphertext_end));
if (! sjcl.bitArray.equal(hmac, expected_hmac)) {
throw new sjcl.exception.corrupt("HMAC mismatch or bad password.");
}
var aes = new sjcl.cipher.aes(encryption_key);
sjcl.beware["CBC mode is dangerous because it doesn't protect message integrity."]()
var decrypted = aes.decrypt(ciphertext, iv);
return decrypted;
}
The error is thrown on the second to last statement where decrypted is defined.
I looked at the sjcl exception and it looks like it's looking for an input of length 4, which I'm guessing is a WordArray. I'm just lost as to how to get a valid input. Like I said, the ciphertext, iv, hmac tag, salts are all being sliced properly on the javascript end. Might just be an encoding problem.
This error also seems to only be happening on json (format: '{"key":"value"}'), when I tried something like "Hello, world" I got back a 4 word array with no errors.
Any suggestions?
var decrypted = aes.decrypt(ciphertext, iv);
should be
var decrypted = sjcl.mode.cbc.decrypt(aes, ciphertext, iv);
I was also having trouble with padding happening in cbc.js (link to source) and it turned out that I hadn't included bitArray.js (link) which includes an important xor function (not to confused with the simple ^ operator).
So: include bitArray.js
The output should also be encoded:
return sjcl.codec.utf8String.fromBits(decrypted);
Related
Question
According to online articles the .NET Core PasswordHasher<> class uses:
ASP.NET Core Identity Version 3: PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations
The source code says this:
/*
* Version 2:
* PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
* (See also: SDL crypto guidelines v5.1, Part III)
* Format: { 0x00, salt, subkey }
*
* Version 3:
* PBKDF2 with HMAC-SHA512, 128-bit salt, 256-bit subkey, 100000 iterations.
* Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
* (All UInt32s are stored big-endian.)
*/
What's the equivalent to hashing and validating a string with PasswordHasher<> V3 in Node?
What have I tried
Since crypto doesn't seem to support "PBKDF2 with HMAC-SHA256" I'm currently trying pbkdf2-hmac like this:
const generatedHash = arrayBufferToBase64(
await pbkdf2Hmac(password, salt, 100000, 32, "SHA-256")
);
The thing I'm least certain about is the fourth argument (32) as on one hand .NET says "256-bit subkey" and on the other pbkdf2Hmac says that the fourth argument should be "intended length in octets of the derived key"; my reasoning was 256 / 8 = 32 but that's a wild guess.
In any case I've tried 32, 64 and 256 for that value, I've also tried all combinations of SHA1, SHA256, SHA512 with 1000, 10000, 100000 iterations and nothing gives me the same base64 string as PasswordHasher<>.
What am I missing?
Sample password
Given the password sample123 ! the hash generated by PasswordHasher<> is:
AQAAAAEAACcQAAAAEAdmj6YuRrVMYBXbeGrV/j3VqSzvhIhE/xYWA0S1hpfxH5FuepoGJjite6n/Fh4quw==
Notice that if I read the implementation correctly then the salt is randomly generated and embedded in the hash.
For validation, the subkey must be calculated using PBKDF2 and compared with the subkey in the hash.
All information needed to calculate the key using PBKDF2 is contained in the hash:
the digest used (prf)
iteration count (iter)
salt length and salt
subkey in the hash is located at the end of the hash and has a length of 256 bits = 32 bytes.
This allows the key to be derived using PBKDF2 if the password is known:
var hashB64 = "AQAAAAEAACcQAAAAEAdmj6YuRrVMYBXbeGrV/j3VqSzvhIhE/xYWA0S1hpfxH5FuepoGJjite6n/Fh4quw=="
var password = "sample123 !";
// Get digest, salt and iteration count:
// Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
var hash = Buffer.from(hashB64, "base64");
var marker = hash.subarray(0,1); // 0: V2, 1: V3
var prf = hash.readUInt32BE(1); // 0: HMAC/SHA1, 1: HMAC/SHA256, 2: HMAC/SHA512
var iter = hash.readUInt32BE(1 + 4);
var saltLen = hash.readUInt32BE(1 + 2*4);
var salt = hash.subarray(1 + 3*4, 1 + 3*4 + saltLen);
var subkey = hash.subarray(1 + 3*4 + saltLen);
// Calculate subkey with pbkdf2
var keySize = 32;
var subkeyPbkdf2 = crypto.pbkdf2Sync(password, salt, iter, keySize, prf == 0 ? "sha1" : (prf == 1 ? "sha256" : "sha512"));
console.log(subkey.compare(subkeyPbkdf2)); // 0 = successfully validated
The posted sample hash uses SHA-256 as digest, an iteration count of 10000, and a salt length of 16 bytes.
A new hash can be determined by generating a random 16 bytes salt, calculating the subkey from it using PBKDF2, and contacting all data according to the above specification.
Example (using the above values):
var marker = 1;
var prf = 1;
var iter = 10000;
var saltLen = 16;
var salt = "B2aPpi5GtUxgFdt4atX+PQ=="; // random salt
var subkey = "1aks74SIRP8WFgNEtYaX8R+RbnqaBiY4rXup/xYeKrs="; // via PBKDF2
var markerBuf = Buffer.allocUnsafe(1);
markerBuf.writeUInt8(marker);
var prfBuf = Buffer.allocUnsafe(4);
prfBuf.writeUInt32BE(prf);
var iterBuf = Buffer.allocUnsafe(4);
iterBuf.writeUInt32BE(iter);
var saltLenBuf = Buffer.allocUnsafe(4);
saltLenBuf.writeUInt32BE(saltLen);
var saltBuf = Buffer.from(salt, "base64");
var subkeyBuf = Buffer.from(subkey, "base64");
var hashPbkdf2 = Buffer.concat([markerBuf, prfBuf, iterBuf, saltLenBuf, saltBuf, subkeyBuf]);
var hashB64 = "AQAAAAEAACcQAAAAEAdmj6YuRrVMYBXbeGrV/j3VqSzvhIhE/xYWA0S1hpfxH5FuepoGJjite6n/Fh4quw==";
console.log(hashB64 === hashPbkdf2.toString("base64")); // true
I'm converting Rijndael decryption from C# to NodeJS.
The Key (or Passphrase) used is 13 characters long. The IV used is 17 characters long.
Note: I have no control over the length choice
Below is the Rijndael decryption in C#
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public class Program
{
public class CryptoProvider
{
private ICryptoTransform encryptor = (ICryptoTransform)null;
private ICryptoTransform decryptor = (ICryptoTransform)null;
private int minSaltLen = -1;
private int maxSaltLen = -1;
public CryptoProvider(string passPhrase, string initVector)
: this(passPhrase, initVector, -1, -1, -1, (string)null, (string)null, 3)
{
}
public CryptoProvider(
string passPhrase,
string initVector,
int minSaltLen,
int maxSaltLen,
int keySize,
string hashAlgorithm,
string saltValue,
int passwordIterations)
{
this.minSaltLen = 4;
this.maxSaltLen = 8;
keySize = 256;
hashAlgorithm = "SHA512";
byte[] rgbIV = Encoding.ASCII.GetBytes(initVector);
byte[] rgbSalt = new byte[0];
byte[] bytes = new PasswordDeriveBytes(passPhrase, rgbSalt, hashAlgorithm, passwordIterations).GetBytes(keySize / 8);
RijndaelManaged rijndaelManaged = new RijndaelManaged();
if (rgbIV.Length == 0)
rijndaelManaged.Mode = CipherMode.ECB;
else
rijndaelManaged.Mode = CipherMode.CBC;
this.encryptor = rijndaelManaged.CreateEncryptor(bytes, rgbIV);
this.decryptor = rijndaelManaged.CreateDecryptor(bytes, rgbIV);
}
public string Decrypt(string cipherText) {
return this.Decrypt(Convert.FromBase64String(cipherText));
}
public string Decrypt(byte[] cipherTextBytes) {
return Encoding.UTF8.GetString(this.DecryptToBytes(cipherTextBytes));
}
public byte[] DecryptToBytes(string cipherText) {
return this.DecryptToBytes(Convert.FromBase64String(cipherText));
}
public byte[] DecryptToBytes(byte[] cipherTextBytes)
{
int num = 0;
int sourceIndex = 0;
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
byte[] numArray = new byte[cipherTextBytes.Length];
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.decryptor, CryptoStreamMode.Read);
num = cryptoStream.Read(numArray, 0, numArray.Length);
memoryStream.Close();
cryptoStream.Close();
}
if (this.maxSaltLen > 0 && this.maxSaltLen >= this.minSaltLen)
sourceIndex = (int)numArray[0] & 3 | (int)numArray[1] & 12 | (int)numArray[2] & 48 | (int)numArray[3] & 192;
byte[] destinationArray = new byte[num - sourceIndex];
Array.Copy((Array)numArray, sourceIndex, (Array)destinationArray, 0, num - sourceIndex);
return destinationArray;
}
}
public static void Main()
{
string Key = "";
string IV = "";
string encryptedUserData = "u7uENpFfpQhMXiTThL/ajA==";
string decryptedUserData;
CryptoProvider crypto = new CryptoProvider(Key, IV);
decryptedUserData = crypto.Decrypt(encryptedUserData.Trim());
Console.WriteLine(decryptedUserData);
}
}
which for some reason, I can decrypt the string in dotnetfiddle, but not in Visual Studio (because it returns an error of 'Specified initialization vector (IV) does not match the block size for this algorithm. (Parameter 'rgbIV')'
Below is my attempt to convert in NodeJS using the rijndael-js library:
const Rijndael = require("rijndael-js");
const key = "";
const iv = "";
const cipher = new Rijndael(key, "cbc");
const ciphertext = "u7uENpFfpQhMXiTThL/ajA==";
const plaintext = Buffer.from(cipher.decrypt(ciphertext, 256, iv));
which returns an error of Unsupported key size: 104 bit
All errors point to the same thing: Invalid Key/IV lengths.
Would there be a work-around where I can force NodeJS to accept the Key and IV as valid lengths? Is there something I am missing, doing incorrectly, or misconfigured?
Edit:
I was able to find a PasswordDeriveBytes implementation for NodeJS and compared the results from C# and they are equal.
I updated my NodeJS implementation (see sandbox) and noticed a few things:
All resulting ciphertexts are the same. I am guessing this stems from salts.
I tried decrypting a ciphertext generated from C#, but there seems to be a few characters to the left of the resulting value.
Example: C# Encrypted String: zAqv5w/gwT0sFYXZEx+Awg==, NodeJS Decrypted String: ���&��4423
When I try to decrypt a ciphertext generated in NodeJS in C#, the C# compiler returns an error of System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.
Edit:
C# code (executable with .NET Framework 4.7.2):
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace ProgramEncrypt
{
public class CryptoProvider
{
private ICryptoTransform encryptor = (ICryptoTransform)null;
private ICryptoTransform decryptor = (ICryptoTransform)null;
private int minSaltLen = -1;
private int maxSaltLen = -1;
public CryptoProvider(string passPhrase, string initVector) : this(passPhrase, initVector, -1, -1, -1, (string)null, (string)null, 3) { }
public CryptoProvider(
string passPhrase,
string initVector,
int minSaltLen,
int maxSaltLen,
int keySize,
string hashAlgorithm,
string saltValue,
int passwordIterations)
{
this.minSaltLen = 4;
this.maxSaltLen = 8;
keySize = 256;
hashAlgorithm = "SHA512";
byte[] rgbIV = Encoding.ASCII.GetBytes(initVector);
byte[] rgbSalt = new byte[0];
byte[] bytes = new PasswordDeriveBytes(passPhrase, rgbSalt, hashAlgorithm, passwordIterations).GetBytes(keySize / 8);
RijndaelManaged rijndaelManaged = new RijndaelManaged();
if (rgbIV.Length == 0)
rijndaelManaged.Mode = CipherMode.ECB;
else
rijndaelManaged.Mode = CipherMode.CBC;
this.encryptor = rijndaelManaged.CreateEncryptor(bytes, rgbIV);
this.decryptor = rijndaelManaged.CreateDecryptor(bytes, rgbIV);
}
public string Encrypt(string plainText) => this.Encrypt(Encoding.UTF8.GetBytes(plainText));
public string Encrypt(byte[] plainTextBytes) => Convert.ToBase64String(this.EncryptToBytes(plainTextBytes));
public byte[] EncryptToBytes(string plainText) => this.EncryptToBytes(Encoding.UTF8.GetBytes(plainText));
public byte[] EncryptToBytes(byte[] plainTextBytes)
{
byte[] buffer = this.AddSalt(plainTextBytes);
MemoryStream memoryStream = new MemoryStream();
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.encryptor, CryptoStreamMode.Write);
cryptoStream.Write(buffer, 0, buffer.Length);
cryptoStream.FlushFinalBlock();
byte[] array = memoryStream.ToArray();
memoryStream.Close();
cryptoStream.Close();
return array;
}
}
public string Decrypt(string cipherText) => this.Decrypt(Convert.FromBase64String(cipherText));
public string Decrypt(byte[] cipherTextBytes) => Encoding.UTF8.GetString(this.DecryptToBytes(cipherTextBytes));
public byte[] DecryptToBytes(string cipherText) => this.DecryptToBytes(Convert.FromBase64String(cipherText));
public byte[] DecryptToBytes(byte[] cipherTextBytes)
{
int num = 0;
int sourceIndex = 0;
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
byte[] numArray = new byte[cipherTextBytes.Length];
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.decryptor, CryptoStreamMode.Read);
num = cryptoStream.Read(numArray, 0, numArray.Length);
memoryStream.Close();
cryptoStream.Close();
}
if (this.maxSaltLen > 0 && this.maxSaltLen >= this.minSaltLen)
sourceIndex = (int)numArray[0] & 3 | (int)numArray[1] & 12 | (int)numArray[2] & 48 | (int)numArray[3] & 192;
byte[] destinationArray = new byte[num - sourceIndex];
Array.Copy((Array)numArray, sourceIndex, (Array)destinationArray, 0, num - sourceIndex);
return destinationArray;
}
private byte[] AddSalt(byte[] plainTextBytes)
{
if (this.maxSaltLen == 0 || this.maxSaltLen < this.minSaltLen)
return plainTextBytes;
byte[] salt = this.GenerateSalt();
byte[] destinationArray = new byte[plainTextBytes.Length + salt.Length];
Array.Copy((Array)salt, (Array)destinationArray, salt.Length);
Array.Copy((Array)plainTextBytes, 0, (Array)destinationArray, salt.Length, plainTextBytes.Length);
return destinationArray;
}
private byte[] GenerateSalt()
{
int length = this.minSaltLen != this.maxSaltLen ? this.GenerateRandomNumber(this.minSaltLen, this.maxSaltLen) : this.minSaltLen;
byte[] data = new byte[length];
new RNGCryptoServiceProvider().GetNonZeroBytes(data);
data[0] = (byte)((int)data[0] & 252 | length & 3);
data[1] = (byte)((int)data[1] & 243 | length & 12);
data[2] = (byte)((int)data[2] & 207 | length & 48);
data[3] = (byte)((int)data[3] & 63 | length & 192);
return data;
}
private int GenerateRandomNumber(int minValue, int maxValue)
{
byte[] data = new byte[4];
new RNGCryptoServiceProvider().GetBytes(data);
return new Random(((int)data[0] & (int)sbyte.MaxValue) << 24 | (int)data[1] << 16 | (int)data[2] << 8 | (int)data[3]).Next(minValue, maxValue + 1);
}
public static void Main()
{
string Key = "HelL!oWoRL3ds";
string IV = "HElL!o#wOrld!##%$";
string toEncrypt = "1234";
string encryptedData, decryptedData;
CryptoProvider crypto = new CryptoProvider(Key, IV);
encryptedData = crypto.Encrypt(toEncrypt.Trim());
decryptedData = crypto.Decrypt(encryptedData.Trim());
Console.WriteLine("ENCRYPTED: " + encryptedData);
Console.WriteLine("DECRYPTED: " + decryptedData);
}
}
}
NodeJS code (codesandbox.io):
import { deriveBytesFromPassword } from "./deriveBytesFromPassword";
const Rijndael = require("rijndael-js");
const dataToEncrypt = "1234";
const SECRET_KEY = "HelL!oWoRL3ds"; // 13 chars
const SECRET_IV = "HElL!o#wOrld!##%$"; // 17 chars
const keySize = 256;
const hashAlgorithm = "SHA512";
// Use only the first 16 bytes of the IV
const rgbIV = Buffer.from(SECRET_IV, "ascii").slice(0, 16); // #ref https://stackoverflow.com/a/57147116/12278028
const rgbSalt = Buffer.from([]);
const derivedPasswordBytes = deriveBytesFromPassword(
SECRET_KEY,
rgbSalt,
3,
hashAlgorithm,
keySize / 8
);
const dataToEncryptInBytes = Buffer.from(dataToEncrypt, "utf8");
const cipher = new Rijndael(derivedPasswordBytes, "cbc");
const encrypted = Buffer.from(cipher.encrypt(dataToEncryptInBytes, 16, rgbIV));
console.log(encrypted.toString("base64"));
// Use this if you only have the Base64 string
// Note: The Base64 string in Line 34 is from C#
// const decrypted = Buffer.from(
// cipher.decrypt(Buffer.from("zAqv5w/gwT0sFYXZEx+Awg==", "base64"), 16, rgbIV)
// );
const decrypted = Buffer.from(cipher.decrypt(encrypted, 16, rgbIV));
console.log(decrypted.toString());
A possible NodeJS implementation based on your sandbox code that is compatible with the C# code is:
const crypto = require("crypto");
const Rijndael = require("rijndael-js");
const pkcs7 = require('pkcs7-padding');
const SECRET_KEY = "HelL!oWoRL3ds"; // 13 chars
const SECRET_IV = "HElL!o#wOrld!##%$"; // 17 chars
const rgbIV = Buffer.from(SECRET_IV, "ascii").slice(0, 16);
const rgbSalt = Buffer.from([]);
const keySize = 256;
const hashAlgorithm = "SHA512";
const minSaltLen = 4;
const maxSaltLen = 8;
function encrypt(plaintextStr) {
var derivedPasswordBytes = deriveBytesFromPassword(SECRET_KEY, rgbSalt, 3, hashAlgorithm, keySize/8);
var cipher = new Rijndael(derivedPasswordBytes, "cbc");
var plaintext = Buffer.from(plaintextStr, "utf8");
var salt = generateSalt();
var saltPlaintext = Buffer.concat([salt, plaintext])
var saltPlaintextPadded = pkcs7.pad(saltPlaintext, 16)
var ciphertext = Buffer.from(cipher.encrypt(saltPlaintextPadded, 128, rgbIV));
return ciphertext.toString("base64");
}
function decrypt(ciphertextB64) {
var derivedPasswordBytes = deriveBytesFromPassword(SECRET_KEY, rgbSalt, 3, hashAlgorithm, keySize/8);
var cipher = new Rijndael(derivedPasswordBytes, "cbc");
var ciphertext = Buffer.from(ciphertextB64, 'base64');
var saltPlaintextPadded = Buffer.from(cipher.decrypt(ciphertext, 128, rgbIV));
var sourceIndex = saltPlaintextPadded[0] & 3 | saltPlaintextPadded[1] & 12 | saltPlaintextPadded[2] & 48 | saltPlaintextPadded[3] & 192
var plaintextPadded = saltPlaintextPadded.subarray(sourceIndex)
var plaintext = pkcs7.unpad(plaintextPadded)
return plaintext;
}
function generateSalt() {
var length = minSaltLen != maxSaltLen ? crypto.randomInt(minSaltLen, maxSaltLen + 1) : minSaltLen;
var data = crypto.randomBytes(length);
data[0] = data[0] & 252 | length & 3;
data[1] = data[1] & 243 | length & 12;
data[2] = data[2] & 207 | length & 48;
data[3] = data[3] & 63 | length & 192;
return data;
}
var plaintext = "1234";
var ciphertextB64 = encrypt(plaintext);
var plaintext = decrypt(ciphertextB64);
console.log(ciphertextB64);
console.log(plaintext.toString('hex'))
using the key derivation from the linked post.
Ciphertexts generated with this code can be decrypted with the C# code, and vice versa, ciphertexts generated with the C# code can be decrypted with this code.
Explanation:
The linked C# code can process a 17 bytes IV under .NET Framework (tested for 4.7.2). However, only the first 16 bytes are taken into account. With the addition rijndaelManaged.IV = rgbIV (as in the MS examples) an exception is thrown. Under .NET Core (tested for 3.0+) an exception is always thrown. This indicates that processing an IV in the .NET Framework that is too large, is more likely a bug. Anyway, in the NodeJS code also only the first 16 bytes of the IV have to be considered.
The C# code uses the proprietary key derivation PasswordDeriveBytes. The same key derivation must be applied in the NodeJS code. In the code above, the implementation linked by the OP is used.
The library involved rijndael-js applies Zero padding, but the C# code uses PKCS#7 padding. Therefore, in the NodeJS code, the plaintext (or concatenation of salt and plaintext) must be padded with PKCS#7 before encryption (this satisfies the length criterion and Zero padding is no longer applied). Accordingly, the padding must be removed after decryption. A possible library is pkcs7-padding. Alternatively, instead of rijndael-js, another library could be used which applies PKCS#7 padding by default.
The C# code uses two salts: One is the empty (!) rgbSalt, which is applied in the key derivation. The other is a second salt, which is randomly generated with respect to both length and content during encryption, is prepended to the plaintext, and contains the information about the salt length, which is determined during decryption. This logic must be implemented in the NodeJS code for both codes to be compatible.
The GenerateRandomNumber() method cannot be ported because its result depends on the internal details of the Random() implementation (which, by the way, is not a CSPRNG). The method is supposed to generate a random integer. For this purpose crypto.randomInt() is used. For RNGCryptoServiceProvider#GetNonZeroBytes() create.RandomBytes() is applied. This NodeJS function also allows 0x00 bytes, which could be optimized if needed.
Security:
The proprietary key derivation PasswordDeriveBytes is deprecated and insecure. Instead, Rfc2898DeriveBytes should be used in the C# code and PBKDF2 in the NodeJS code.
The missing salt in the key derivation is insecure and allows attacks e.g. via rainbow tables. Instead, a salt of sufficient size (at least 8 bytes) should be randomly generated for each encryption. This salt is not secret and is therefore usually concatenated with the ciphertext.
The C# implementation uses a static IV, which is insecure as well. Although the random second salt provides a different ciphertext for identical plaintexts and identical IVs, a best practice should be applied instead of a user defined construct. A proven way is a randomly generated IV, analogous to the salt used for key derivation (randomly generated for each encryption, concatenated with the ciphertext).
I could verify an ECDSA / SHA256 signature using the standard library of Javascript (window.crypto.subtle.verify) but cannot using the jsrsasign library (KJUR.crypto). I have also tried 'KJUR.crypto.ECDSA' class directly but no luck neither.
See below both script methods which dont provide same result. Could someone advise the issue(s) ?
//function to convert HEX to Decimal - return Arraybuffer
function hexStringToUint8Array(hexString) {
if (hexString.length % 2 != 0)
throw "Invalid hexString";
var arrayBuffer = new Uint8Array(hexString.length / 2);
for (var i = 0; i < hexString.length; i += 2) {
var byteValue = parseInt(hexString.substr(i, 2), 16);
if (byteValue == NaN)
throw "Invalid hexString";
arrayBuffer[i / 2] = byteValue;
}
return arrayBuffer;
}
//function to convert Base64 to hex (8 bits formats)
function base64ToHex(str) {
const raw = atob(str);
let result = '';
for (let i = 0; i < raw.length; i++) {
const hex = raw.charCodeAt(i).toString(16);
result += (hex.length === 2 ? hex : '0' + hex);
}
return result;
}
//convert Base64 URL to Base64
function base64urlToBase64(base64url) {
base64url = base64url.toString();
return base64url
.replace(/\-/g, "+")
.replace(/_/g, "/");
}
//Define values
Base64URL_coordX = '2uYQAsY-bvzz7r7SL-tK2C0eySfYEf1blv91cnd_1G4';
Base64URL_coordY = 'S3j1vy2sbkExAYXumb3w1HMVH-4ztoHclVTwQd45Reg';
signature = 'ed0c2b2e56731511ce2cea1d7320cdbc39dbabca7f525ec5d646b7c11cb35d5846a1cb70c2a1d8480f5ef88b46d401ca78b18ccae9ae4e3934a6b8fe412f7b11';
dataHex = '48656c6c6f20386777696669'; // ='Hello 8gwifi'
////////////Verifying Method using standard javascript
var dataToVerify = hexStringToUint8Array(dataHex);
var SignatureToVerify = hexStringToUint8Array(signature);
window.crypto.subtle.importKey(
"jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only)
{ //this is an example jwk key, other key types are Uint8Array objects
kty: "EC",
crv: "P-256",
x: Base64URL_coordX, // expects x and y to be «base64url» encoded
y: Base64URL_coordY,
ext: true,
},
{ //these are the algorithm options
name: "ECDSA",
namedCurve: "P-256", //can be "P-256", "P-384", or "P-521"
},
false, //whether the key is extractable (i.e. can be used in exportKey)
["verify"] //"verify" for public key import, "sign" for private key imports
)
.then(function(publicKey){
window.crypto.subtle.verify(
{
name: "ECDSA",
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
publicKey, //from generateKey or importKey above
SignatureToVerify, //ArrayBuffer of the signature
dataToVerify //ArrayBuffer of the data
)
.then(function(isvalid){
console.log('Signature valid1: ', isvalid);
})
.catch(function(err){
console.error(err);
});
});
////////////Verifying Method using KJUR
Hex_coordX = base64ToHex(base64urlToBase64(Base64URL_coordX));
Hex_coordY = base64ToHex(base64urlToBase64(Base64URL_coordY));
var XY = Hex_coordX.toString(16) + Hex_coordY.toString(16);
var sig = new KJUR.crypto.Signature({"alg": "SHA256withECDSA", "prov": "cryptojs/jsrsa"});
sig.init({xy: XY, curve: "secp256r1"});
sig.updateHex(dataHex);
var result = sig.verify(signature);
//Printing Verification
console.log('Signature valid2: ', result);
It says in the description of the library that it is JCA style. This probably means that the signature generation / verification functions have a ASN.1 / DER encoded input / output.
This consist of an ASN.1 SEQUENCE (tag 0x30), the length of the two integers inside. These two INTEGER's have tag 0x02 and a length of the size of the integer value of the r and s components of the signature. These are big endian, signed integers (which means stripping bytes if they are 0x00 or adding a 0x00 if the top byte is 0x80 or higher).
In your case that would be:
r = ed0c2b2e56731511ce2cea1d7320cdbc39dbabca7f525ec5d646b7c11cb35d58
s = 46a1cb70c2a1d8480f5ef88b46d401ca78b18ccae9ae4e3934a6b8fe412f7b11
Now converting these to DER ASN.1:
ri = 02 21 00 ed0c2b2e56731511ce2cea1d7320cdbc39dbabca7f525ec5d646b7c11cb35d58
si = 02 20 46a1cb70c2a1d8480f5ef88b46d401ca78b18ccae9ae4e3934a6b8fe412f7b11
and finally adding the sequence and adding the concatenation of above:
sig = 30 45 02 21 00 ed0c2b2e56731511ce2cea1d7320cdbc39dbabca7f525ec5d646b7c11cb35d58
02 20 46a1cb70c2a1d8480f5ef88b46d401ca78b18ccae9ae4e3934a6b8fe412f7b11
and checking the result e.g. here.
But I guess in your case just calling the function concatSigToASN1Sig would be faster :P
I'm learning about Blockchain and wanted to create an example of creating an address, purely for educational purposes - WOULD NOT BE DONE ANYWHERE NEAR PRODUCTION.
Task: create 160 random bits, convert it to hex, convert that to base 58, then to test correctness by reversing the process.
It kind of works, however I get intermittent 'false' on comparison of before and after binary. The hexStringToBinary function returns strings with varying lengths:
const bs58 = require('bs58');
//20 * 8 = 160
function generate20Bytes () {
let byteArray = [];
let bytes = 0;
while (bytes < 20) {
let byte = '';
while (byte.length < 8) {
byte += Math.floor(Math.random() * 2);
}
byteArray.push(byte);
bytes++;
}
return byteArray;
}
//the issue is probably from here
function hexStringToBinary (string) {
return string.match(/.{1,2}/g)
.map(hex => parseInt(hex, 16).toString(2).padStart(8, '0'));
}
const addressArray = generate20Bytes();
const binaryAddress = addressArray.join('');
const hex = addressArray.map(byte => parseInt(byte, 2).toString(16)).join('');
console.log(hex);
// then lets convert it to base 58
const base58 = bs58.encode(Buffer.from(hex));
console.log('base 58');
console.log(base58);
// lets see if we can reverse the process
const destructuredHex = bs58.decode(base58).toString();
console.log('hex is the same');
console.log(hex === destructuredHex);
// lets convert back to a binary string
const destructuredAddress = hexStringToBinary(destructuredHex).join('');
console.log('destructured address');
console.log(destructuredAddress);
console.log('binaryAddress address');
console.log(binaryAddress);
//intermittent false/true
console.log(destructuredAddress === binaryAddress);
Got round to refactoring with tdd. Realised it wasn't zero filling hex < 16. My playground repo
EDIT: Per discussion in the comments, let me clarify that this will be happening server side, behind SSL. I do not intend to expose the hashed password or the hashing scheme to the client.
Assume we have an existing asp.net identity database with the default tables (aspnet_Users, aspnet_Roles, etc.). Based on my understanding, the password hashing algorithm uses sha256 and stores the salt + (hashed password) as a base64 encoded string. EDIT: This assumption is incorrect, see answer below.
I would like to replicate the function of the Microsoft.AspNet.Identity.Crypto class' VerifyHashedPassword function with a JavaScript version.
Let's say that a password is welcome1 and its asp.net hashed password is ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==
So far I have been able to reproduce the parts of the method that get the salt and the stored sub key.
Where the C# implementation does more or less this:
var salt = new byte[SaltSize];
Buffer.BlockCopy(hashedPasswordBytes, 1, salt, 0, SaltSize);
var storedSubkey = new byte[PBKDF2SubkeyLength];
Buffer.BlockCopy(hashedPasswordBytes, 1 + SaltSize, storedSubkey, 0, PBKDF2SubkeyLength);
I have the following in JavaScript (not elegant by any stretch):
var hashedPwd = "ADOEtXqGCnWCuuc5UOAVIvMVJWjANOA/LoVy0E4XCyUHIfJ7dfSY0Id+uJ20DTtG+A==";
var hashedPasswordBytes = new Buffer(hashedPwd, 'base64');
var saltbytes = [];
var storedSubKeyBytes = [];
for(var i=1;i<hashedPasswordBytes.length;i++)
{
if(i > 0 && i <= 16)
{
saltbytes.push(hashedPasswordBytes[i]);
}
if(i > 0 && i >16) {
storedSubKeyBytes.push(hashedPasswordBytes[i]);
}
}
Again, it ain't pretty, but after running this snippet the saltbytes and storedSubKeyBytes match byte for byte what I see in the C# debugger for salt and storedSubkey.
Finally, in C#, an instance of Rfc2898DeriveBytes is used to generate a new subkey based on the salt and the password provided, like so:
byte[] generatedSubkey;
using (var deriveBytes = new Rfc2898DeriveBytes(password, salt, PBKDF2IterCount))
{
generatedSubkey = deriveBytes.GetBytes(PBKDF2SubkeyLength);
}
This is where I'm stuck. I have tried others' solutions such as this one, I have used Google's and Node's CryptoJS and crypto libraries respectively, and my output never generates anything resembling the C# version.
(Example:
var output = crypto.pbkdf2Sync(new Buffer('welcome1', 'utf16le'),
new Buffer(parsedSaltString), 1000, 32, 'sha256');
console.log(output.toString('base64'))
generates "LSJvaDM9u7pXRfIS7QDFnmBPvsaN2z7FMXURGHIuqdY=")
Many of the pointers I've found online indicate problems involving encoding mismatches (NodeJS / UTF-8 vs. .NET / UTF-16LE), so I've tried encoding using the default .NET encoding format but to no avail.
Or I could be completely wrong about what I assume these libraries are doing. But any pointers in the right direction would be much appreciated.
Ok, I think this problem ended up being quite a bit simpler than I was making it (aren't they always). After performing a RTFM operation on the pbkdf2 spec, I ran some side-by-side tests with Node crypto and .NET crypto, and have made pretty good progress on a solution.
The following JavaScript code correctly parses the stored salt and subkey, then verifies the given password by hashing it with the stored salt. There are doubtless better / cleaner / more secure tweaks, so comments welcome.
// NodeJS implementation of crypto, I'm sure google's
// cryptoJS would work equally well.
var crypto = require('crypto');
// The value stored 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 storedSubKeyString = "";
// build strings of octets for the salt and the stored 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) {
storedSubKeyString += 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 considered
// tasteless for some odd reason
console.log('cleartext: ' + password);
console.log('saltString: ' + saltString);
console.log('storedSubKeyString: ' + storedSubKeyString);
// 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 stored sub key
if (derivedKeyOctets.indexOf(storedSubKeyString) === 0) {
console.info("passwords match!");
} else {
console.warn("passwords DO NOT match!");
}
The previous solution will not work in all cases.
Let's say that you want to compare a password source against a hash in the database hash, which can be technically possible if the database is compromised, then the function will return true because the subkey is an empty string.
Modify the function to catch that up and return false instead.
// NodeJS implementation of crypto, I'm sure google's
// cryptoJS would work equally well.
var crypto = require('crypto');
// The value stored 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 storedSubKeyString = "";
// build strings of octets for the salt and the stored 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) {
storedSubKeyString += hexChar[(hashedPasswordBytes[i] >> 4) & 0x0f] + hexChar[hashedPasswordBytes[i] & 0x0f];
}
}
if (storedSubKeyString === '') { return false }
// password provided by the user
var password = 'welcome1';
// TODO remove debug - logging passwords in prod is considered
// tasteless for some odd reason
console.log('cleartext: ' + password);
console.log('saltString: ' + saltString);
console.log('storedSubKeyString: ' + storedSubKeyString);
// 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 stored sub key
if (derivedKeyOctets.indexOf(storedSubKeyString) === 0) {
console.info("passwords match!");
} else {
console.warn("passwords DO NOT match!");
}
Here's another option which actually compares the bytes as opposed to converting to a string representation.
const crypto = require('crypto');
const password = 'Password123';
const storedHashString = 'J9IBFSw0U1EFsH/ysL+wak6wb8s=';
const storedSaltString = '2nX0MZPZlwiW8bYLlVrfjBYLBKM=';
const storedHashBytes = new Buffer.from(storedHashString, 'base64');
const storedSaltBytes = new Buffer.from(storedSaltString, 'base64');
crypto.pbkdf2(password, storedSaltBytes, 1000, 20, 'sha1',
(err, calculatedHashBytes) => {
const correct = calculatedHashBytes.equals(storedHashBytes);
console.log('Password is ' + (correct ? 'correct 😎' : 'incorrect 😭'));
}
);
1000 is the default number of iterations in System.Security.Cryptography.Rfc2898DeriveBytes and 20 is the number of bytes we are using to store the salt (again the default).
I know this is rather late, but I ran into an issue with reproducing C#'s Rfc2898DeriveBytes.GetBytes in Node, and kept coming back to this SO answer. I ended up creating a minimal class for my own usage, and I figured I'd share in case someone else was having the same issues. It's not perfect, but it works.
const crypto = require('crypto');
const $key = Symbol('key');
const $saltSize = Symbol('saltSize');
const $salt = Symbol('salt');
const $iterationCount = Symbol('iterationCount');
const $position = Symbol('position');
class Rfc2898DeriveBytes {
constructor(key, saltSize = 32, iterationCount = 1000) {
this[$key] = key;
this[$saltSize] = saltSize;
this[$iterationCount] = iterationCount;
this[$position] = 0;
this[$salt] = crypto.randomBytes(this[$saltSize]);
}
get salt() {
return this[$salt];
}
set salt(buffer) {
this[$salt] = buffer;
}
get iterationCount() {
return this[$iterationCount];
}
set iterationCount(count) {
this[$iterationCount] = count;
}
getBytes(byteCount) {
let position = this[$position];
let bytes = crypto.pbkdf2Sync(Buffer.from(this[$key]), this.salt, this.iterationCount, position + byteCount, 'sha1');
this[$position] += byteCount;
let result = Buffer.alloc(byteCount);
for (let i = 0; i < byteCount; i++) { result[i] = bytes[position + i]; }
return result;
}
}
module.exports = Rfc2898DeriveBytes;