Skip to content

Commit 7cf4753

Browse files
bartlomiejuclaude
andauthored
fix(ext/node): implement ECDH.convertKey and fix related ECDH bugs (#32532)
## Summary - Implement `ECDH.convertKey()` static method supporting compressed, uncompressed, and hybrid point formats - Fix `setPrivateKey()` not decoding string keys with encoding parameter - Fix `getPublicKey()` not supporting hybrid format - Fix `op_node_ecdh_compute_public_key` using `to_sec1_bytes()` (compressed, 33 bytes) instead of `to_encoded_point(false)` (uncompressed, 65 bytes), which caused a panic on `copy_from_slice` - Enable `test-crypto-ecdh-convert-key.js` in node_compat config --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f123d84 commit 7cf4753

File tree

3 files changed

+68
-14
lines changed

3 files changed

+68
-14
lines changed

ext/node/polyfills/internal/crypto/diffiehellman.ts

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import {
1515
op_node_gen_prime,
1616
} from "ext:core/ops";
1717

18-
import { notImplemented } from "ext:deno_node/_utils.ts";
1918
import {
2019
isAnyArrayBuffer,
2120
isArrayBufferView,
2221
} from "ext:deno_node/internal/util/types.ts";
2322
import {
23+
ERR_CRYPTO_ECDH_INVALID_FORMAT,
2424
ERR_CRYPTO_UNKNOWN_DH_GROUP,
2525
ERR_INVALID_ARG_TYPE,
2626
NodeError,
@@ -1230,13 +1230,55 @@ export class ECDHImpl {
12301230
}
12311231

12321232
static convertKey(
1233-
_key: BinaryLike,
1234-
_curve: string,
1235-
_inputEncoding?: BinaryToTextEncoding,
1236-
_outputEncoding?: "latin1" | "hex" | "base64" | "base64url",
1237-
_format?: "uncompressed" | "compressed" | "hybrid",
1233+
key: BinaryLike,
1234+
curve: string,
1235+
inputEncoding?: BinaryToTextEncoding,
1236+
outputEncoding?: "latin1" | "hex" | "base64" | "base64url",
1237+
format?: "uncompressed" | "compressed" | "hybrid",
12381238
): Buffer | string {
1239-
notImplemented("crypto.ECDH.prototype.convertKey");
1239+
validateString(curve, "curve");
1240+
const buf = getArrayBufferOrView(key, "key", inputEncoding);
1241+
1242+
let compress: boolean;
1243+
if (format) {
1244+
if (format === "compressed") {
1245+
compress = true;
1246+
} else if (format === "hybrid" || format === "uncompressed") {
1247+
compress = false;
1248+
} else {
1249+
throw new ERR_CRYPTO_ECDH_INVALID_FORMAT(format);
1250+
}
1251+
} else {
1252+
compress = false;
1253+
}
1254+
1255+
let result;
1256+
try {
1257+
result = Buffer.from(
1258+
op_node_ecdh_encode_pubkey(curve, buf, compress),
1259+
);
1260+
} catch (e) {
1261+
if (e instanceof TypeError && e.message === "Unsupported curve") {
1262+
throw new TypeError("Invalid EC curve name");
1263+
}
1264+
throw new Error("Failed to convert Buffer to EC_POINT");
1265+
}
1266+
1267+
if (format === "hybrid") {
1268+
// Hybrid format: same as uncompressed but first byte is 06 or 07
1269+
// Get compressed form to determine parity
1270+
const compressedBuf = Buffer.from(
1271+
op_node_ecdh_encode_pubkey(curve, buf, true),
1272+
);
1273+
// compressed first byte is 02 (even) or 03 (odd)
1274+
// hybrid first byte is 06 (even) or 07 (odd)
1275+
result[0] = compressedBuf[0] + 4;
1276+
}
1277+
1278+
if (outputEncoding && outputEncoding !== "buffer") {
1279+
return result.toString(outputEncoding);
1280+
}
1281+
return result;
12401282
}
12411283

12421284
computeSecret(otherPublicKey: ArrayBufferView): Buffer;
@@ -1312,8 +1354,16 @@ export class ECDHImpl {
13121354
const pubbuf = Buffer.from(op_node_ecdh_encode_pubkey(
13131355
this.#curve.name,
13141356
this.#pubbuf,
1315-
format == "compressed",
1357+
format === "compressed",
13161358
));
1359+
if (format === "hybrid") {
1360+
const compressedBuf = Buffer.from(op_node_ecdh_encode_pubkey(
1361+
this.#curve.name,
1362+
this.#pubbuf,
1363+
true,
1364+
));
1365+
pubbuf[0] = compressedBuf[0] + 4;
1366+
}
13171367
if (encoding !== undefined) {
13181368
return pubbuf.toString(encoding);
13191369
}
@@ -1326,7 +1376,9 @@ export class ECDHImpl {
13261376
privateKey: ArrayBufferView | string,
13271377
encoding?: BinaryToTextEncoding,
13281378
): Buffer | string {
1329-
this.#privbuf = privateKey;
1379+
this.#privbuf = typeof privateKey === "string"
1380+
? Buffer.from(privateKey, encoding)
1381+
: Buffer.from(privateKey);
13301382
this.#pubbuf = Buffer.alloc(this.#curve.publicKeySize);
13311383

13321384
op_node_ecdh_compute_public_key(
@@ -1343,6 +1395,7 @@ export class ECDHImpl {
13431395
}
13441396

13451397
ECDH.prototype = ECDHImpl.prototype;
1398+
ECDH.convertKey = ECDHImpl.convertKey;
13461399

13471400
export function diffieHellman(options: {
13481401
privateKey: KeyObject;

ext/node_crypto/lib.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,35 +1118,35 @@ pub fn op_node_ecdh_compute_public_key(
11181118
elliptic_curve::SecretKey::<k256::Secp256k1>::from_slice(privkey)
11191119
.expect("bad private key");
11201120
let public_key = this_private_key.public_key();
1121-
pubkey.copy_from_slice(public_key.to_sec1_bytes().as_ref());
1121+
pubkey.copy_from_slice(public_key.to_encoded_point(false).as_ref());
11221122
}
11231123
"prime256v1" | "secp256r1" => {
11241124
let this_private_key =
11251125
elliptic_curve::SecretKey::<NistP256>::from_slice(privkey)
11261126
.expect("bad private key");
11271127
let public_key = this_private_key.public_key();
1128-
pubkey.copy_from_slice(public_key.to_sec1_bytes().as_ref());
1128+
pubkey.copy_from_slice(public_key.to_encoded_point(false).as_ref());
11291129
}
11301130
"secp384r1" => {
11311131
let this_private_key =
11321132
elliptic_curve::SecretKey::<NistP384>::from_slice(privkey)
11331133
.expect("bad private key");
11341134
let public_key = this_private_key.public_key();
1135-
pubkey.copy_from_slice(public_key.to_sec1_bytes().as_ref());
1135+
pubkey.copy_from_slice(public_key.to_encoded_point(false).as_ref());
11361136
}
11371137
"secp521r1" => {
11381138
let this_private_key =
11391139
elliptic_curve::SecretKey::<NistP521>::from_slice(privkey)
11401140
.expect("bad private key");
11411141
let public_key = this_private_key.public_key();
1142-
pubkey.copy_from_slice(public_key.to_sec1_bytes().as_ref());
1142+
pubkey.copy_from_slice(public_key.to_encoded_point(false).as_ref());
11431143
}
11441144
"secp224r1" => {
11451145
let this_private_key =
11461146
elliptic_curve::SecretKey::<NistP224>::from_slice(privkey)
11471147
.expect("bad private key");
11481148
let public_key = this_private_key.public_key();
1149-
pubkey.copy_from_slice(public_key.to_sec1_bytes().as_ref());
1149+
pubkey.copy_from_slice(public_key.to_encoded_point(false).as_ref());
11501150
}
11511151
&_ => todo!(),
11521152
}

tests/node_compat/config.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@
262262
"parallel/test-crypto-dh-shared.js": {},
263263
"parallel/test-crypto-domain.js": {},
264264
"parallel/test-crypto-domains.js": {},
265+
"parallel/test-crypto-ecdh-convert-key.js": {},
265266
"parallel/test-crypto-ecb.js": {},
266267
"parallel/test-crypto-from-binary.js": {},
267268
"parallel/test-crypto-keygen-async-elliptic-curve-jwk-rsa.js": {},

0 commit comments

Comments
 (0)