3

I'm facing a issue now which need you guys help.

I use c# to do some encryption. Then need to use node.js to decry-pt it. But I just found that I can't do it correctly base on my c# encryption algorithm. If you guys have any solution, please help me.

Here is my c# encryption code:

public static string Encrypt(string text, String password, string salt, string hashAlgorithm, int passwordIterations, string initialVector, int keySize)
{
    if (string.IsNullOrEmpty(text))
        return "";

    var initialVectorBytes = Encoding.ASCII.GetBytes(initialVector);
    var saltValueBytes = Encoding.ASCII.GetBytes(salt);
    var plainTextBytes = Encoding.UTF8.GetBytes(text);
    var derivedPassword = new PasswordDeriveBytes(password, saltValueBytes, hashAlgorithm, passwordIterations);
    var keyBytes = derivedPassword.GetBytes(keySize / 8);
    var symmetricKey = new RijndaelManaged();
    symmetricKey.Mode = CipherMode.CBC;
    byte[] cipherTextBytes = null;
    using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, initialVectorBytes))
    {
        using (var memStream = new MemoryStream())
        {
            using (var cryptoStream = new CryptoStream(memStream, encryptor, CryptoStreamMode.Write))
            {
                cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                cryptoStream.FlushFinalBlock();
                cipherTextBytes = memStream.ToArray();
                memStream.Close();
                cryptoStream.Close();
            }
        }
    }
    symmetricKey.Clear();
    return Convert.ToBase64String(cipherTextBytes);
}
public static string Decrypt(string text, String password, string salt, string hashAlgorithm, int passwordIterations, string initialVector, int keySize)
{
    if (string.IsNullOrEmpty(text))
        return "";

    var initialVectorBytes = Encoding.ASCII.GetBytes(initialVector);
    var saltValueBytes = Encoding.ASCII.GetBytes(salt);
    var cipherTextBytes = Convert.FromBase64String(text);
    var derivedPassword = new PasswordDeriveBytes(password, saltValueBytes, hashAlgorithm, passwordIterations);
    var keyBytes = derivedPassword.GetBytes(keySize / 8);
    var symmetricKey = new RijndaelManaged();
    symmetricKey.Mode = CipherMode.CBC;
    var plainTextBytes = new byte[cipherTextBytes.Length];
    var byteCount = 0;
    using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, initialVectorBytes))
    {
        using (var memStream = new MemoryStream(cipherTextBytes))
        {
            using (var cryptoStream = new CryptoStream(memStream, decryptor, CryptoStreamMode.Read))
            {

                byteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
                memStream.Close();
                cryptoStream.Close();
            }
        }
    }
    symmetricKey.Clear();
    return Encoding.UTF8.GetString(plainTextBytes, 0, byteCount);    
}

If anyone can give me a same function for nodejs, that's will be really help. Anyway, thanks for read this post.

6
  • Look at the syntax highlighting. This is why you should follow convention and not use UpperCase variables names. Commented Apr 29, 2013 at 3:24
  • 1
    had to clean that up @JonathonReinhart, too painful :) Commented Apr 29, 2013 at 3:36
  • sorry guys, I will make sure clean next time for sure. Thanks for clean my code. Commented Apr 29, 2013 at 3:38
  • Have you tried using the crypto module? Commented Apr 29, 2013 at 3:39
  • yes I do.I have try this:crypto.pbkdf2(password, salt, iterations, keySize / 8, function(err, key) { var decipher = crypto.createDecipheriv('aes-128-cbc', key, initialVector); var dec = decipher.update(text, 'base64', 'utf8') + decipher.final('utf8'); }); not working, and I found there is a "hashAlgorithm" I have used in c#, but not in Node. Any comments? Commented Apr 29, 2013 at 5:25

3 Answers 3

3

The problem is, that PasswordDeriveBytes does not implement pbkdf2 but a modified version of pbkdf1 (PKCS #5 v2.1). See this what-is-the-algorithm-behind-passwordderivebytes for more information.

Note: The defaults of the PasswordDeriveBytes constructor are iterations = 100 and hashAlgorithm = "sha1"

Here is my approach to implement this algorithm in javascript/typescript:

export function deriveBytesFromPassword(password: string, salt: Buffer, iterations: number, hashAlgorithm: string, keyLength: number) {
    if (keyLength < 1) throw new Error("keyLength must be greater than 1")
    if (iterations < 2) throw new Error("iterations must be greater than 2")

    const passwordWithSalt = Buffer.concat([Buffer.from(password, "utf-8"), salt])
    const hashMissingLastIteration = hashKeyNTimes(passwordWithSalt, iterations - 1, hashAlgorithm)
    let result = hashKeyNTimes(hashMissingLastIteration, 1, hashAlgorithm)
    result = extendResultIfNeeded(result, keyLength, hashMissingLastIteration, hashAlgorithm)

    return result.slice(0, keyLength)
}

function hashKeyNTimes(key: Buffer, times: number, hashAlgorithm: string): Buffer {
    let result = key
    for (let i = 0; i < times; i++) {
        result = crypto.createHash(hashAlgorithm).update(result).digest()
    }
    return result
}

function extendResultIfNeeded(result: Buffer, keyLength: number, hashMissingLastIteration: Buffer, hashAlgorithm: string): Buffer {
    let counter = 1
    while (result.length < keyLength) {
        result = Buffer.concat([result, calculateSpecialMicrosoftHash(hashMissingLastIteration, counter, hashAlgorithm)])
        counter++
    }
    return result
}

function calculateSpecialMicrosoftHash(hashMissingLastIteration: Buffer, counter: number, hashAlgorithm: string): Buffer {
    // Here comes the magic: Convert an integer that increases from call to call to a string
    // and convert that string to utf-8 bytes. These bytes are than used to slightly modify a given base-hash.
    // The modified hash is than piped through the hash algorithm.
    // Note: The PasswordDeriveBytes algorithm converts each character to utf-16 and then drops the second byte.
    const prefixCalculatedByCounter = Buffer.from(counter.toString(), "utf-8")
    const inputForAdditionalHashIteration = Buffer.concat([prefixCalculatedByCounter, hashMissingLastIteration])
    return crypto.createHash(hashAlgorithm).update(inputForAdditionalHashIteration).digest()
}
Sign up to request clarification or add additional context in comments.

2 Comments

This was super helpful! I compared the output of deriveBytesFromPassword(password, salt, 100, 'sha1', 32) to the C# output from PasswordDeriveBytes.GetBytes() and the byte values match exactly.
Perfect, this solve my issue!
1

First, we'll do the PasswordDeriveBytes function with Node.js. This is crypto.pbkdf2:

// assumes HMAC-SHA1
crypto.pbkdf2(password, salt, iterations, keySize / 8, function(err, key) {
    if(err) /* handle error */
    // ...
});

Next, use crypto.createDecipheriv to create a decryptor:

// find algorithm from the available ciphers; see crypto.getCiphers()
var decipher = crypto.createDecipheriv(/* algorithm */, key, initialVector);

Then use decipher.update and decipher.final to feed it the data. They will return portions of decrypted data to you.

2 Comments

I have try this:crypto.pbkdf2(password, salt, iterations, keySize / 8, function(err, key) { var decipher = crypto.createDecipheriv('aes-128-cbc', key, initialVector); var dec = decipher.update(text, 'base64', 'utf8') + decipher.final('utf8'); }); not working, and I found there is a "hashAlgorithm" I have used in c#, but not in Node. Any comments?
@Zhe: For PBKDF2, Node.js only supports HMAC-SHA1.
0

Having the exact same scenario I was able to get to a successful "C# encrypt => Node decrypt" solution using the code provided by @icktoofay above, but with the PasswordDeriveBytes replaced with Rfc2898DeriveBytes

My code is roughly:

C#

private byte[] saltBytes = ASCIIEncoding.ASCII.GetBytes(salt);

public string Encrypt<T>(string value, string password) where T: SymmetricAlgorithm, new() {
  byte[] valueBytes = UTF8Encoding.UTF8.GetBytes(value);

  byte[] encrypted = null;
  using (T cipher = new T()) {
    var db = new Rfc2898DeriveBytes(password, saltBytes);
    db.IterationCount = iterationsConst;
    var key = db.GetBytes(keySizeConst / 8);

    cipher.Mode = CipherMode.CBC;

    using (ICryptoTransform encryptor = cipher.CreateEncryptor(key, vectorBytes)) {
      using (MemoryStream ms = new MemoryStream()) {
        using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) {
          cs.Write(valueBytes, 0, valueBytes.Length);
          cs.FlushFinalBlock();
          encrypted = ms.ToArray();
        }
      }
    }
    cipher.Clear();
  }
  return Convert.ToBase64String(encrypted);
}

JavaScript:

var crypto = require('crypto');
var base64 = require('base64-js');
var algorithm = 'AES-256-CBC';

[...]

var saltBuffer = new Buffer(salt);
var passwordBuffer = new Buffer(password);

[...]

var encodedBuffer = new Buffer(base64.toByteArray(encryptedStringBase64Encoded));

crypto.pbkdf2(passwordBuffer, saltBuffer, iterations, keySize / 8, function(err, key) {
  var decipher = crypto.createDecipheriv(algorithm, key, iv);
  var dec = Buffer.concat([decipher.update(encodedBuffer), decipher.final()]);
  return dec;
});

and is actually a combination of a few examples I found on the Internet.

Because I had a problem with the Buffer's implementation of Base64 in some specific cases ('+' sign at the beginning of the encoded string), I used base64-js from https://github.com/beatgammit/base64-js, which seems to work fine.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.