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:
parent
a0adf56855
commit
f2e02883e7
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
101
test/parallel/test-crypto-ecdh-convert-key.js
Normal file
101
test/parallel/test-crypto-ecdh-convert-key.js
Normal 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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user