diff --git a/src/bun.js/bindings/KeyObject.cpp b/src/bun.js/bindings/KeyObject.cpp index 8d8715f8be..c11a7d74c7 100644 --- a/src/bun.js/bindings/KeyObject.cpp +++ b/src/bun.js/bindings/KeyObject.cpp @@ -2722,7 +2722,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__generateKeyPairSync, (JSC::JSGlobalObject * obj->putDirect(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "privateKey"_s)), JSCryptoKey::create(structure, zigGlobalObject, pair.privateKey.releaseNonNull()), 0); return JSValue::encode(obj); } else if (type_str == "x25519"_s) { - auto result = CryptoKeyOKP::generatePair(CryptoAlgorithmIdentifier::Ed25519, CryptoKeyOKP::NamedCurve::X25519, true, CryptoKeyUsageSign | CryptoKeyUsageVerify); + auto result = CryptoKeyOKP::generatePair(CryptoAlgorithmIdentifier::X25519, CryptoKeyOKP::NamedCurve::X25519, true, CryptoKeyUsageDeriveKey | CryptoKeyUsageDeriveBits); if (result.hasException()) { WebCore::propagateException(*lexicalGlobalObject, scope, result.releaseException()); return JSC::JSValue::encode(JSC::JSValue {}); @@ -2881,6 +2881,7 @@ AsymmetricKeyValue::AsymmetricKeyValue(WebCore::CryptoKey& cryptoKey) case CryptoAlgorithmIdentifier::ECDH: key = downcast(cryptoKey).platformKey(); break; + case CryptoAlgorithmIdentifier::X25519: case CryptoAlgorithmIdentifier::Ed25519: { const auto& okpKey = downcast(cryptoKey); auto keyData = okpKey.exportKey(); diff --git a/src/bun.js/bindings/NodeCrypto.cpp b/src/bun.js/bindings/NodeCrypto.cpp index 2d68321d37..739ff1ff86 100644 --- a/src/bun.js/bindings/NodeCrypto.cpp +++ b/src/bun.js/bindings/NodeCrypto.cpp @@ -84,11 +84,16 @@ JSC_DEFINE_HOST_FUNCTION(jsStatelessDH, (JSC::JSGlobalObject * lexicalGlobalObje // Use DHPointer::stateless to compute the shared secret auto secret = ncrypto::DHPointer::stateless(ourKeyPtr, theirKeyPtr).release(); + // These are owned by AsymmetricKeyValue, not by EVPKeyPointer. + ourKeyPtr.release(); + theirKeyPtr.release(); + auto buffer = ArrayBuffer::createFromBytes({ reinterpret_cast(secret.data), secret.len }, createSharedTask([](void* p) { OPENSSL_free(p); })); Zig::GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); auto* result = JSC::JSUint8Array::create(lexicalGlobalObject, globalObject->JSBufferSubclassStructure(), WTFMove(buffer), 0, secret.len); + RETURN_IF_EXCEPTION(scope, {}); if (!result) { return Bun::ERR::INVALID_ARG_VALUE(scope, lexicalGlobalObject, "diffieHellman"_s, jsUndefined(), "failed to allocate result buffer"_s); } diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp index 7bb59bdfb1..76cbd58ae9 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp @@ -504,6 +504,7 @@ enum class CryptoAlgorithmIdentifierTag { HKDF = 20, PBKDF2 = 21, ED25519 = 22, + X25519 = 23, }; const uint8_t cryptoAlgorithmIdentifierTagMaximumValue = 22; @@ -2293,6 +2294,9 @@ private: case CryptoAlgorithmIdentifier::Ed25519: write(CryptoAlgorithmIdentifierTag::ED25519); break; + case CryptoAlgorithmIdentifier::X25519: + write(CryptoAlgorithmIdentifierTag::X25519); + break; case CryptoAlgorithmIdentifier::None: { RELEASE_ASSERT_NOT_REACHED(); break; @@ -3777,6 +3781,9 @@ private: case CryptoAlgorithmIdentifierTag::ED25519: result = CryptoAlgorithmIdentifier::Ed25519; break; + case CryptoAlgorithmIdentifierTag::X25519: + result = CryptoAlgorithmIdentifier::X25519; + break; } return true; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h index d6a112ecb8..8f0ef468e8 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h @@ -50,7 +50,8 @@ enum class CryptoAlgorithmIdentifier : uint8_t { SHA_512, HKDF, PBKDF2, - Ed25519 + Ed25519, + X25519 }; } // namespace WebCore diff --git a/src/bun.js/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp b/src/bun.js/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp index 5d5e3c2ff3..09f9a5e726 100644 --- a/src/bun.js/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp @@ -38,12 +38,12 @@ namespace WebCore { bool CryptoKeyOKP::isPlatformSupportedCurve(NamedCurve namedCurve) { - return namedCurve == NamedCurve::Ed25519; + return namedCurve == NamedCurve::Ed25519 || namedCurve == NamedCurve::X25519; } std::optional CryptoKeyOKP::platformGeneratePair(CryptoAlgorithmIdentifier identifier, NamedCurve namedCurve, bool extractable, CryptoKeyUsageBitmap usages) { - if (namedCurve != NamedCurve::Ed25519) + if (namedCurve != NamedCurve::Ed25519 && namedCurve != NamedCurve::X25519) return {}; Vector public_key(ED25519_PUBLIC_KEY_LEN), private_key(ED25519_PRIVATE_KEY_LEN); diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp index cf0c2ae915..eb6cc11ee6 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp @@ -260,6 +260,9 @@ static ExceptionOr> normalizeCryptoAl case CryptoAlgorithmIdentifier::Ed25519: result = makeUnique(params); break; + case CryptoAlgorithmIdentifier::X25519: + result = makeUnique(params); + break; default: return Exception { NotSupportedError }; } @@ -328,6 +331,7 @@ static ExceptionOr> normalizeCryptoAl case CryptoAlgorithmIdentifier::AES_CFB: case CryptoAlgorithmIdentifier::AES_KW: case CryptoAlgorithmIdentifier::Ed25519: + case CryptoAlgorithmIdentifier::X25519: result = makeUnique(params); break; case CryptoAlgorithmIdentifier::HMAC: { @@ -540,6 +544,7 @@ static bool isSupportedExportKey(JSGlobalObject& state, CryptoAlgorithmIdentifie case CryptoAlgorithmIdentifier::ECDSA: case CryptoAlgorithmIdentifier::ECDH: case CryptoAlgorithmIdentifier::Ed25519: + case CryptoAlgorithmIdentifier::X25519: return true; default: return false; diff --git a/test/js/node/crypto/crypto.key-objects.test.ts b/test/js/node/crypto/crypto.key-objects.test.ts index 3f9dac8ed4..51e5794b49 100644 --- a/test/js/node/crypto/crypto.key-objects.test.ts +++ b/test/js/node/crypto/crypto.key-objects.test.ts @@ -390,8 +390,8 @@ describe("crypto.KeyObjects", () => { }, ].forEach(info => { const keyType = info.keyType; - // X25519 implementation is incomplete, Ed448 and X448 are not supported yet - const test = keyType === "ed25519" ? it : it.skip; + // Ed448 and X448 are not supported yet + const test = keyType === "x448" || keyType === "ed448" ? it.skip : it; let privateKey: KeyObject; test(`${keyType} from Buffer should work`, async () => { const key = createPrivateKey(info.private); @@ -677,8 +677,7 @@ describe("crypto.KeyObjects", () => { }); ["ed25519", "x25519"].forEach(keyType => { - const test = keyType === "ed25519" ? it : it.skip; - test(`${keyType} equals should work`, async () => { + it(`${keyType} equals should work`, async () => { const first = generateKeyPairSync(keyType); const second = generateKeyPairSync(keyType); @@ -824,7 +823,7 @@ describe("crypto.KeyObjects", () => { describe("Test async elliptic curve key generation with 'jwk' encoding", () => { ["ed25519", "ed448", "x25519", "x448"].forEach(type => { - const test = type === "ed25519" ? it : it.skip; + const test = type === "x448" || type === "ed448" ? it.skip : it; test(`should work with ${type}`, async () => { const { promise, resolve, reject } = Promise.withResolvers(); generateKeyPair( @@ -1171,7 +1170,7 @@ describe("crypto.KeyObjects", () => { describe("Test sync elliptic curve key generation with 'jwk' encoding", () => { ["ed25519", "ed448", "x25519", "x448"].forEach(type => { - const test = type === "ed25519" ? it : it.skip; + const test = type === "x448" || type === "ed448" ? it.skip : it; test(`should work with ${type}`, async () => { const { publicKey, privateKey } = generateKeyPairSync(type, { publicKeyEncoding: { diff --git a/test/js/node/crypto/node-crypto.test.js b/test/js/node/crypto/node-crypto.test.js index af41d61407..681ef22910 100644 --- a/test/js/node/crypto/node-crypto.test.js +++ b/test/js/node/crypto/node-crypto.test.js @@ -536,3 +536,106 @@ it("createDecipheriv should validate iv and password", () => { expect(() => crypto.createDecipheriv("aes-128-cbc", key).setAutoPadding(false)).toThrow(); expect(() => crypto.createDecipheriv("aes-128-cbc", key, Buffer.alloc(16)).setAutoPadding(false)).not.toThrow(); }); + +it("x25519", () => { + // Generate Alice's keys + const alice = crypto.generateKeyPairSync("x25519", { + publicKeyEncoding: { + type: "spki", + format: "der", + }, + privateKeyEncoding: { + type: "pkcs8", + format: "der", + }, + }); + + // Generate Bob's keys + const bob = crypto.generateKeyPairSync("x25519", { + publicKeyEncoding: { + type: "spki", + format: "der", + }, + privateKeyEncoding: { + type: "pkcs8", + format: "der", + }, + }); + + // Convert keys to KeyObjects before DH computation + const alicePrivateKey = crypto.createPrivateKey({ + key: alice.privateKey, + format: "der", + type: "pkcs8", + }); + + const bobPublicKey = crypto.createPublicKey({ + key: bob.publicKey, + format: "der", + type: "spki", + }); + + const bobPrivateKey = crypto.createPrivateKey({ + key: bob.privateKey, + format: "der", + type: "pkcs8", + }); + + const alicePublicKey = crypto.createPublicKey({ + key: alice.publicKey, + format: "der", + type: "spki", + }); + + // Compute shared secrets using KeyObjects + const aliceSecret = crypto.diffieHellman({ + privateKey: alicePrivateKey, + publicKey: bobPublicKey, + }); + + const bobSecret = crypto.diffieHellman({ + privateKey: bobPrivateKey, + publicKey: alicePublicKey, + }); + + // Verify both parties computed the same secret + expect(aliceSecret).toEqual(bobSecret); + expect(aliceSecret.length).toBe(32); + + // Verify valid key generation + expect(() => { + crypto.generateKeyPairSync("x25519", { + publicKeyEncoding: { + type: "spki", + format: "der", + }, + privateKeyEncoding: { + type: "pkcs8", + format: "der", + }, + }); + }).not.toThrow(); + + // Test invalid keys - need to create proper KeyObjects even for invalid cases + expect(() => { + crypto.diffieHellman({ + privateKey: crypto.createPrivateKey({ + key: Buffer.from("invalid"), + format: "der", + type: "pkcs8", + }), + publicKey: bobPublicKey, + }); + }).toThrow(); + + expect(() => { + crypto.diffieHellman({ + privateKey: bobPrivateKey, + publicKey: crypto.createPublicKey({ + key: Buffer.from("invalid"), + format: "der", + type: "spki", + }), + }); + }).toThrow(); +});