diff --git a/doc/api/crypto.markdown b/doc/api/crypto.markdown index cc13acacc3..a414d9b922 100644 --- a/doc/api/crypto.markdown +++ b/doc/api/crypto.markdown @@ -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 diff --git a/lib/crypto.js b/lib/crypto.js index 828c0f4310..a38ccb77c1 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -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, diff --git a/src/node_constants.cc b/src/node_constants.cc index 430a09c685..118824e95d 100644 --- a/src/node_constants.cc +++ b/src/node_constants.cc @@ -33,6 +33,7 @@ #include #if HAVE_OPENSSL +# include # include # ifndef OPENSSL_NO_ENGINE # include @@ -974,6 +975,13 @@ void DefineOpenSSLConstants(Handle 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 target) { diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 6085a18a4d..6adedeeb77 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -4085,6 +4085,224 @@ bool DiffieHellman::VerifyContext() { } +void ECDH::Initialize(Environment* env, Handle target) { + HandleScope scope(env->isolate()); + + Local 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& 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& args) { + Environment* env = Environment::GetCurrent(args.GetIsolate()); + HandleScope scope(env->isolate()); + + ECDH* ecdh = Unwrap(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(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& args) { + Environment* env = Environment::GetCurrent(args.GetIsolate()); + HandleScope scope(env->isolate()); + + ASSERT_IS_BUFFER(args[0]); + + ECDH* ecdh = Unwrap(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(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& args) { + Environment* env = Environment::GetCurrent(args.GetIsolate()); + HandleScope scope(env->isolate()); + + // Conversion form + CHECK_EQ(args.Length(), 1); + + ECDH* ecdh = Unwrap(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(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(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(out), + size)); +} + + +void ECDH::GetPrivateKey(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args.GetIsolate()); + HandleScope scope(env->isolate()); + + ECDH* ecdh = Unwrap(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(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(out), + size)); +} + + +void ECDH::SetPrivateKey(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args.GetIsolate()); + HandleScope scope(env->isolate()); + + ECDH* ecdh = Unwrap(args.Holder()); + + ASSERT_IS_BUFFER(args[0]); + + BIGNUM* priv = BN_bin2bn( + reinterpret_cast(Buffer::Data(args[0].As())), + Buffer::Length(args[0].As()), + 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& args) { + Environment* env = Environment::GetCurrent(args.GetIsolate()); + HandleScope scope(env->isolate()); + + ECDH* ecdh = Unwrap(args.Holder()); + + ASSERT_IS_BUFFER(args[0]); + + EC_POINT* pub = ecdh->BufferToPoint(Buffer::Data(args[0].As()), + Buffer::Length(args[0].As())); + 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 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); diff --git a/src/node_crypto.h b/src/node_crypto.h index 2a02c89bc2..178afc80ea 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -39,6 +39,8 @@ #include "v8.h" #include +#include +#include #ifndef OPENSSL_NO_ENGINE # include #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 target); + + protected: + ECDH(Environment* env, v8::Local wrap, EC_KEY* key) + : BaseObject(env, wrap), + generated_(false), + key_(key), + group_(EC_KEY_get0_group(key_)) { + MakeWeak(this); + ASSERT(group_ != NULL); + } + + static void New(const v8::FunctionCallbackInfo& args); + static void GenerateKeys(const v8::FunctionCallbackInfo& args); + static void ComputeSecret(const v8::FunctionCallbackInfo& args); + static void GetPrivateKey(const v8::FunctionCallbackInfo& args); + static void SetPrivateKey(const v8::FunctionCallbackInfo& args); + static void GetPublicKey(const v8::FunctionCallbackInfo& args); + static void SetPublicKey(const v8::FunctionCallbackInfo& 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 target); diff --git a/test/simple/test-crypto.js b/test/simple/test-crypto.js index 74baaa71a0..4b623380b4 100644 --- a/test/simple/test-crypto.js +++ b/test/simple/test-crypto.js @@ -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()); +});