crypto: support custom pbkdf2 digest methods
authorBen Noordhuis <info@bnoordhuis.nl>
Thu, 21 Nov 2013 10:29:07 +0000 (11:29 +0100)
committerFedor Indutny <fedor.indutny@gmail.com>
Wed, 22 Jan 2014 11:58:07 +0000 (15:58 +0400)
Make the HMAC digest method configurable.  Update crypto.pbkdf2() and
crypto.pbkdf2Sync() to take an extra, optional digest argument.

Before this commit, SHA-1 (admittedly the most common method) was used
exclusively.

Fixes #6553.

doc/api/crypto.markdown
lib/crypto.js
src/node_crypto.cc
test/simple/test-crypto.js

index 35d4c39..82374df 100644 (file)
@@ -487,13 +487,25 @@ Example (obtaining a shared secret):
     /* alice_secret and bob_secret should be the same */
     console.log(alice_secret == bob_secret);
 
-## crypto.pbkdf2(password, salt, iterations, keylen, callback)
+## crypto.pbkdf2(password, salt, iterations, keylen, [digest], callback)
 
-Asynchronous PBKDF2 applies pseudorandom function HMAC-SHA1 to derive
-a key of given length from the given password, salt and iterations.
-The callback gets two arguments `(err, derivedKey)`.
+Asynchronous PBKDF2 function.  Applies the selected HMAC digest function
+(default: SHA1) to derive a key of the requested length from the password,
+salt and number of iterations.  The callback gets two arguments:
+`(err, derivedKey)`.
 
-## crypto.pbkdf2Sync(password, salt, iterations, keylen)
+Example:
+
+    crypto.pbkdf2('secret', 'salt', 4096, 512, 'sha256', function(err, key) {
+      if (err)
+        throw err;
+      console.log(key.toString('hex'));  // 'c5e478d...1469e50'
+    });
+
+You can get a list of supported digest functions with
+[crypto.getHashes()](#crypto_crypto_gethashes).
+
+## crypto.pbkdf2Sync(password, salt, iterations, keylen, [digest])
 
 Synchronous PBKDF2 function.  Returns derivedKey or throws error.
 
index 050d54a..8226241 100644 (file)
@@ -575,36 +575,47 @@ DiffieHellman.prototype.setPrivateKey = function(key, encoding) {
 
 
 
-exports.pbkdf2 = function(password, salt, iterations, keylen, callback) {
+exports.pbkdf2 = function(password,
+                          salt,
+                          iterations,
+                          keylen,
+                          digest,
+                          callback) {
+  if (util.isFunction(digest)) {
+    callback = digest;
+    digest = undefined;
+  }
+
   if (!util.isFunction(callback))
     throw new Error('No callback provided to pbkdf2');
 
-  return pbkdf2(password, salt, iterations, keylen, callback);
+  return pbkdf2(password, salt, iterations, keylen, digest, callback);
 };
 
 
-exports.pbkdf2Sync = function(password, salt, iterations, keylen) {
-  return pbkdf2(password, salt, iterations, keylen);
+exports.pbkdf2Sync = function(password, salt, iterations, keylen, digest) {
+  return pbkdf2(password, salt, iterations, keylen, digest);
 };
 
 
-function pbkdf2(password, salt, iterations, keylen, callback) {
+function pbkdf2(password, salt, iterations, keylen, digest, callback) {
   password = toBuf(password);
   salt = toBuf(salt);
 
   if (exports.DEFAULT_ENCODING === 'buffer')
-    return binding.PBKDF2(password, salt, iterations, keylen, callback);
+    return binding.PBKDF2(password, salt, iterations, keylen, digest, callback);
 
   // at this point, we need to handle encodings.
   var encoding = exports.DEFAULT_ENCODING;
   if (callback) {
-    binding.PBKDF2(password, salt, iterations, keylen, function(er, ret) {
+    function next(er, ret) {
       if (ret)
         ret = ret.toString(encoding);
       callback(er, ret);
-    });
+    }
+    binding.PBKDF2(password, salt, iterations, keylen, digest, next);
   } else {
-    var ret = binding.PBKDF2(password, salt, iterations, keylen);
+    var ret = binding.PBKDF2(password, salt, iterations, keylen, digest);
     return ret.toString(encoding);
   }
 }
index cf1b637..20f4137 100644 (file)
@@ -3468,6 +3468,7 @@ class PBKDF2Request : public AsyncWrap {
  public:
   PBKDF2Request(Environment* env,
                 Local<Object> object,
+                const EVP_MD* digest,
                 ssize_t passlen,
                 char* pass,
                 ssize_t saltlen,
@@ -3475,6 +3476,7 @@ class PBKDF2Request : public AsyncWrap {
                 ssize_t iter,
                 ssize_t keylen)
       : AsyncWrap(env, object),
+        digest_(digest),
         error_(0),
         passlen_(passlen),
         pass_(pass),
@@ -3495,6 +3497,10 @@ class PBKDF2Request : public AsyncWrap {
     return &work_req_;
   }
 
+  inline const EVP_MD* digest() const {
+    return digest_;
+  }
+
   inline ssize_t passlen() const {
     return passlen_;
   }
@@ -3544,6 +3550,7 @@ class PBKDF2Request : public AsyncWrap {
   uv_work_t work_req_;
 
  private:
+  const EVP_MD* digest_;
   int error_;
   ssize_t passlen_;
   char* pass_;
@@ -3556,12 +3563,13 @@ class PBKDF2Request : public AsyncWrap {
 
 
 void EIO_PBKDF2(PBKDF2Request* req) {
-  req->set_error(PKCS5_PBKDF2_HMAC_SHA1(
+  req->set_error(PKCS5_PBKDF2_HMAC(
     req->pass(),
     req->passlen(),
     reinterpret_cast<unsigned char*>(req->salt()),
     req->saltlen(),
     req->iter(),
+    req->digest(),
     req->keylen(),
     reinterpret_cast<unsigned char*>(req->key())));
   memset(req->pass(), 0, req->passlen());
@@ -3606,6 +3614,7 @@ void PBKDF2(const FunctionCallbackInfo<Value>& args) {
   HandleScope handle_scope(args.GetIsolate());
   Environment* env = Environment::GetCurrent(args.GetIsolate());
 
+  const EVP_MD* digest = NULL;
   const char* type_error = NULL;
   char* pass = NULL;
   char* salt = NULL;
@@ -3618,7 +3627,7 @@ void PBKDF2(const FunctionCallbackInfo<Value>& args) {
   PBKDF2Request* req = NULL;
   Local<Object> obj;
 
-  if (args.Length() != 4 && args.Length() != 5) {
+  if (args.Length() != 5 && args.Length() != 6) {
     type_error = "Bad parameter";
     goto err;
   }
@@ -3673,11 +3682,32 @@ void PBKDF2(const FunctionCallbackInfo<Value>& args) {
     goto err;
   }
 
-  obj = Object::New();
-  req = new PBKDF2Request(env, obj, passlen, pass, saltlen, salt, iter, keylen);
+  if (args[4]->IsString()) {
+    String::Utf8Value digest_name(args[4]);
+    digest = EVP_get_digestbyname(*digest_name);
+    if (digest == NULL) {
+      type_error = "Bad digest name";
+      goto err;
+    }
+  }
+
+  if (digest == NULL) {
+    digest = EVP_sha1();
+  }
 
-  if (args[4]->IsFunction()) {
-    obj->Set(env->ondone_string(), args[4]);
+  obj = Object::New();
+  req = new PBKDF2Request(env,
+                          obj,
+                          digest,
+                          passlen,
+                          pass,
+                          saltlen,
+                          salt,
+                          iter,
+                          keylen);
+
+  if (args[5]->IsFunction()) {
+    obj->Set(env->ondone_string(), args[5]);
     // XXX(trevnorris): This will need to go with the rest of domains.
     if (env->in_domain())
       obj->Set(env->domain_string(), env->domain_array()->Get(0));
index 47a8f73..7afb958 100644 (file)
@@ -912,6 +912,19 @@ testPBKDF2('pass\0word', 'sa\0lt', 4096, 16,
            '\x56\xfa\x6a\xa7\x55\x48\x09\x9d\xcc\x37\xd7\xf0\x34' +
            '\x25\xe0\xc3');
 
+(function() {
+  var expected =
+      '64c486c55d30d4c5a079b8823b7d7cb37ff0556f537da8410233bcec330ed956';
+  var key = crypto.pbkdf2Sync('password', 'salt', 32, 32, 'sha256');
+  assert.equal(key.toString('hex'), expected);
+
+  crypto.pbkdf2('password', 'salt', 32, 32, 'sha256', common.mustCall(ondone));
+  function ondone(err, key) {
+    if (err) throw err;
+    assert.equal(key.toString('hex'), expected);
+  }
+})();
+
 function assertSorted(list) {
   assert.deepEqual(list, list.sort());
 }