diff --git a/src/bun.js/bindings/AsymmetricKeyValue.cpp b/src/bun.js/bindings/AsymmetricKeyValue.cpp index 0e7cb6744c41..d72f9abce0b6 100644 --- a/src/bun.js/bindings/AsymmetricKeyValue.cpp +++ b/src/bun.js/bindings/AsymmetricKeyValue.cpp @@ -127,6 +127,9 @@ AsymmetricKeyValue::AsymmetricKeyValue(WebCore::CryptoKey& cryptoKey) case CryptoAlgorithmIdentifier::SHA_256: case CryptoAlgorithmIdentifier::SHA_384: case CryptoAlgorithmIdentifier::SHA_512: + case CryptoAlgorithmIdentifier::SHA3_256: + case CryptoAlgorithmIdentifier::SHA3_384: + case CryptoAlgorithmIdentifier::SHA3_512: case CryptoAlgorithmIdentifier::HKDF: case CryptoAlgorithmIdentifier::PBKDF2: case CryptoAlgorithmIdentifier::None: diff --git a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp index 673ac6503231..2d41a482d08f 100644 --- a/src/bun.js/bindings/webcore/SerializedScriptValue.cpp +++ b/src/bun.js/bindings/webcore/SerializedScriptValue.cpp @@ -2406,6 +2406,14 @@ class CloneSerializer : public CloneBase { case CryptoAlgorithmIdentifier::X25519: write(CryptoAlgorithmIdentifierTag::X25519); break; + case CryptoAlgorithmIdentifier::SHA3_256: + case CryptoAlgorithmIdentifier::SHA3_384: + case CryptoAlgorithmIdentifier::SHA3_512: + // SHA-3 algorithms are currently only used as digest functions and + // are not associated with serializable CryptoKey instances. If this + // changes (e.g. when HMAC/SHA-3 or KEM keys land), add new tags. + RELEASE_ASSERT_NOT_REACHED(); + break; case CryptoAlgorithmIdentifier::None: { RELEASE_ASSERT_NOT_REACHED(); break; diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h index 8f0ef468e8ed..a59c6c185f22 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmIdentifier.h @@ -51,7 +51,10 @@ enum class CryptoAlgorithmIdentifier : uint8_t { HKDF, PBKDF2, Ed25519, - X25519 + X25519, + SHA3_256, + SHA3_384, + SHA3_512 }; } // namespace WebCore diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRegistryOpenSSL.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRegistryOpenSSL.cpp index b0d12c4970b9..4f0bfe859dc2 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRegistryOpenSSL.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRegistryOpenSSL.cpp @@ -48,6 +48,9 @@ #include "CryptoAlgorithmSHA256.h" #include "CryptoAlgorithmSHA384.h" #include "CryptoAlgorithmSHA512.h" +#include "CryptoAlgorithmSHA3_256.h" +#include "CryptoAlgorithmSHA3_384.h" +#include "CryptoAlgorithmSHA3_512.h" #include "CryptoAlgorithmX25519.h" namespace WebCore { @@ -73,6 +76,9 @@ void CryptoAlgorithmRegistry::platformRegisterAlgorithms() registerAlgorithmWithAlternativeName(); registerAlgorithmWithAlternativeName(); registerAlgorithmWithAlternativeName(); + registerAlgorithm(); + registerAlgorithm(); + registerAlgorithm(); registerAlgorithm(); registerAlgorithm(); } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.cpp new file mode 100644 index 000000000000..c14a694076ef --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "CryptoAlgorithmSHA3_256.h" + +#if ENABLE(WEB_CRYPTO) + +#include "ScriptExecutionContext.h" +#include "CryptoDigest.h" + +namespace WebCore { + +Ref CryptoAlgorithmSHA3_256::create() +{ + return adoptRef(*new CryptoAlgorithmSHA3_256); +} + +CryptoAlgorithmIdentifier CryptoAlgorithmSHA3_256::identifier() const +{ + return s_identifier; +} + +void CryptoAlgorithmSHA3_256::digest(Vector&& message, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) +{ + auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA3_256); + if (!digest) { + exceptionCallback(OperationError, ""_s); + return; + } + + if (message.size() < 64) { + auto moved = WTF::move(message); + digest->addBytes(moved.begin(), moved.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(context.identifier(), [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + return; + } + + workQueue.dispatch(context.globalObject(), [digest = WTF::move(digest), message = WTF::move(message), callback = WTF::move(callback), contextIdentifier = context.identifier()]() mutable { + digest->addBytes(message.begin(), message.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + }); +} + +} + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.h new file mode 100644 index 000000000000..63a8f4e6d981 --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_256.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "CryptoAlgorithm.h" + +#if ENABLE(WEB_CRYPTO) + +namespace WebCore { + +class CryptoAlgorithmSHA3_256 final : public CryptoAlgorithm { +public: + static constexpr ASCIILiteral s_name = "SHA3-256"_s; + + static const CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::SHA3_256; + static Ref create(); + +private: + CryptoAlgorithmSHA3_256() = default; + CryptoAlgorithmIdentifier identifier() const final; + void digest(Vector&&, VectorCallback&&, ExceptionCallback&&, ScriptExecutionContext&, WorkQueue&) final; +}; + +} // namespace WebCore + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.cpp new file mode 100644 index 000000000000..925f52bb87c5 --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "CryptoAlgorithmSHA3_384.h" + +#if ENABLE(WEB_CRYPTO) + +#include "ScriptExecutionContext.h" +#include "CryptoDigest.h" + +namespace WebCore { + +Ref CryptoAlgorithmSHA3_384::create() +{ + return adoptRef(*new CryptoAlgorithmSHA3_384); +} + +CryptoAlgorithmIdentifier CryptoAlgorithmSHA3_384::identifier() const +{ + return s_identifier; +} + +void CryptoAlgorithmSHA3_384::digest(Vector&& message, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) +{ + auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA3_384); + if (!digest) { + exceptionCallback(OperationError, ""_s); + return; + } + + if (message.size() < 64) { + auto moved = WTF::move(message); + digest->addBytes(moved.begin(), moved.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(context.identifier(), [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + return; + } + + workQueue.dispatch(context.globalObject(), [digest = WTF::move(digest), message = WTF::move(message), callback = WTF::move(callback), contextIdentifier = context.identifier()]() mutable { + digest->addBytes(message.begin(), message.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + }); +} + +} + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.h new file mode 100644 index 000000000000..cd5551e2a33f --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_384.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "CryptoAlgorithm.h" + +#if ENABLE(WEB_CRYPTO) + +namespace WebCore { + +class CryptoAlgorithmSHA3_384 final : public CryptoAlgorithm { +public: + static constexpr ASCIILiteral s_name = "SHA3-384"_s; + + static const CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::SHA3_384; + static Ref create(); + +private: + CryptoAlgorithmSHA3_384() = default; + CryptoAlgorithmIdentifier identifier() const final; + void digest(Vector&&, VectorCallback&&, ExceptionCallback&&, ScriptExecutionContext&, WorkQueue&) final; +}; + +} // namespace WebCore + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.cpp new file mode 100644 index 000000000000..f964e8b8dd03 --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "CryptoAlgorithmSHA3_512.h" + +#if ENABLE(WEB_CRYPTO) + +#include "ScriptExecutionContext.h" +#include "CryptoDigest.h" + +namespace WebCore { + +Ref CryptoAlgorithmSHA3_512::create() +{ + return adoptRef(*new CryptoAlgorithmSHA3_512); +} + +CryptoAlgorithmIdentifier CryptoAlgorithmSHA3_512::identifier() const +{ + return s_identifier; +} + +void CryptoAlgorithmSHA3_512::digest(Vector&& message, VectorCallback&& callback, ExceptionCallback&& exceptionCallback, ScriptExecutionContext& context, WorkQueue& workQueue) +{ + auto digest = PAL::CryptoDigest::create(PAL::CryptoDigest::Algorithm::SHA3_512); + if (!digest) { + exceptionCallback(OperationError, ""_s); + return; + } + + if (message.size() < 64) { + auto moved = WTF::move(message); + digest->addBytes(moved.begin(), moved.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(context.identifier(), [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + return; + } + + workQueue.dispatch(context.globalObject(), [digest = WTF::move(digest), message = WTF::move(message), callback = WTF::move(callback), contextIdentifier = context.identifier()]() mutable { + digest->addBytes(message.begin(), message.size()); + auto result = digest->computeHash(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTF::move(callback), result = WTF::move(result)](auto&) { + callback(result); + }); + }); +} + +} + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.h new file mode 100644 index 000000000000..51bf0cb956f3 --- /dev/null +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA3_512.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "CryptoAlgorithm.h" + +#if ENABLE(WEB_CRYPTO) + +namespace WebCore { + +class CryptoAlgorithmSHA3_512 final : public CryptoAlgorithm { +public: + static constexpr ASCIILiteral s_name = "SHA3-512"_s; + + static const CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::SHA3_512; + static Ref create(); + +private: + CryptoAlgorithmSHA3_512() = default; + CryptoAlgorithmIdentifier identifier() const final; + void digest(Vector&&, VectorCallback&&, ExceptionCallback&&, ScriptExecutionContext&, WorkQueue&) final; +}; + +} // namespace WebCore + +#endif // ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp index 0c8f3aa5e8fe..345121120250 100644 --- a/src/bun.js/bindings/webcrypto/CryptoDigest.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoDigest.cpp @@ -26,9 +26,253 @@ #include "config.h" #include "CryptoDigest.h" +#include +#include #include namespace { + +// Keccak-f[1600] permutation and SHA-3 sponge, used to implement the +// fixed-output SHA3-256/384/512 hashes from FIPS 202. +// +// BoringSSL's internal keccak_* helpers only expose SHA3-256 and SHA3-512, +// so we provide a small self-contained implementation here rather than +// depending on internal BoringSSL headers. The same state machine can be +// extended with SHAKE/cSHAKE/TurboSHAKE domain separators when those +// XOFs land alongside the rest of the WICG "Modern Algorithms" spec. + +struct KeccakState { + uint64_t lanes[25]; +}; + +static constexpr uint64_t kKeccakRoundConstants[24] = { + 0x0000000000000001ULL, + 0x0000000000008082ULL, + 0x800000000000808aULL, + 0x8000000080008000ULL, + 0x000000000000808bULL, + 0x0000000080000001ULL, + 0x8000000080008081ULL, + 0x8000000000008009ULL, + 0x000000000000008aULL, + 0x0000000000000088ULL, + 0x0000000080008009ULL, + 0x000000008000000aULL, + 0x000000008000808bULL, + 0x800000000000008bULL, + 0x8000000000008089ULL, + 0x8000000000008003ULL, + 0x8000000000008002ULL, + 0x8000000000000080ULL, + 0x000000000000800aULL, + 0x800000008000000aULL, + 0x8000000080008081ULL, + 0x8000000000008080ULL, + 0x0000000080000001ULL, + 0x8000000080008008ULL, +}; + +static inline uint64_t rotl64(uint64_t x, unsigned n) +{ + return (x << n) | (x >> (64 - n)); +} + +static void keccakF1600(KeccakState& state) +{ + for (unsigned round = 0; round < 24; round++) { + // θ step + uint64_t c[5]; + for (unsigned x = 0; x < 5; x++) + c[x] = state.lanes[x] ^ state.lanes[x + 5] ^ state.lanes[x + 10] ^ state.lanes[x + 15] ^ state.lanes[x + 20]; + uint64_t d[5]; + for (unsigned x = 0; x < 5; x++) + d[x] = c[(x + 4) % 5] ^ rotl64(c[(x + 1) % 5], 1); + for (unsigned x = 0; x < 5; x++) { + for (unsigned y = 0; y < 5; y++) + state.lanes[y * 5 + x] ^= d[x]; + } + + // ρ and π steps (in-place along the 24-step trail) + uint64_t prev = state.lanes[1]; + static constexpr std::pair piRho[24] = { + { 10, 1 }, + { 7, 3 }, + { 11, 6 }, + { 17, 10 }, + { 18, 15 }, + { 3, 21 }, + { 5, 28 }, + { 16, 36 }, + { 8, 45 }, + { 21, 55 }, + { 24, 2 }, + { 4, 14 }, + { 15, 27 }, + { 23, 41 }, + { 19, 56 }, + { 13, 8 }, + { 12, 25 }, + { 2, 43 }, + { 20, 62 }, + { 14, 18 }, + { 22, 39 }, + { 9, 61 }, + { 6, 20 }, + { 1, 44 }, + }; + for (const auto& step : piRho) { + uint64_t rotated = rotl64(prev, static_cast(step.second)); + prev = state.lanes[step.first]; + state.lanes[step.first] = rotated; + } + + // χ step + for (unsigned y = 0; y < 5; y++) { + const unsigned row = 5 * y; + const uint64_t a0 = state.lanes[row]; + const uint64_t a1 = state.lanes[row + 1]; + state.lanes[row] ^= ~a1 & state.lanes[row + 2]; + state.lanes[row + 1] ^= ~state.lanes[row + 2] & state.lanes[row + 3]; + state.lanes[row + 2] ^= ~state.lanes[row + 3] & state.lanes[row + 4]; + state.lanes[row + 3] ^= ~state.lanes[row + 4] & a0; + state.lanes[row + 4] ^= ~a0 & a1; + } + + // ι step + state.lanes[0] ^= kKeccakRoundConstants[round]; + } +} + +static inline uint64_t loadLE64(const uint8_t* bytes) +{ + uint64_t v; + std::memcpy(&v, bytes, sizeof(v)); +#if defined(__BIG_ENDIAN__) || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + v = __builtin_bswap64(v); +#endif + return v; +} + +static inline void storeLE64(uint8_t* bytes, uint64_t v) +{ +#if defined(__BIG_ENDIAN__) || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + v = __builtin_bswap64(v); +#endif + std::memcpy(bytes, &v, sizeof(v)); +} + +// Streaming SHA-3 sponge for one of the fixed output sizes. +struct Sha3Context { + KeccakState state {}; + // Input bytes buffered before the next permutation. Sized for the largest + // fixed-output FIPS 202 rate, which is SHA3-224's 144 bytes (not yet + // registered); the three variants this file ships only need up to + // SHA3-256's 136-byte rate. XOF variants are NOT representable with this + // struct as-is: SHAKE128's rate is 168 bytes (would overflow this + // buffer), and both SHAKE variants also need a different digestLength + // model because their output is variable-length. Adding SHAKE support + // requires resizing this buffer and teaching sha3Final to squeeze across + // multiple rate-sized blocks. + uint8_t buffer[144] {}; + size_t bufferLength = 0; + size_t rateBytes = 0; + size_t digestLength = 0; +}; + +static void sha3Init(Sha3Context& ctx, size_t outputBytes) +{ + ctx = {}; + ctx.digestLength = outputBytes; + // rate = 1600 - 2 * outputBits + const size_t capacityBytes = 2 * outputBytes; + ctx.rateBytes = 200 - capacityBytes; + // Symmetric to the ASSERT in sha3Final: catch any mistaken wiring of a + // variant whose rate exceeds our fixed-output buffer. sha3Update memcpys + // up to rateBytes at a time, so if a caller ever wires SHAKE128 + // (rateBytes = 168) through this path the absorption phase would + // silently overflow buffer[144] before the sha3Final guard ever runs. + ASSERT(ctx.rateBytes <= sizeof(ctx.buffer)); +} + +static void sha3Update(Sha3Context& ctx, const uint8_t* input, size_t length) +{ + while (length > 0) { + const size_t space = ctx.rateBytes - ctx.bufferLength; + const size_t take = length < space ? length : space; + std::memcpy(ctx.buffer + ctx.bufferLength, input, take); + ctx.bufferLength += take; + input += take; + length -= take; + + if (ctx.bufferLength == ctx.rateBytes) { + // Absorb one full rate-sized block. + const size_t rateLanes = ctx.rateBytes / 8; + for (size_t i = 0; i < rateLanes; i++) + ctx.state.lanes[i] ^= loadLE64(ctx.buffer + 8 * i); + keccakF1600(ctx.state); + ctx.bufferLength = 0; + } + } +} + +static void sha3Final(Sha3Context& ctx, uint8_t* output) +{ + // This single-block squeeze is only valid when the entire digest fits + // inside one rate-sized squeeze block. All three currently registered + // variants satisfy that (SHA3-256: 32 < 136, SHA3-384: 48 < 104, + // SHA3-512: 64 < 72); see Sha3Context's comment above. When XOF + // variants (SHAKE / cSHAKE / TurboSHAKE) are added they will need a + // multi-block squeeze loop — this assert catches anyone accidentally + // taking this path with a variant that doesn't satisfy the invariant + // before it silently reads capacity bits out of the state. + ASSERT(ctx.digestLength <= ctx.rateBytes); + + // Pad10*1 with the SHA-3 domain separator 0x06. + std::memset(ctx.buffer + ctx.bufferLength, 0, ctx.rateBytes - ctx.bufferLength); + ctx.buffer[ctx.bufferLength] |= 0x06; + ctx.buffer[ctx.rateBytes - 1] |= 0x80; + + const size_t rateLanes = ctx.rateBytes / 8; + for (size_t i = 0; i < rateLanes; i++) + ctx.state.lanes[i] ^= loadLE64(ctx.buffer + 8 * i); + keccakF1600(ctx.state); + + // Squeeze. For the fixed-output SHA-3 variants the digest always fits + // inside a single rate-sized squeeze block: the smallest rate is + // SHA3-512's 72 bytes and its digest is 64 bytes, so no additional + // permutation is needed. + size_t produced = 0; + uint8_t lane[8]; + while (produced < ctx.digestLength) { + const size_t laneIndex = produced / 8; + storeLE64(lane, ctx.state.lanes[laneIndex]); + const size_t take = std::min(8, ctx.digestLength - produced); + std::memcpy(output + produced, lane, take); + produced += take; + } +} + +struct SHA3_256Functions { + static void init(Sha3Context* ctx) { sha3Init(*ctx, 32); } + static void update(Sha3Context* ctx, const void* data, size_t len) { sha3Update(*ctx, static_cast(data), len); } + static void final(uint8_t* out, Sha3Context* ctx) { sha3Final(*ctx, out); } + static constexpr size_t digestLength = 32; +}; + +struct SHA3_384Functions { + static void init(Sha3Context* ctx) { sha3Init(*ctx, 48); } + static void update(Sha3Context* ctx, const void* data, size_t len) { sha3Update(*ctx, static_cast(data), len); } + static void final(uint8_t* out, Sha3Context* ctx) { sha3Final(*ctx, out); } + static constexpr size_t digestLength = 48; +}; + +struct SHA3_512Functions { + static void init(Sha3Context* ctx) { sha3Init(*ctx, 64); } + static void update(Sha3Context* ctx, const void* data, size_t len) { sha3Update(*ctx, static_cast(data), len); } + static void final(uint8_t* out, Sha3Context* ctx) { sha3Final(*ctx, out); } + static constexpr size_t digestLength = 64; +}; + struct SHA1Functions { static constexpr auto init = SHA1_Init; static constexpr auto update = SHA1_Update; @@ -131,6 +375,15 @@ std::unique_ptr CryptoDigest::create(CryptoDigest::Algorithm algor case CryptoDigest::Algorithm::SHA_512: digest->m_context = CryptoDigestContextImpl::create(); return digest; + case CryptoDigest::Algorithm::SHA3_256: + digest->m_context = CryptoDigestContextImpl::create(); + return digest; + case CryptoDigest::Algorithm::SHA3_384: + digest->m_context = CryptoDigestContextImpl::create(); + return digest; + case CryptoDigest::Algorithm::SHA3_512: + digest->m_context = CryptoDigestContextImpl::create(); + return digest; } return nullptr; diff --git a/src/bun.js/bindings/webcrypto/CryptoDigest.h b/src/bun.js/bindings/webcrypto/CryptoDigest.h index 5da849a1d33c..12c3907e1a55 100644 --- a/src/bun.js/bindings/webcrypto/CryptoDigest.h +++ b/src/bun.js/bindings/webcrypto/CryptoDigest.h @@ -47,6 +47,9 @@ class CryptoDigest { SHA_256, SHA_384, SHA_512, + SHA3_256, + SHA3_384, + SHA3_512, }; PAL_EXPORT static std::unique_ptr create(Algorithm); PAL_EXPORT ~CryptoDigest(); diff --git a/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp b/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp index 84a506191bff..ec1564901f5d 100644 --- a/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp +++ b/src/bun.js/bindings/webcrypto/JSSubtleCrypto.cpp @@ -126,6 +126,7 @@ static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_importKey); static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_exportKey); static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_wrapKey); static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_unwrapKey); +static JSC_DECLARE_HOST_FUNCTION(jsSubtleCryptoConstructorFunction_supports); // Attributes @@ -180,6 +181,12 @@ template<> void JSSubtleCryptoDOMConstructor::initializeProperties(VM& vm, JSDOM m_originalName.set(vm, this, nameString); putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); putDirect(vm, vm.propertyNames->prototype, JSSubtleCrypto::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + + // WICG "Modern Algorithms in the Web Cryptography API" defines supports() + // as a static on the SubtleCrypto interface object, i.e. + // `SubtleCrypto.supports(...)` rather than `subtle.supports(...)`. + JSFunction* supportsFunction = JSFunction::create(vm, &globalObject, 2, "supports"_s, jsSubtleCryptoConstructorFunction_supports, ImplementationVisibility::Public); + putDirect(vm, Identifier::fromString(vm, "supports"_s), supportsFunction, 0); } /* Hash table for prototype */ @@ -598,6 +605,27 @@ JSC_DEFINE_HOST_FUNCTION(jsSubtleCryptoPrototypeFunction_unwrapKey, (JSGlobalObj return IDLOperationReturningPromise::call(*lexicalGlobalObject, *callFrame, "unwrapKey"); } +// WICG "Modern Algorithms in the Web Cryptography API" defines supports() as +// a static on the SubtleCrypto interface object. It is installed in +// JSSubtleCryptoDOMConstructor::initializeProperties() and does not live on +// the prototype, so we do not go through IDLOperation (which +// would require a SubtleCrypto `this`). The implementation does not touch +// any instance state — it is a pure feature-detect over (operation, algorithm). +JSC_DEFINE_HOST_FUNCTION(jsSubtleCryptoConstructorFunction_supports, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + if (callFrame->argumentCount() < 2) [[unlikely]] + return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto operation = convert(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, {}); + EnsureStillAliveScope argument1 = callFrame->uncheckedArgument(1); + auto algorithm = convert>(*lexicalGlobalObject, argument1.value()); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, JSValue::encode(jsBoolean(SubtleCrypto::supports(*jsCast(lexicalGlobalObject), operation, WTF::move(algorithm))))); +} + JSC::GCClient::IsoSubspace* JSSubtleCrypto::subspaceForImpl(JSC::VM& vm) { return WebCore::subspaceForImpl( diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp index 5f57e71a2d4f..7b2901974201 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.cpp @@ -87,7 +87,21 @@ static ExceptionOr toHashIdentifier(JSGlobalObject& s auto digestParams = normalizeCryptoAlgorithmParameters(state, algorithmIdentifier, Operations::Digest); if (digestParams.hasException()) return digestParams.releaseException(); - return digestParams.returnValue()->identifier; + auto identifier = digestParams.returnValue()->identifier; + // SHA-3 is only implemented as a top-level digest operation in this + // slice of the WICG "Modern Algorithms" spec. It is NOT yet wired into + // OpenSSLUtilities::digestAlgorithm(), CryptoKeyHMAC::getKeyLengthFromHash(), + // or SerializedScriptValue, so accepting SHA-3 as a hash sub-algorithm for + // HMAC / RSA / ECDSA would create broken CryptoKey instances that crash + // at sign() or postMessage() time. Reject the name up front so callers + // get a clean NotSupportedError at importKey()/generateKey() time and can + // use supports() to progressively adopt it when the rest of the plumbing + // lands. + if (identifier == CryptoAlgorithmIdentifier::SHA3_256 + || identifier == CryptoAlgorithmIdentifier::SHA3_384 + || identifier == CryptoAlgorithmIdentifier::SHA3_512) + return Exception { NotSupportedError }; + return identifier; } static bool isRSAESPKCSWebCryptoDeprecated(JSGlobalObject& state) @@ -204,6 +218,9 @@ static ExceptionOr> normalizeCryptoAl case CryptoAlgorithmIdentifier::SHA_256: case CryptoAlgorithmIdentifier::SHA_384: case CryptoAlgorithmIdentifier::SHA_512: + case CryptoAlgorithmIdentifier::SHA3_256: + case CryptoAlgorithmIdentifier::SHA3_384: + case CryptoAlgorithmIdentifier::SHA3_512: result = makeUnique(params); break; default: @@ -381,6 +398,9 @@ static ExceptionOr> normalizeCryptoAl case CryptoAlgorithmIdentifier::SHA_256: case CryptoAlgorithmIdentifier::SHA_384: case CryptoAlgorithmIdentifier::SHA_512: + case CryptoAlgorithmIdentifier::SHA3_256: + case CryptoAlgorithmIdentifier::SHA3_384: + case CryptoAlgorithmIdentifier::SHA3_512: return Exception { NotSupportedError }; case CryptoAlgorithmIdentifier::None: return Exception { NotSupportedError }; @@ -803,6 +823,124 @@ void SubtleCrypto::digest(JSC::JSGlobalObject& state, AlgorithmIdentifier&& algo algorithm->digest(WTF::move(data), WTF::move(callback), WTF::move(exceptionCallback), *scriptExecutionContext(), m_workQueue); } +// WICG "Modern Algorithms in the Web Cryptography API": +// https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-supports +// +// Synchronously report whether this SubtleCrypto implementation supports +// a given (operation, algorithm) pair. supports() has to match the exact +// dispatch logic of the corresponding method so that callers can use it for +// progressive enhancement without hitting runtime NotSupportedError for +// algorithms that supports() promised. Any exception produced by the +// normalization step (or the subsequent method-specific checks) is swallowed +// and surfaced as false — supports() itself never rejects or throws other +// than for missing required arguments. +static bool normalizesWithoutException(JSGlobalObject& state, WebCore::SubtleCrypto::AlgorithmIdentifier& alg, Operations op) +{ + auto& vm = state.vm(); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + + // normalizeCryptoAlgorithmParameters takes the identifier by value; + // we clone the variant so callers can retry with a different op. + WebCore::SubtleCrypto::AlgorithmIdentifier copy = alg; + auto params = normalizeCryptoAlgorithmParameters(state, WTF::move(copy), op); + if (scope.exception()) { + scope.clearException(); + return false; + } + return !params.hasException(); +} + +bool SubtleCrypto::supports(JSC::JSGlobalObject& state, const String& operation, AlgorithmIdentifier&& algorithmIdentifier) +{ + auto& vm = state.vm(); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + + // Simple cases: the operation maps to a single normalize bucket. The + // method's runtime behaviour is then "normalize succeeded → run", so + // supports() returns true iff normalization succeeds. + if (operation == "encrypt"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::Encrypt); + if (operation == "decrypt"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::Decrypt); + if (operation == "sign"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::Sign); + if (operation == "verify"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::Verify); + if (operation == "digest"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::Digest); + if (operation == "generateKey"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::GenerateKey); + if (operation == "deriveBits"_s || operation == "deriveKey"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::DeriveBits); + if (operation == "importKey"_s) + return normalizesWithoutException(state, algorithmIdentifier, Operations::ImportKey); + + // exportKey has no dedicated normalization — the real exportKey() only + // checks the CryptoKey's algorithm against isSupportedExportKey(). To + // make supports() answer symmetric questions ("can this algorithm be + // exported?"), we need to first recognise the algorithm at all and then + // run it through the same isSupportedExportKey() gate. Algorithms like + // HKDF/PBKDF2 that normalize as importable but are explicitly excluded + // from isSupportedExportKey() must report false. + if (operation == "exportKey"_s) { + // Resolve the algorithm name to an identifier without running any + // operation-specific validation. + if (std::holds_alternative(algorithmIdentifier)) { + auto identifier = CryptoAlgorithmRegistry::singleton().identifier(std::get(algorithmIdentifier)); + if (!identifier) + return false; + return isSupportedExportKey(state, *identifier); + } + // Dictionary form: pull out the "name" field. + auto& value = std::get>(algorithmIdentifier); + JSValue nameValue = value.get()->get(&state, vm.propertyNames->name); + if (scope.exception()) { + scope.clearException(); + return false; + } + if (!nameValue.isString()) + return false; + auto name = nameValue.toWTFString(&state); + if (scope.exception()) { + scope.clearException(); + return false; + } + auto identifier = CryptoAlgorithmRegistry::singleton().identifier(name); + if (!identifier) + return false; + return isSupportedExportKey(state, *identifier); + } + + // wrapKey/unwrapKey dispatch in the real implementations: + // wrapKey: try WrapKey normalization; on failure, fall back to Encrypt. + // unwrapKey: try UnwrapKey normalization; on failure, fall back to Decrypt. + // supports() must mirror that two-step fallback or it produces false + // negatives for AES-GCM/CBC/CTR/CFB and RSA-OAEP as wrapping algorithms. + if (operation == "wrapKey"_s) { + if (normalizesWithoutException(state, algorithmIdentifier, Operations::WrapKey)) + return true; + return normalizesWithoutException(state, algorithmIdentifier, Operations::Encrypt); + } + if (operation == "unwrapKey"_s) { + if (normalizesWithoutException(state, algorithmIdentifier, Operations::UnwrapKey)) + return true; + return normalizesWithoutException(state, algorithmIdentifier, Operations::Decrypt); + } + + // Operations introduced by the WICG "Modern Algorithms" spec that this + // slice does not implement yet. They flip to true in follow-up PRs. + if (operation == "getPublicKey"_s + || operation == "encapsulateBits"_s + || operation == "encapsulateKey"_s + || operation == "decapsulateBits"_s + || operation == "decapsulateKey"_s) { + return false; + } + + // Unknown operation. + return false; +} + void SubtleCrypto::generateKey(JSC::JSGlobalObject& state, AlgorithmIdentifier&& algorithmIdentifier, bool extractable, Vector&& keyUsages, Ref&& promise) { auto& vm = state.vm(); diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.h b/src/bun.js/bindings/webcrypto/SubtleCrypto.h index cd4f68ddb481..24a7521d96af 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.h +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.h @@ -78,6 +78,10 @@ class SubtleCrypto : public ContextDestructionObserver, public RefCounted&&); void wrapKey(JSC::JSGlobalObject&, KeyFormat, CryptoKey&, CryptoKey& wrappingKey, AlgorithmIdentifier&& wrapAlgorithm, Ref&&); void unwrapKey(JSC::JSGlobalObject&, KeyFormat, BufferSource&& wrappedKey, CryptoKey& unwrappingKey, AlgorithmIdentifier&& unwrapAlgorithm, AlgorithmIdentifier&& unwrappedKeyAlgorithm, bool extractable, Vector&&, Ref&&); + // WICG "Modern Algorithms in the Web Cryptography API" defines this as a + // static on the interface object, not an instance method. It is exposed + // in JS as `SubtleCrypto.supports(operation, algorithm)`. + static bool supports(JSC::JSGlobalObject&, const String& operation, AlgorithmIdentifier&&); private: explicit SubtleCrypto(ScriptExecutionContext*); diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.idl b/src/bun.js/bindings/webcrypto/SubtleCrypto.idl index 358b9950b2a9..90c170ace6c9 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.idl +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.idl @@ -45,4 +45,5 @@ typedef (object or DOMString) AlgorithmIdentifier; Promise exportKey(KeyFormat format, CryptoKey key); [CallWith=CurrentGlobalObject] Promise wrapKey(KeyFormat format, CryptoKey key, CryptoKey wrappingKey, AlgorithmIdentifier wrapAlgorithm); [CallWith=CurrentGlobalObject] Promise unwrapKey(KeyFormat format, BufferSource wrappedKey, CryptoKey unwrappingKey, AlgorithmIdentifier unwrapAlgorithm, AlgorithmIdentifier unwrappedKeyAlgorithm, boolean extractable, sequence keyUsages); + [CallWith=CurrentGlobalObject] static boolean supports(DOMString operation, AlgorithmIdentifier algorithm); }; diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 5936bf7ec476..6188d36fcbac 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -146,6 +146,7 @@ test/js/web/crypto/web-crypto.test.ts test/regression/issue/01466.test.ts test/regression/issue/21311.test.ts test/regression/issue/24399.test.ts +test/regression/issue/29218.test.ts vendor/elysia/test/aot/response.test.ts vendor/elysia/test/cookie/response.test.ts vendor/elysia/test/cookie/signature.test.ts diff --git a/test/regression/issue/29218.test.ts b/test/regression/issue/29218.test.ts new file mode 100644 index 000000000000..8cb6be7119cb --- /dev/null +++ b/test/regression/issue/29218.test.ts @@ -0,0 +1,395 @@ +// https://github.com/oven-sh/bun/issues/29218 +// +// First slice of the WICG "Modern Algorithms in the Web Cryptography API" +// specification: SHA-3 fixed-output hashes (SHA3-256 / SHA3-384 / SHA3-512) +// exposed through `crypto.subtle.digest`, plus the new synchronous feature +// detection method `SubtleCrypto.supports(operation, algorithm)`. +// +// Spec: https://wicg.github.io/webcrypto-modern-algos/ +// +// Test coverage mirrors the intent of the Web Platform Tests that ship with +// the spec (WebCryptoAPI/digest/sha3.tentative.https.any.js and +// WebCryptoAPI/idlharness.*). Digest vectors are the NIST FIPS 202 +// Cryptographic Algorithm Validation Program vectors used by those WPTs, and +// the `supports()` cases exercise the progressive-enhancement semantics that +// the modern-algos spec defines in its "supports" algorithm. +import { describe, expect, test } from "bun:test"; +import { createHash } from "node:crypto"; + +const te = new TextEncoder(); + +function hex(buf: ArrayBuffer | Uint8Array): string { + const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf); + return Array.from(bytes, b => b.toString(16).padStart(2, "0")).join(""); +} + +// Map the Web Crypto SHA-3 algorithm name to the name Node.js's OpenSSL +// bindings use, so we can cross-check SubtleCrypto output against a second +// independent implementation of the same primitive. +const nodeAlg = { + "SHA3-256": "sha3-256", + "SHA3-384": "sha3-384", + "SHA3-512": "sha3-512", +} as const; + +describe("crypto.subtle SHA-3", () => { + // NIST FIPS 202 test vectors for the empty message and for the ASCII + // string "abc". These are the same vectors the WPT uses. + // https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/secure-hashing + const vectors = { + "SHA3-256": { + empty: "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + abc: "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532", + }, + "SHA3-384": { + empty: "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", + abc: "ec01498288516fc926459f58e2c6ad8df9b473cb0fc08c2596da7cf0e49be4b298d88cea927ac7f539f1edf228376d25", + }, + "SHA3-512": { + empty: + "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26", + abc: "b751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0", + }, + } as const; + + for (const [alg, vec] of Object.entries(vectors)) { + test(`${alg} digests the empty string to the FIPS 202 test vector`, async () => { + const out = await crypto.subtle.digest(alg, new Uint8Array(0)); + expect(hex(out)).toBe(vec.empty); + }); + + test(`${alg} digests "abc" to the FIPS 202 test vector`, async () => { + const out = await crypto.subtle.digest(alg, te.encode("abc")); + expect(hex(out)).toBe(vec.abc); + }); + + test(`${alg} digests a multi-block message identically to node:crypto`, async () => { + // 1024 bytes is longer than every SHA-3 sponge rate (the largest rate + // is SHA3-256's 136 bytes; SHA3-512 uses the smallest rate at 72 + // bytes), so this input is guaranteed to span multiple blocks for all + // three variants and exercise multi-block absorption. Cross-check + // against node:crypto (which uses OpenSSL) instead of hard-coding a + // hex string, so both implementations must agree. + const buf = new Uint8Array(1024); + for (let i = 0; i < buf.length; i++) buf[i] = i & 0xff; + + const out = await crypto.subtle.digest(alg, buf); + const reference = createHash(nodeAlg[alg as keyof typeof nodeAlg]) + .update(buf) + .digest(); + expect(hex(out)).toBe(reference.toString("hex")); + + const expectedLen = { "SHA3-256": 32, "SHA3-384": 48, "SHA3-512": 64 }[alg as keyof typeof vectors]; + expect(out.byteLength).toBe(expectedLen); + }); + + test(`${alg} is deterministic across calls`, async () => { + const buf = te.encode("the quick brown fox jumps over the lazy dog"); + const a = await crypto.subtle.digest(alg, buf); + const b = await crypto.subtle.digest(alg, buf); + expect(hex(a)).toBe(hex(b)); + }); + } + + test("SHA-3 digest accepts a dictionary algorithm identifier", async () => { + const out = await crypto.subtle.digest({ name: "SHA3-256" }, te.encode("abc")); + expect(hex(out)).toBe("3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"); + }); + + test("SHA-3 digest is case-insensitive on the algorithm name", async () => { + const out = await crypto.subtle.digest("sha3-256", te.encode("abc")); + expect(hex(out)).toBe("3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"); + }); + + test("unknown SHA-3 variant is rejected with NotSupportedError", async () => { + // SHA3-224 is defined by FIPS 202 but is intentionally not exposed by the + // WICG spec, so it must be rejected rather than silently accepted. + await expect(crypto.subtle.digest("SHA3-224", te.encode("abc"))).rejects.toMatchObject({ + name: "NotSupportedError", + }); + }); + + test("SHA-3 is rejected as a hash sub-algorithm for HMAC/RSA/ECDSA", async () => { + // SHA-3 is implemented only as a top-level `digest` operation in this + // slice of the WICG spec. It is not yet wired into OpenSSL's digest + // dispatcher, CryptoKeyHMAC::getKeyLengthFromHash(), or structured + // clone, so accepting it as a hash sub-algorithm for HMAC/RSA/ECDSA + // would create broken CryptoKey instances that crash at sign() or + // postMessage() time. Reject up front until those paths are wired. + // + // For HMAC and RSA-PSS the hash is supplied at key creation time, so + // importKey()/generateKey() is where the rejection lives. For ECDSA + // the hash is supplied at sign()/verify() time, so we generate a real + // P-256 key pair first (which must still succeed) and then assert + // that sign() rejects when a SHA-3 hash is requested. + const ecdsaPair = (await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [ + "sign", + "verify", + ])) as CryptoKeyPair; + expect(ecdsaPair.privateKey).toBeDefined(); + + for (const alg of ["SHA3-256", "SHA3-384", "SHA3-512"]) { + await expect( + crypto.subtle.importKey("raw", new Uint8Array(32), { name: "HMAC", hash: alg }, true, ["sign"]), + ).rejects.toMatchObject({ name: "NotSupportedError" }); + await expect(crypto.subtle.generateKey({ name: "HMAC", hash: alg }, true, ["sign"])).rejects.toMatchObject({ + name: "NotSupportedError", + }); + await expect( + crypto.subtle.generateKey( + { name: "RSA-PSS", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: alg }, + true, + ["sign"], + ), + ).rejects.toMatchObject({ name: "NotSupportedError" }); + await expect( + crypto.subtle.sign({ name: "ECDSA", hash: alg }, ecdsaPair.privateKey, te.encode("msg")), + ).rejects.toMatchObject({ name: "NotSupportedError" }); + } + }); + + // The full (algorithm × input-size) matrix shipped by WPT's + // WebCryptoAPI/digest/sha3.tentative.https.any.js, ported to hex. If you + // need to verify these bytes against upstream, compare to: + // https://github.com/web-platform-tests/wpt/blob/master/WebCryptoAPI/digest/sha3.tentative.https.any.js + // + // The WPT file exercises four source payloads per algorithm: + // - empty: 0 bytes + // - short: 16 bytes (one sample block) + // - medium: 85 bytes + // - long: 85 * 1024 bytes (multi-block) + // Each digest must match to the byte. + test("SHA-3 matches every vector from the WPT sha3.tentative digest suite", async () => { + const sourceData = { + empty: new Uint8Array(0), + short: new Uint8Array([21, 110, 234, 124, 193, 76, 86, 203, 148, 219, 3, 10, 74, 157, 149, 255]), + medium: new Uint8Array([ + 182, 200, 249, 223, 100, 140, 208, 136, 183, 15, 56, 231, 65, 151, 177, 140, 184, 30, 30, 67, 80, 213, 11, 204, + 184, 251, 90, 115, 121, 200, 123, 178, 227, 214, 237, 84, 97, 237, 30, 159, 54, 243, 64, 163, 150, 42, 68, 107, + 129, 91, 121, 75, 75, 212, 58, 68, 3, 80, 32, 119, 178, 37, 108, 200, 7, 131, 127, 58, 172, 209, 24, 235, 75, + 156, 43, 174, 184, 151, 6, 134, 37, 171, 172, 161, 147, + ]), + long: new Uint8Array(0), // filled in below + }; + const longBuf = new Uint8Array(1024 * sourceData.medium.byteLength); + for (let i = 0; i < 1024; i++) longBuf.set(sourceData.medium, i * sourceData.medium.byteLength); + sourceData.long = longBuf; + + const wptVectors = { + "SHA3-256": { + empty: "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + short: "3059af7aa33b517084e8ad7bbc4fb208a44c28ef32b4698d103dd540e4f91aa1", + medium: "1fa7cd1da74cd8046417508c8314e74a9a4a9d38f9f18e6cb215b8c891a0a80e", + long: "b2cfc61e0386cdaef5e10a2be189891f5ef52a7624bfcd8edc893acc64fec600", + }, + "SHA3-384": { + empty: "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", + short: "54b8f0e4cf4974de740098f66b3024479b01631315a6773606c33eadc32556a6e778e08f0225ae79265aec666cb2390b", + medium: "437b7d8b68b250b5c1739ea4cc86db2033879dfb18de292c9c50d9c193a4c79a08a6cae3f4e483c2795ea5d1ef7e69d2", + long: "3b39c4c97ad87613305d0ccc987181713e2d5e84b1f9760011bcce0c297499005bdce8a3d2409b5ad0164f32bb8778d0", + }, + "SHA3-512": { + empty: + "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26", + short: + "2dd2e07a62e6ad0498ba84f313c4d4024cb46001f78f75db336b0d4d8bd2a9ec152c4ad20878735d82ba0872ecf59608ef3ced2b2a8669427e7da31e362333d8", + medium: + "e640a21909536640369e9b0a48931c5cb2efcbc91fecf247306bc96a0e4ca33307cb8e1b9af367946dd01c243f3907508d04f1692a3161df1f898de8ee25febe", + long: "bd262cecf565c338032de5ba0138f0aacfe7dde83d272d0d37d952829ed25de1a1342d98659ef7d2fa4aca7ce2b1aa0784d8fc1dcbf81bcec7a7431a3da36bf7", + }, + } as const; + + for (const [alg, sizes] of Object.entries(wptVectors)) { + for (const [size, expected] of Object.entries(sizes)) { + const input = sourceData[size as keyof typeof sourceData]; + const got = await crypto.subtle.digest(alg, input); + expect(hex(got)).toBe(expected); + } + } + }); +}); + +describe("SubtleCrypto.supports", () => { + test("is a function of length 2", () => { + expect(typeof SubtleCrypto.supports).toBe("function"); + expect(SubtleCrypto.supports.length).toBe(2); + }); + + test("returns true for classic algorithms that existed before this slice", () => { + // Every algorithm Bun already supported must continue to report true so + // that supports() is a safe feature-detect for the whole API surface. + expect(SubtleCrypto.supports("digest", "SHA-1")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA-256")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA-384")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA-512")).toBe(true); + expect(SubtleCrypto.supports("generateKey", { name: "AES-GCM", length: 256 })).toBe(true); + expect(SubtleCrypto.supports("importKey", "AES-GCM")).toBe(true); + expect(SubtleCrypto.supports("importKey", "PBKDF2")).toBe(true); + expect( + SubtleCrypto.supports("deriveBits", { + name: "HKDF", + hash: "SHA-256", + salt: new Uint8Array(), + info: new Uint8Array(), + }), + ).toBe(true); + }); + + test("returns true for SHA-3 digest", () => { + expect(SubtleCrypto.supports("digest", "SHA3-256")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA3-384")).toBe(true); + expect(SubtleCrypto.supports("digest", "SHA3-512")).toBe(true); + }); + + test("accepts dictionary-form algorithm identifiers", () => { + expect(SubtleCrypto.supports("digest", { name: "SHA3-256" })).toBe(true); + expect(SubtleCrypto.supports("digest", { name: "SHA-256" })).toBe(true); + }); + + describe("mirrors dispatch-time fallback", () => { + test("wrapKey with AES-KW uses the dedicated WrapKey path", () => { + expect(SubtleCrypto.supports("wrapKey", "AES-KW")).toBe(true); + expect(SubtleCrypto.supports("wrapKey", { name: "AES-KW" })).toBe(true); + }); + + test("wrapKey with an encryption algorithm is reported via the Encrypt fallback", () => { + // wrapKey() in the real implementation tries WrapKey normalization + // and, on NotSupportedError, falls back to Encrypt normalization so + // that AES-GCM/CBC/CTR/CFB and RSA-OAEP can be used as wrapping + // algorithms. supports() must mirror that fallback or it reports + // false for operations that actually succeed. + expect(SubtleCrypto.supports("wrapKey", { name: "AES-GCM", iv: new Uint8Array(12) })).toBe(true); + expect(SubtleCrypto.supports("wrapKey", { name: "AES-CBC", iv: new Uint8Array(16) })).toBe(true); + expect(SubtleCrypto.supports("wrapKey", { name: "AES-CTR", counter: new Uint8Array(16), length: 64 })).toBe(true); + expect(SubtleCrypto.supports("wrapKey", { name: "RSA-OAEP" })).toBe(true); + }); + + test("unwrapKey with a decryption algorithm is reported via the Decrypt fallback", () => { + // Symmetric to the wrapKey case, with Decrypt as the fallback. + expect(SubtleCrypto.supports("unwrapKey", "AES-KW")).toBe(true); + expect(SubtleCrypto.supports("unwrapKey", { name: "AES-GCM", iv: new Uint8Array(12) })).toBe(true); + expect(SubtleCrypto.supports("unwrapKey", { name: "AES-CBC", iv: new Uint8Array(16) })).toBe(true); + expect(SubtleCrypto.supports("unwrapKey", { name: "RSA-OAEP" })).toBe(true); + }); + + test("wrapKey/unwrapKey reject algorithms that are neither WrapKey nor en/decryptable", () => { + expect(SubtleCrypto.supports("wrapKey", "HKDF")).toBe(false); + expect(SubtleCrypto.supports("wrapKey", "PBKDF2")).toBe(false); + expect(SubtleCrypto.supports("wrapKey", "SHA-256")).toBe(false); + expect(SubtleCrypto.supports("unwrapKey", "HKDF")).toBe(false); + expect(SubtleCrypto.supports("unwrapKey", "SHA-256")).toBe(false); + }); + }); + + describe("exportKey", () => { + test("reports true for exportable algorithms", () => { + // Matches isSupportedExportKey() in SubtleCrypto.cpp. Note that + // Bun registers AES-CFB under the "AES-CFB-8" name, matching the + // original WebKit spelling. + for (const alg of [ + "AES-GCM", + "AES-CBC", + "AES-CTR", + "AES-CFB-8", + "AES-KW", + "HMAC", + "ECDSA", + "ECDH", + "Ed25519", + "X25519", + ]) { + expect(SubtleCrypto.supports("exportKey", alg)).toBe(true); + } + }); + + test("reports false for key-derivation algorithms that are not exportable", () => { + // HKDF and PBKDF2 normalize as importable but isSupportedExportKey() + // excludes them, and the real exportKey() rejects with + // NotSupportedError. supports() must match that behaviour so + // progressive-enhancement callers do not hit false positives. + expect(SubtleCrypto.supports("exportKey", "HKDF")).toBe(false); + expect(SubtleCrypto.supports("exportKey", "PBKDF2")).toBe(false); + }); + + test("reports false for hash-only algorithms", () => { + expect(SubtleCrypto.supports("exportKey", "SHA-256")).toBe(false); + expect(SubtleCrypto.supports("exportKey", "SHA3-256")).toBe(false); + }); + + test("reports false for unknown algorithms", () => { + expect(SubtleCrypto.supports("exportKey", "bogus")).toBe(false); + expect(SubtleCrypto.supports("exportKey", { name: "bogus" })).toBe(false); + }); + }); + + test("returns false for unknown algorithms", () => { + expect(SubtleCrypto.supports("digest", "MD5")).toBe(false); + expect(SubtleCrypto.supports("digest", "not-a-real-algorithm")).toBe(false); + expect(SubtleCrypto.supports("digest", { name: "nope" })).toBe(false); + }); + + test("returns false for unknown operations", () => { + expect(SubtleCrypto.supports("not-a-real-op", "SHA-256")).toBe(false); + }); + + test("returns false for modern-algos operations that are not yet implemented", () => { + // encapsulateKey/Bits, decapsulateKey/Bits, and getPublicKey are new + // surface introduced by the WICG spec. They must report false for now + // so callers can progressively adopt them; they will flip to true in + // follow-up PRs as each algorithm lands. + expect(SubtleCrypto.supports("encapsulateKey", "ML-KEM-768")).toBe(false); + expect(SubtleCrypto.supports("encapsulateBits", "ML-KEM-768")).toBe(false); + expect(SubtleCrypto.supports("decapsulateKey", "ML-KEM-768")).toBe(false); + expect(SubtleCrypto.supports("decapsulateBits", "ML-KEM-768")).toBe(false); + expect(SubtleCrypto.supports("getPublicKey", "RSA-PSS")).toBe(false); + expect(SubtleCrypto.supports("getPublicKey", "Ed25519")).toBe(false); + // getPublicKey is conceptually asymmetric-only; even if it were + // implemented, symmetric algorithms must never report true. + expect(SubtleCrypto.supports("getPublicKey", "AES-GCM")).toBe(false); + expect(SubtleCrypto.supports("getPublicKey", "HMAC")).toBe(false); + }); + + test("returns false (not a throw) for malformed algorithm input", () => { + // supports() must never throw for any well-formed call, even with + // obviously bad input — that is the point of a synchronous + // feature-detect method. + expect(SubtleCrypto.supports("digest", null as any)).toBe(false); + expect(SubtleCrypto.supports("digest", 42 as any)).toBe(false); + expect(SubtleCrypto.supports("digest", [] as any)).toBe(false); + expect(SubtleCrypto.supports("exportKey", null as any)).toBe(false); + expect(SubtleCrypto.supports("exportKey", {} as any)).toBe(false); + }); + + test("throws when called with fewer than two arguments", () => { + // The only way supports() is allowed to throw is when JavaScript itself + // rejects the call (missing required arguments), mirroring every other + // SubtleCrypto method. + // @ts-expect-error + expect(() => SubtleCrypto.supports()).toThrow(); + // @ts-expect-error + expect(() => SubtleCrypto.supports("digest")).toThrow(); + }); + + test("supports() answers match what the underlying method would do", async () => { + // End-to-end cross-check: for each (op, alg) pair that supports() + // reports true, the real method must not throw NotSupportedError for + // well-formed input. This is the property the modern-algos spec relies + // on for progressive enhancement. + const buf = te.encode("hello"); + for (const alg of ["SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512"]) { + expect(SubtleCrypto.supports("digest", alg)).toBe(true); + const out = await crypto.subtle.digest(alg, buf); + expect(out).toBeInstanceOf(ArrayBuffer); + } + + // And the negative: every op/alg pair that reports false must actually + // reject at runtime. + expect(SubtleCrypto.supports("digest", "MD5")).toBe(false); + await expect(crypto.subtle.digest("MD5", buf)).rejects.toMatchObject({ name: "NotSupportedError" }); + + expect(SubtleCrypto.supports("exportKey", "HKDF")).toBe(false); + // importKey for HKDF to get an actual CryptoKey, then exportKey must reject. + const hkdfKey = await crypto.subtle.importKey("raw", buf, "HKDF", false, ["deriveBits"]); + await expect(crypto.subtle.exportKey("raw", hkdfKey)).rejects.toMatchObject({ name: "NotSupportedError" }); + }); +});