crypto: add ECDH.convertKey to convert public keys

ECDH.convertKey is used to convert public keys between different
formats.

PR-URL: https://github.com/nodejs/node/pull/19080
Fixes: https://github.com/nodejs/node/issues/18977
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Wei-Wei Wu 2018-03-01 17:13:12 -08:00 committed by Tobias Nießen
parent a0adf56855
commit f2e02883e7
No known key found for this signature in database
GPG Key ID: 718207F8FD156B70
5 changed files with 275 additions and 45 deletions

View File

@ -656,6 +656,54 @@ assert.strictEqual(aliceSecret.toString('hex'), bobSecret.toString('hex'));
// OK
```
### ECDH.convertKey(key, curve[, inputEncoding[, outputEncoding[, format]]])
<!-- YAML
added: REPLACEME
-->
- `key` {string | Buffer | TypedArray | DataView}
- `curve` {string}
- `inputEncoding` {string}
- `outputEncoding` {string}
- `format` {string} Defaults to `uncompressed`.
Converts the EC Diffie-Hellman public key specified by `key` and `curve` to the
format specified by `format`. The `format` argument specifies point encoding
and can be `'compressed'`, `'uncompressed'` or `'hybrid'`. The supplied key is
interpreted using the specified `inputEncoding`, and the returned key is encoded
using the specified `outputEncoding`. Encodings can be `'latin1'`, `'hex'`,
or `'base64'`.
Use [`crypto.getCurves()`][] to obtain a list of available curve names.
On recent OpenSSL releases, `openssl ecparam -list_curves` will also display
the name and description of each available elliptic curve.
If `format` is not specified the point will be returned in `'uncompressed'`
format.
If the `inputEncoding` is not provided, `key` is expected to be a [`Buffer`][],
`TypedArray`, or `DataView`.
Example (uncompressing a key):
```js
const { ECDH } = require('crypto');
const ecdh = ECDH('secp256k1');
ecdh.generateKeys();
const compressedKey = ecdh.getPublicKey('hex', 'compressed');
const uncompressedKey = ECDH.convertKey(compressedKey,
'secp256k1',
'hex',
'hex',
'uncompressed');
// the converted key and the uncompressed public key should be the same
console.log(uncompressedKey === ecdh.getPublicKey('hex'));
```
### ecdh.computeSecret(otherPublicKey[, inputEncoding][, outputEncoding])
<!-- YAML
added: v0.11.14

View File

@ -14,7 +14,8 @@ const {
const {
DiffieHellman: _DiffieHellman,
DiffieHellmanGroup: _DiffieHellmanGroup,
ECDH: _ECDH
ECDH: _ECDH,
ECDHConvertKey: _ECDHConvertKey
} = process.binding('crypto');
const {
POINT_CONVERSION_COMPRESSED,
@ -84,11 +85,9 @@ DiffieHellmanGroup.prototype.generateKeys =
dhGenerateKeys;
function dhGenerateKeys(encoding) {
var keys = this._handle.generateKeys();
const keys = this._handle.generateKeys();
encoding = encoding || getDefaultEncoding();
if (encoding && encoding !== 'buffer')
keys = keys.toString(encoding);
return keys;
return encode(keys, encoding);
}
@ -100,12 +99,10 @@ function dhComputeSecret(key, inEnc, outEnc) {
const encoding = getDefaultEncoding();
inEnc = inEnc || encoding;
outEnc = outEnc || encoding;
var ret = this._handle.computeSecret(toBuf(key, inEnc));
const ret = this._handle.computeSecret(toBuf(key, inEnc));
if (typeof ret === 'string')
throw new ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY();
if (outEnc && outEnc !== 'buffer')
ret = ret.toString(outEnc);
return ret;
return encode(ret, outEnc);
}
@ -114,11 +111,9 @@ DiffieHellmanGroup.prototype.getPrime =
dhGetPrime;
function dhGetPrime(encoding) {
var prime = this._handle.getPrime();
const prime = this._handle.getPrime();
encoding = encoding || getDefaultEncoding();
if (encoding && encoding !== 'buffer')
prime = prime.toString(encoding);
return prime;
return encode(prime, encoding);
}
@ -127,11 +122,9 @@ DiffieHellmanGroup.prototype.getGenerator =
dhGetGenerator;
function dhGetGenerator(encoding) {
var generator = this._handle.getGenerator();
const generator = this._handle.getGenerator();
encoding = encoding || getDefaultEncoding();
if (encoding && encoding !== 'buffer')
generator = generator.toString(encoding);
return generator;
return encode(generator, encoding);
}
@ -140,11 +133,9 @@ DiffieHellmanGroup.prototype.getPublicKey =
dhGetPublicKey;
function dhGetPublicKey(encoding) {
var key = this._handle.getPublicKey();
const key = this._handle.getPublicKey();
encoding = encoding || getDefaultEncoding();
if (encoding && encoding !== 'buffer')
key = key.toString(encoding);
return key;
return encode(key, encoding);
}
@ -153,11 +144,9 @@ DiffieHellmanGroup.prototype.getPrivateKey =
dhGetPrivateKey;
function dhGetPrivateKey(encoding) {
var key = this._handle.getPrivateKey();
const key = this._handle.getPrivateKey();
encoding = encoding || getDefaultEncoding();
if (encoding && encoding !== 'buffer')
key = key.toString(encoding);
return key;
return encode(key, encoding);
}
@ -197,7 +186,40 @@ ECDH.prototype.generateKeys = function generateKeys(encoding, format) {
};
ECDH.prototype.getPublicKey = function getPublicKey(encoding, format) {
var f;
const f = getFormat(format);
const key = this._handle.getPublicKey(f);
encoding = encoding || getDefaultEncoding();
return encode(key, encoding);
};
ECDH.convertKey = function convertKey(key, curve, inEnc, outEnc, format) {
if (typeof key !== 'string' && !isArrayBufferView(key)) {
throw new ERR_INVALID_ARG_TYPE(
'key',
['string', 'Buffer', 'TypedArray', 'DataView']
);
}
if (typeof curve !== 'string') {
throw new ERR_INVALID_ARG_TYPE('curve', 'string');
}
const encoding = getDefaultEncoding();
inEnc = inEnc || encoding;
outEnc = outEnc || encoding;
const f = getFormat(format);
const convertedKey = _ECDHConvertKey(toBuf(key, inEnc), curve, f);
return encode(convertedKey, outEnc);
};
function encode(buffer, encoding) {
if (encoding && encoding !== 'buffer')
buffer = buffer.toString(encoding);
return buffer;
}
function getFormat(format) {
let f;
if (format) {
if (format === 'compressed')
f = POINT_CONVERSION_COMPRESSED;
@ -211,12 +233,8 @@ ECDH.prototype.getPublicKey = function getPublicKey(encoding, format) {
} else {
f = POINT_CONVERSION_UNCOMPRESSED;
}
var key = this._handle.getPublicKey(f);
encoding = encoding || getDefaultEncoding();
if (encoding && encoding !== 'buffer')
key = key.toString(encoding);
return key;
};
return f;
}
module.exports = {
DiffieHellman,

View File

@ -4731,31 +4731,31 @@ void ECDH::GenerateKeys(const FunctionCallbackInfo<Value>& args) {
}
EC_POINT* ECDH::BufferToPoint(char* data, size_t len) {
EC_POINT* ECDH::BufferToPoint(Environment* env,
const EC_GROUP* group,
char* data,
size_t len) {
EC_POINT* pub;
int r;
pub = EC_POINT_new(group_);
pub = EC_POINT_new(group);
if (pub == nullptr) {
env()->ThrowError("Failed to allocate EC_POINT for a public key");
env->ThrowError("Failed to allocate EC_POINT for a public key");
return nullptr;
}
r = EC_POINT_oct2point(
group_,
group,
pub,
reinterpret_cast<unsigned char*>(data),
len,
nullptr);
if (!r) {
goto fatal;
EC_POINT_free(pub);
return nullptr;
}
return pub;
fatal:
EC_POINT_free(pub);
return nullptr;
}
@ -4772,7 +4772,9 @@ void ECDH::ComputeSecret(const FunctionCallbackInfo<Value>& args) {
if (!ecdh->IsKeyPairValid())
return env->ThrowError("Invalid key pair");
EC_POINT* pub = ecdh->BufferToPoint(Buffer::Data(args[0]),
EC_POINT* pub = ECDH::BufferToPoint(env,
ecdh->group_,
Buffer::Data(args[0]),
Buffer::Length(args[0]));
if (pub == nullptr) {
args.GetReturnValue().Set(
@ -4921,7 +4923,9 @@ void ECDH::SetPublicKey(const FunctionCallbackInfo<Value>& args) {
MarkPopErrorOnReturn mark_pop_error_on_return;
EC_POINT* pub = ecdh->BufferToPoint(Buffer::Data(args[0].As<Object>()),
EC_POINT* pub = ECDH::BufferToPoint(env,
ecdh->group_,
Buffer::Data(args[0].As<Object>()),
Buffer::Length(args[0].As<Object>()));
if (pub == nullptr)
return env->ThrowError("Failed to convert Buffer to EC_POINT");
@ -5597,6 +5601,61 @@ void ExportChallenge(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(outString);
}
// Convert the input public key to compressed, uncompressed, or hybrid formats.
void ConvertKey(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_EQ(args.Length(), 3);
size_t len = Buffer::Length(args[0]);
if (len == 0)
return args.GetReturnValue().SetEmptyString();
node::Utf8Value curve(env->isolate(), args[1]);
int nid = OBJ_sn2nid(*curve);
if (nid == NID_undef)
return env->ThrowTypeError("Invalid ECDH curve name");
EC_GROUP* group = EC_GROUP_new_by_curve_name(nid);
if (group == nullptr)
return env->ThrowError("Failed to get EC_GROUP");
EC_POINT* pub = ECDH::BufferToPoint(env,
group,
Buffer::Data(args[0]),
len);
std::shared_ptr<void> cleanup(nullptr, [group, pub] (...) {
EC_GROUP_free(group);
EC_POINT_free(pub);
});
if (pub == nullptr)
return env->ThrowError("Failed to convert Buffer to EC_POINT");
point_conversion_form_t form =
static_cast<point_conversion_form_t>(args[2]->Uint32Value());
int size = EC_POINT_point2oct(group, pub, form, nullptr, 0, nullptr);
if (size == 0)
return env->ThrowError("Failed to get public key length");
unsigned char* out = node::Malloc<unsigned char>(size);
int r = EC_POINT_point2oct(group, pub, form, out, size, nullptr);
if (r != size) {
free(out);
return env->ThrowError("Failed to get public key");
}
Local<Object> buf =
Buffer::New(env, reinterpret_cast<char*>(out), size).ToLocalChecked();
args.GetReturnValue().Set(buf);
}
void TimingSafeEqual(const FunctionCallbackInfo<Value>& args) {
CHECK(Buffer::HasInstance(args[0]));
CHECK(Buffer::HasInstance(args[1]));
@ -5739,6 +5798,8 @@ void InitCrypto(Local<Object> target,
env->SetMethod(target, "certVerifySpkac", VerifySpkac);
env->SetMethod(target, "certExportPublicKey", ExportPublicKey);
env->SetMethod(target, "certExportChallenge", ExportChallenge);
env->SetMethod(target, "ECDHConvertKey", ConvertKey);
#ifndef OPENSSL_NO_ENGINE
env->SetMethod(target, "setEngine", SetEngine);
#endif // !OPENSSL_NO_ENGINE

View File

@ -627,6 +627,10 @@ class ECDH : public BaseObject {
}
static void Initialize(Environment* env, v8::Local<v8::Object> target);
static EC_POINT* BufferToPoint(Environment* env,
const EC_GROUP* group,
char* data,
size_t len);
protected:
ECDH(Environment* env, v8::Local<v8::Object> wrap, EC_KEY* key)
@ -645,8 +649,6 @@ class ECDH : public BaseObject {
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 IsKeyPairValid();
bool IsKeyValidForCurve(const BIGNUM* private_key);

View File

@ -0,0 +1,101 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const { ECDH, getCurves } = require('crypto');
// A valid private key for the secp256k1 curve.
const cafebabeKey = 'cafebabe'.repeat(8);
// Associated compressed and uncompressed public keys (points).
const cafebabePubPtComp =
'03672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3';
const cafebabePubPtUnComp =
'04672a31bfc59d3f04548ec9b7daeeba2f61814e8ccc40448045007f5479f693a3' +
'2e02c7f93d13dc2732b760ca377a5897b9dd41a1c1b29dc0442fdce6d0a04d1d';
// Invalid test: key argument is undefined.
common.expectsError(
() => ECDH.convertKey(),
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
});
// Invalid test: curve argument is undefined.
common.expectsError(
() => ECDH.convertKey(cafebabePubPtComp),
{
code: 'ERR_INVALID_ARG_TYPE',
type: TypeError,
});
// Invalid test: curve argument is invalid.
common.expectsError(
() => ECDH.convertKey(cafebabePubPtComp, 'badcurve'),
{
type: TypeError,
message: 'Invalid ECDH curve name'
});
if (getCurves().includes('secp256k1')) {
// Invalid test: format argument is undefined.
common.expectsError(
() => ECDH.convertKey(cafebabePubPtComp, 'secp256k1', 'hex', 'hex', 10),
{
code: 'ERR_CRYPTO_ECDH_INVALID_FORMAT',
type: TypeError,
message: 'Invalid ECDH format: 10'
});
// Point formats.
let uncompressed = ECDH.convertKey(cafebabePubPtComp,
'secp256k1',
'hex',
'buffer',
'uncompressed');
let compressed = ECDH.convertKey(cafebabePubPtComp,
'secp256k1',
'hex',
'buffer',
'compressed');
let hybrid = ECDH.convertKey(cafebabePubPtComp,
'secp256k1',
'hex',
'buffer',
'hybrid');
assert.strictEqual(uncompressed[0], 4);
let firstByte = compressed[0];
assert(firstByte === 2 || firstByte === 3);
firstByte = hybrid[0];
assert(firstByte === 6 || firstByte === 7);
// Format conversion from hex to hex
uncompressed = ECDH.convertKey(cafebabePubPtComp,
'secp256k1',
'hex',
'hex',
'uncompressed');
compressed = ECDH.convertKey(cafebabePubPtComp,
'secp256k1',
'hex',
'hex',
'compressed');
hybrid = ECDH.convertKey(cafebabePubPtComp,
'secp256k1',
'hex',
'hex',
'hybrid');
assert.strictEqual(uncompressed, cafebabePubPtUnComp);
assert.strictEqual(compressed, cafebabePubPtComp);
// Compare to getPublicKey.
const ecdh1 = ECDH('secp256k1');
ecdh1.generateKeys();
ecdh1.setPrivateKey(cafebabeKey, 'hex');
assert.strictEqual(ecdh1.getPublicKey('hex', 'uncompressed'), uncompressed);
assert.strictEqual(ecdh1.getPublicKey('hex', 'compressed'), compressed);
assert.strictEqual(ecdh1.getPublicKey('hex', 'hybrid'), hybrid);
}