crypto: introduce ECDH
authorFedor Indutny <fedor@indutny.com>
Wed, 27 Aug 2014 14:01:01 +0000 (18:01 +0400)
committerFedor Indutny <fedor@indutny.com>
Thu, 28 Aug 2014 20:27:09 +0000 (00:27 +0400)
doc/api/crypto.markdown
lib/crypto.js
src/node_constants.cc
src/node_crypto.cc
src/node_crypto.h
test/simple/test-crypto.js

index cc13aca..a414d9b 100644 (file)
@@ -517,6 +517,85 @@ Example (obtaining a shared secret):
     /* alice_secret and bob_secret should be the same */
     console.log(alice_secret == bob_secret);
 
+## crypto.createECDH(curve_name)
+
+Creates a Elliptic Curve (EC) Diffie-Hellman key exchange object using a
+predefined curve specified by `curve_name` string.
+
+## Class: ECDH
+
+The class for creating EC Diffie-Hellman key exchanges.
+
+Returned by `crypto.createECDH`.
+
+### ECDH.generateKeys([encoding[, format]])
+
+Generates private and public EC Diffie-Hellman key values, and returns
+the public key in the specified format and encoding. This key should be
+transferred to the other party.
+
+Format specifies point encoding and can be `'compressed'`, `'uncompressed'`, or
+`'hybrid'`. If no format is provided - the point will be returned in
+`'uncompressed'` format.
+
+Encoding can be `'binary'`, `'hex'`, or `'base64'`. If no encoding is provided,
+then a buffer is returned.
+
+### ECDH.computeSecret(other_public_key, [input_encoding], [output_encoding])
+
+Computes the shared secret using `other_public_key` as the other
+party's public key and returns the computed shared secret. Supplied
+key is interpreted using specified `input_encoding`, and secret is
+encoded using specified `output_encoding`. Encodings can be
+`'binary'`, `'hex'`, or `'base64'`. If the input encoding is not
+provided, then a buffer is expected.
+
+If no output encoding is given, then a buffer is returned.
+
+### ECDH.getPublicKey([encoding[, format]])
+
+Returns the EC Diffie-Hellman public key in the specified encoding and format.
+
+Format specifies point encoding and can be `'compressed'`, `'uncompressed'`, or
+`'hybrid'`. If no format is provided - the point will be returned in
+`'uncompressed'` format.
+
+Encoding can be `'binary'`, `'hex'`, or `'base64'`. If no encoding is provided,
+then a buffer is returned.
+
+### ECDH.getPrivateKey([encoding])
+
+Returns the EC Diffie-Hellman private key in the specified encoding,
+which can be `'binary'`, `'hex'`, or `'base64'`. If no encoding is
+provided, then a buffer is returned.
+
+### ECDH.setPublicKey(public_key, [encoding])
+
+Sets the EC Diffie-Hellman public key. Key encoding can be `'binary'`,
+`'hex'` or `'base64'`. If no encoding is provided, then a buffer is
+expected.
+
+### ECDH.setPrivateKey(private_key, [encoding])
+
+Sets the EC Diffie-Hellman private key. Key encoding can be `'binary'`,
+`'hex'` or `'base64'`. If no encoding is provided, then a buffer is
+expected.
+
+Example (obtaining a shared secret):
+
+    var crypto = require('crypto');
+    var alice = crypto.createECDH('secp256k1');
+    var bob = crypto.createECDH('secp256k1');
+
+    alice.generateKeys();
+    bob.generateKeys();
+
+    var alice_secret = alice.computeSecret(bob.getPublicKey(), null, 'hex');
+    var bob_secret = bob.computeSecret(alice.getPublicKey(), null, 'hex');
+
+    /* alice_secret and bob_secret should be the same */
+    console.log(alice_secret == bob_secret);
+
 ## crypto.pbkdf2(password, salt, iterations, keylen, [digest], callback)
 
 Asynchronous PBKDF2 function.  Applies the selected HMAC digest function
index 828c0f4..a38ccb7 100644 (file)
@@ -514,6 +514,53 @@ DiffieHellman.prototype.setPrivateKey = function(key, encoding) {
 };
 
 
+function ECDH(curve) {
+  if (!util.isString(curve))
+    throw new TypeError('curve should be a string');
+
+  this._handle = new binding.ECDH(curve);
+}
+
+exports.createECDH = function createECDH(curve) {
+  return new ECDH(curve);
+};
+
+ECDH.prototype.computeSecret = DiffieHellman.prototype.computeSecret;
+ECDH.prototype.setPrivateKey = DiffieHellman.prototype.setPrivateKey;
+ECDH.prototype.setPublicKey = DiffieHellman.prototype.setPublicKey;
+ECDH.prototype.getPrivateKey = DiffieHellman.prototype.getPrivateKey;
+
+ECDH.prototype.generateKeys = function generateKeys(encoding, format) {
+  this._handle.generateKeys();
+
+  return this.getPublicKey(encoding, format);
+};
+
+ECDH.prototype.getPublicKey = function getPublicKey(encoding, format) {
+  var f;
+  if (format) {
+    if (typeof format === 'number')
+      f = format;
+    if (format === 'compressed')
+      f = constants.POINT_CONVERSION_COMPRESSED;
+    else if (format === 'hybrid')
+      f = constants.POINT_CONVERSION_HYBRID;
+    // Default
+    else if (format === 'uncompressed')
+      f = constants.POINT_CONVERSION_UNCOMPRESSED;
+    else
+      throw TypeError('Bad format: ' + format);
+  } else {
+    f = constants.POINT_CONVERSION_UNCOMPRESSED;
+  }
+  var key = this._handle.getPublicKey(f);
+  encoding = encoding || exports.DEFAULT_ENCODING;
+  if (encoding && encoding !== 'buffer')
+    key = key.toString(encoding);
+  return key;
+};
+
+
 
 exports.pbkdf2 = function(password,
                           salt,
index 430a09c..118824e 100644 (file)
@@ -33,6 +33,7 @@
 #include <sys/stat.h>
 
 #if HAVE_OPENSSL
+# include <openssl/ec.h>
 # include <openssl/ssl.h>
 # ifndef OPENSSL_NO_ENGINE
 #  include <openssl/engine.h>
@@ -974,6 +975,13 @@ void DefineOpenSSLConstants(Handle<Object> target) {
 #ifdef RSA_PKCS1_PSS_PADDING
     NODE_DEFINE_CONSTANT(target, RSA_PKCS1_PSS_PADDING);
 #endif
+
+  // NOTE: These are not defines
+  NODE_DEFINE_CONSTANT(target, POINT_CONVERSION_COMPRESSED);
+
+  NODE_DEFINE_CONSTANT(target, POINT_CONVERSION_UNCOMPRESSED);
+
+  NODE_DEFINE_CONSTANT(target, POINT_CONVERSION_HYBRID);
 }
 
 void DefineSystemConstants(Handle<Object> target) {
index 6085a18..6adedee 100644 (file)
@@ -4085,6 +4085,224 @@ bool DiffieHellman::VerifyContext() {
 }
 
 
+void ECDH::Initialize(Environment* env, Handle<Object> target) {
+  HandleScope scope(env->isolate());
+
+  Local<FunctionTemplate> t = FunctionTemplate::New(env->isolate(), New);
+
+  t->InstanceTemplate()->SetInternalFieldCount(1);
+
+  NODE_SET_PROTOTYPE_METHOD(t, "generateKeys", GenerateKeys);
+  NODE_SET_PROTOTYPE_METHOD(t, "computeSecret", ComputeSecret);
+  NODE_SET_PROTOTYPE_METHOD(t, "getPublicKey", GetPublicKey);
+  NODE_SET_PROTOTYPE_METHOD(t, "getPrivateKey", GetPrivateKey);
+  NODE_SET_PROTOTYPE_METHOD(t, "setPublicKey", SetPublicKey);
+  NODE_SET_PROTOTYPE_METHOD(t, "setPrivateKey", SetPrivateKey);
+
+  target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "ECDH"),
+              t->GetFunction());
+}
+
+
+void ECDH::New(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args.GetIsolate());
+  HandleScope scope(env->isolate());
+
+  // TODO(indutny): Support raw curves?
+  CHECK(args[0]->IsString());
+  node::Utf8Value curve(args[0]);
+
+  int nid = OBJ_sn2nid(*curve);
+  if (nid == NID_undef)
+    return env->ThrowTypeError("First argument should be a valid curve name");
+
+  EC_KEY* key = EC_KEY_new_by_curve_name(nid);
+  if (key == NULL)
+    return env->ThrowError("Failed to create EC_KEY using curve name");
+
+  new ECDH(env, args.This(), key);
+}
+
+
+void ECDH::GenerateKeys(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args.GetIsolate());
+  HandleScope scope(env->isolate());
+
+  ECDH* ecdh = Unwrap<ECDH>(args.Holder());
+
+  if (!EC_KEY_generate_key(ecdh->key_))
+    return env->ThrowError("Failed to generate EC_KEY");
+
+  ecdh->generated_ = true;
+}
+
+
+EC_POINT* ECDH::BufferToPoint(char* data, size_t len) {
+  EC_POINT* pub;
+  int r;
+
+  pub = EC_POINT_new(group_);
+  if (pub == NULL) {
+    env()->ThrowError("Failed to allocate EC_POINT for a public key");
+    return NULL;
+  }
+
+  r = EC_POINT_oct2point(
+      group_,
+      pub,
+      reinterpret_cast<unsigned char*>(data),
+      len,
+      NULL);
+  if (!r) {
+    env()->ThrowError("Failed to translate Buffer to a EC_POINT");
+    goto fatal;
+  }
+
+  return pub;
+
+ fatal:
+  EC_POINT_free(pub);
+  return NULL;
+}
+
+
+void ECDH::ComputeSecret(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args.GetIsolate());
+  HandleScope scope(env->isolate());
+
+  ASSERT_IS_BUFFER(args[0]);
+
+  ECDH* ecdh = Unwrap<ECDH>(args.Holder());
+
+  EC_POINT* pub = ecdh->BufferToPoint(Buffer::Data(args[0]),
+                                      Buffer::Length(args[0]));
+  if (pub == NULL)
+    return;
+
+  // NOTE: field_size is in bits
+  int field_size = EC_GROUP_get_degree(ecdh->group_);
+  size_t out_len = (field_size + 7) / 8;
+  char* out = static_cast<char*>(malloc(out_len));
+  CHECK_NE(out, NULL);
+
+  int r = ECDH_compute_key(out, out_len, pub, ecdh->key_, NULL);
+  EC_POINT_free(pub);
+  if (!r) {
+    free(out);
+    return env->ThrowError("Failed to compute ECDH key");
+  }
+
+  args.GetReturnValue().Set(Buffer::Use(env, out, out_len));
+}
+
+
+void ECDH::GetPublicKey(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args.GetIsolate());
+  HandleScope scope(env->isolate());
+
+  // Conversion form
+  CHECK_EQ(args.Length(), 1);
+
+  ECDH* ecdh = Unwrap<ECDH>(args.Holder());
+
+  if (!ecdh->generated_)
+    return env->ThrowError("You should generate ECDH keys first");
+
+  const EC_POINT* pub = EC_KEY_get0_public_key(ecdh->key_);
+  if (pub == NULL)
+    return env->ThrowError("Failed to get ECDH public key");
+
+  int size;
+  point_conversion_form_t form =
+      static_cast<point_conversion_form_t>(args[0]->Uint32Value());
+
+  size = EC_POINT_point2oct(ecdh->group_, pub, form, NULL, 0, NULL);
+  if (size == 0)
+    return env->ThrowError("Failed to get public key length");
+
+  unsigned char* out = static_cast<unsigned char*>(malloc(size));
+  CHECK_NE(out, NULL);
+
+  int r = EC_POINT_point2oct(ecdh->group_, pub, form, out, size, NULL);
+  if (r != size) {
+    free(out);
+    return env->ThrowError("Failed to get public key");
+  }
+
+  args.GetReturnValue().Set(Buffer::Use(env,
+                                        reinterpret_cast<char*>(out),
+                                        size));
+}
+
+
+void ECDH::GetPrivateKey(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args.GetIsolate());
+  HandleScope scope(env->isolate());
+
+  ECDH* ecdh = Unwrap<ECDH>(args.Holder());
+
+  if (!ecdh->generated_)
+    return env->ThrowError("You should generate ECDH keys first");
+
+  const BIGNUM* b = EC_KEY_get0_private_key(ecdh->key_);
+  if (b == NULL)
+    return env->ThrowError("Failed to get ECDH private key");
+
+  int size = BN_num_bytes(b);
+  unsigned char* out = static_cast<unsigned char*>(malloc(size));
+  CHECK_NE(out, NULL);
+
+  if (size != BN_bn2bin(b, out)) {
+    free(out);
+    return env->ThrowError("Failed to convert ECDH private key to Buffer");
+  }
+
+  args.GetReturnValue().Set(Buffer::Use(env,
+                                        reinterpret_cast<char*>(out),
+                                        size));
+}
+
+
+void ECDH::SetPrivateKey(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args.GetIsolate());
+  HandleScope scope(env->isolate());
+
+  ECDH* ecdh = Unwrap<ECDH>(args.Holder());
+
+  ASSERT_IS_BUFFER(args[0]);
+
+  BIGNUM* priv = BN_bin2bn(
+      reinterpret_cast<unsigned char*>(Buffer::Data(args[0].As<Object>())),
+      Buffer::Length(args[0].As<Object>()),
+      NULL);
+  if (priv == NULL)
+    return env->ThrowError("Failed to convert Buffer to BN");
+
+  if (!EC_KEY_set_private_key(ecdh->key_, priv))
+    return env->ThrowError("Failed to convert BN to a private key");
+}
+
+
+void ECDH::SetPublicKey(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args.GetIsolate());
+  HandleScope scope(env->isolate());
+
+  ECDH* ecdh = Unwrap<ECDH>(args.Holder());
+
+  ASSERT_IS_BUFFER(args[0]);
+
+  EC_POINT* pub = ecdh->BufferToPoint(Buffer::Data(args[0].As<Object>()),
+                                      Buffer::Length(args[0].As<Object>()));
+  if (pub == NULL)
+    return;
+
+  int r = EC_KEY_set_public_key(ecdh->key_, pub);
+  EC_POINT_free(pub);
+  if (!r)
+    return env->ThrowError("Failed to convert BN to a private key");
+}
+
+
 class PBKDF2Request : public AsyncWrap {
  public:
   PBKDF2Request(Environment* env,
@@ -4855,6 +5073,7 @@ void InitCrypto(Handle<Object> target,
   Connection::Initialize(env, target);
   CipherBase::Initialize(env, target);
   DiffieHellman::Initialize(env, target);
+  ECDH::Initialize(env, target);
   Hmac::Initialize(env, target);
   Hash::Initialize(env, target);
   Sign::Initialize(env, target);
index 2a02c89..178afc8 100644 (file)
@@ -39,6 +39,8 @@
 #include "v8.h"
 
 #include <openssl/ssl.h>
+#include <openssl/ec.h>
+#include <openssl/ecdh.h>
 #ifndef OPENSSL_NO_ENGINE
 # include <openssl/engine.h>
 #endif  // !OPENSSL_NO_ENGINE
@@ -635,6 +637,42 @@ class DiffieHellman : public BaseObject {
   DH* dh;
 };
 
+class ECDH : public BaseObject {
+ public:
+  ~ECDH() {
+    if (key_ != NULL)
+      EC_KEY_free(key_);
+    key_ = NULL;
+    group_ = NULL;
+  }
+
+  static void Initialize(Environment* env, v8::Handle<v8::Object> target);
+
+ protected:
+  ECDH(Environment* env, v8::Local<v8::Object> wrap, EC_KEY* key)
+      : BaseObject(env, wrap),
+        generated_(false),
+        key_(key),
+        group_(EC_KEY_get0_group(key_)) {
+    MakeWeak<ECDH>(this);
+    ASSERT(group_ != NULL);
+  }
+
+  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void GenerateKeys(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void ComputeSecret(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void GetPrivateKey(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void SetPrivateKey(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void GetPublicKey(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void SetPublicKey(const v8::FunctionCallbackInfo<v8::Value>& args);
+
+  EC_POINT* BufferToPoint(char* data, size_t len);
+
+  bool generated_;
+  EC_KEY* key_;
+  const EC_GROUP* group_;
+};
+
 class Certificate : public AsyncWrap {
  public:
   static void Initialize(Environment* env, v8::Handle<v8::Object> target);
index 74baaa7..4b62338 100644 (file)
@@ -1167,3 +1167,38 @@ assert.throws(function() {
 
 // Make sure memory isn't released before being returned
 console.log(crypto.randomBytes(16));
+
+// Test ECDH
+var ecdh1 = crypto.createECDH('prime256v1');
+var ecdh2 = crypto.createECDH('prime256v1');
+var key1 = ecdh1.generateKeys();
+var key2 = ecdh2.generateKeys('hex');
+var secret1 = ecdh1.computeSecret(key2, 'hex', 'base64');
+var secret2 = ecdh2.computeSecret(key1, 'binary', 'buffer');
+
+assert.equal(secret1, secret2.toString('base64'));
+
+// Point formats
+assert.equal(ecdh1.getPublicKey('buffer', 'uncompressed')[0], 4);
+var firstByte = ecdh1.getPublicKey('buffer', 'compressed')[0];
+assert(firstByte === 2 || firstByte === 3);
+var firstByte = ecdh1.getPublicKey('buffer', 'hybrid')[0];
+assert(firstByte === 6 || firstByte === 7);
+
+// ECDH should check that point is on curve
+var ecdh3 = crypto.createECDH('secp256k1');
+var key3 = ecdh3.generateKeys();
+
+assert.throws(function() {
+  var secret3 = ecdh2.computeSecret(key3, 'binary', 'buffer');
+});
+
+// ECDH should allow .setPrivateKey()/.setPublicKey()
+var ecdh4 = crypto.createECDH('prime256v1');
+
+ecdh4.setPrivateKey(ecdh1.getPrivateKey());
+ecdh4.setPublicKey(ecdh1.getPublicKey());
+
+assert.throws(function() {
+  ecdh4.setPublicKey(ecdh3.getPublicKey());
+});