Repository: denoland/deno
Pull request: fix(ext/crypto): export private x25519 JWK key

Ref https://github.com/denoland/deno/issues/26431

Changed files:
filename: ext/crypto/00_crypto.js
patch: @@ -48,6 +48,7 @@ import {
   op_crypto_verify_ed25519,
   op_crypto_verify_key,
   op_crypto_wrap_key,
+  op_crypto_x25519_public_key,
 } from "ext:core/ops";
 const {
   ArrayBufferIsView,
@@ -4532,17 +4533,18 @@ function exportKeyX25519(format, key, innerKey) {
       return TypedArrayPrototypeGetBuffer(pkcs8Der);
     }
     case "jwk": {
-      if (key[_type] === "private") {
-        throw new DOMException("Not implemented", "NotSupportedError");
-      }
-      const x = op_crypto_base64url_encode(innerKey);
       const jwk = {
         kty: "OKP",
         crv: "X25519",
-        x,
         "key_ops": key.usages,
         ext: key[_extractable],
       };
+      if (key[_type] === "private") {
+        jwk.x = op_crypto_x25519_public_key(innerKey);
+        jwk.d = op_crypto_base64url_encode(innerKey);
+      } else {
+        jwk.x = op_crypto_base64url_encode(innerKey);
+      }
       return jwk;
     }
     default:

filename: ext/crypto/lib.rs
patch: @@ -98,6 +98,7 @@ deno_core::extension!(deno_crypto,
     op_crypto_base64url_decode,
     op_crypto_base64url_encode,
     x25519::op_crypto_generate_x25519_keypair,
+    x25519::op_crypto_x25519_public_key,
     x25519::op_crypto_derive_bits_x25519,
     x25519::op_crypto_import_spki_x25519,
     x25519::op_crypto_import_pkcs8_x25519,

filename: ext/crypto/x25519.rs
patch: @@ -1,5 +1,6 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 
+use base64::prelude::BASE64_URL_SAFE_NO_PAD;
 use curve25519_dalek::montgomery::MontgomeryPoint;
 use deno_core::op2;
 use deno_core::ToJsBuffer;
@@ -20,17 +21,16 @@ pub enum X25519Error {
   #[error(transparent)]
   Der(#[from] spki::der::Error),
 }
-
+// u-coordinate of the base point.
+const X25519_BASEPOINT_BYTES: [u8; 32] = [
+  9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0,
+];
 #[op2(fast)]
 pub fn op_crypto_generate_x25519_keypair(
   #[buffer] pkey: &mut [u8],
   #[buffer] pubkey: &mut [u8],
 ) {
-  // u-coordinate of the base point.
-  const X25519_BASEPOINT_BYTES: [u8; 32] = [
-    9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-    0, 0, 0, 0, 0, 0, 0,
-  ];
   let mut rng = OsRng;
   rng.fill_bytes(pkey);
   // https://www.rfc-editor.org/rfc/rfc7748#section-6.1
@@ -42,6 +42,17 @@ pub fn op_crypto_generate_x25519_keypair(
   pubkey.copy_from_slice(&x25519_dalek::x25519(pkey, X25519_BASEPOINT_BYTES));
 }
 
+#[op2]
+#[string]
+pub fn op_crypto_x25519_public_key(#[buffer] private_key: &[u8]) -> String {
+  use base64::Engine;
+
+  let private_key: [u8; 32] =
+    private_key.try_into().expect("Expected byteLength 32");
+  BASE64_URL_SAFE_NO_PAD
+    .encode(x25519_dalek::x25519(private_key, X25519_BASEPOINT_BYTES))
+}
+
 const MONTGOMERY_IDENTITY: MontgomeryPoint = MontgomeryPoint([0; 32]);
 
 #[op2(fast)]

filename: tests/unit/webcrypto_test.ts
patch: @@ -2085,3 +2085,20 @@ Deno.test(async function x25519SharedSecret() {
   assertEquals(sharedSecret1.byteLength, 16);
   assertEquals(new Uint8Array(sharedSecret1), new Uint8Array(sharedSecret2));
 });
+
+Deno.test(async function x25519ExportJwk() {
+  const keyPair = await crypto.subtle.generateKey(
+    {
+      name: "X25519",
+    },
+    true,
+    ["deriveBits"],
+  ) as CryptoKeyPair;
+
+  const jwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
+
+  assertEquals(jwk.kty, "OKP");
+  assertEquals(jwk.crv, "X25519");
+  assert(jwk.d);
+  assert(jwk.x);
+});

filename: tests/wpt/runner/expectation.json
patch: @@ -845,14 +845,6 @@
         "Good parameters: Ed448 bits (jwk, object(crv, d, x, kty), {name: Ed448}, false, [sign])",
         "Good parameters: Ed448 bits (pkcs8, buffer(73), {name: Ed448}, false, [sign, sign])",
         "Good parameters: Ed448 bits (jwk, object(crv, d, x, kty), {name: Ed448}, false, [sign, sign])",
-        "Good parameters: X25519 bits (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveKey])",
-        "Good parameters with ignored JWK alg: X25519 (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveKey])",
-        "Good parameters: X25519 bits (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveBits, deriveKey])",
-        "Good parameters with ignored JWK alg: X25519 (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveBits, deriveKey])",
-        "Good parameters: X25519 bits (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveBits])",
-        "Good parameters with ignored JWK alg: X25519 (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveBits])",
-        "Good parameters: X25519 bits (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveKey, deriveBits, deriveKey, deriveBits])",
-        "Good parameters with ignored JWK alg: X25519 (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveKey, deriveBits, deriveKey, deriveBits])",
         "Good parameters: X448 bits (pkcs8, buffer(72), {name: X448}, true, [deriveKey])",
         "Good parameters: X448 bits (jwk, object(crv, d, x, kty), {name: X448}, true, [deriveKey])",
         "Good parameters with ignored JWK alg: X448 (jwk, object(crv, d, x, kty), {name: X448}, true, [deriveKey])",
@@ -902,14 +894,6 @@
         "Good parameters: Ed448 bits (jwk, object(crv, d, x, kty), {name: Ed448}, false, [sign])",
         "Good parameters: Ed448 bits (pkcs8, buffer(73), {name: Ed448}, false, [sign, sign])",
         "Good parameters: Ed448 bits (jwk, object(crv, d, x, kty), {name: Ed448}, false, [sign, sign])",
-        "Good parameters: X25519 bits (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveKey])",
-        "Good parameters with ignored JWK alg: X25519 (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveKey])",
-        "Good parameters: X25519 bits (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveBits, deriveKey])",
-        "Good parameters with ignored JWK alg: X25519 (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveBits, deriveKey])",
-        "Good parameters: X25519 bits (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveBits])",
-        "Good parameters with ignored JWK alg: X25519 (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveBits])",
-        "Good parameters: X25519 bits (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveKey, deriveBits, deriveKey, deriveBits])",
-        "Good parameters with ignored JWK alg: X25519 (jwk, object(crv, d, x, kty), {name: X25519}, true, [deriveKey, deriveBits, deriveKey, deriveBits])",
         "Good parameters: X448 bits (pkcs8, buffer(72), {name: X448}, true, [deriveKey])",
         "Good parameters: X448 bits (jwk, object(crv, d, x, kty), {name: X448}, true, [deriveKey])",
         "Good parameters with ignored JWK alg: X448 (jwk, object(crv, d, x, kty), {name: X448}, true, [deriveKey])",