Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion experiments/WebScene.NativeEngine.Probe/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ if(WEBSCENE_NATIVE_ENGINE_STATIC)
else()
set(webscene_runtime_kind SHARED)
endif()
add_library(webscene_native_engine ${webscene_runtime_kind} native/webscene_native_engine.cpp)
add_library(webscene_native_engine ${webscene_runtime_kind}
native/webscene_native_engine.cpp
native/webscene_secure_random.cpp)
if(TARGET webscene_core)
target_link_libraries(webscene_native_engine PRIVATE webscene_core)
else()
Expand All @@ -89,6 +91,24 @@ target_compile_features(webscene_native_engine PRIVATE cxx_std_20)
target_compile_definitions(webscene_native_engine PRIVATE WEBSCENE_NATIVE_ENGINE_BUILD)
target_include_directories(webscene_native_engine PUBLIC native)

if(WIN32)
target_link_libraries(webscene_native_engine PRIVATE bcrypt)
endif()

if(BUILD_TESTING)
add_executable(webscene_secure_random_tests
tests/secure_random_tests.cpp
native/webscene_secure_random.cpp)
target_compile_features(webscene_secure_random_tests PRIVATE cxx_std_20)
target_include_directories(webscene_secure_random_tests PRIVATE native)
if(WIN32)
target_link_libraries(webscene_secure_random_tests PRIVATE bcrypt)
endif()
add_test(NAME webscene_secure_random_tests COMMAND webscene_secure_random_tests)
set_tests_properties(webscene_secure_random_tests PROPERTIES
LABELS "webcrypto;security;performance" TIMEOUT 10)
endif()

option(WEBSCENE_NATIVE_ENGINE_ENABLE_MEDIA "Build native HTML media and Web Audio services" ON)
if(WEBSCENE_NATIVE_ENGINE_ENABLE_MEDIA)
add_subdirectory(native/media)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#include "webscene_secure_random.h"

#include <algorithm>
#include <cerrno>
#include <limits>

#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <bcrypt.h>
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
#include <sys/random.h>
#if defined(__linux__)
#define WEBSCENE_USE_GETRANDOM 1
#endif
#elif defined(__OpenBSD__)
#include <unistd.h>
#else
#error "WebScene secure random requires an audited operating-system CSPRNG binding"
#endif

namespace webscene_native {

namespace {

secure_random_chunk_result operating_system_chunk(
void*,
std::span<std::uint8_t> output) noexcept
{
#if defined(_WIN32)
const auto count = static_cast<ULONG>(output.size());
if (!BCRYPT_SUCCESS(BCryptGenRandom(
nullptr,
reinterpret_cast<PUCHAR>(output.data()),
count,
BCRYPT_USE_SYSTEM_PREFERRED_RNG))) {
return {secure_random_chunk_status::failure, 0U};
}
return {secure_random_chunk_status::success, output.size()};
#elif defined(WEBSCENE_USE_GETRANDOM)
const auto received = getrandom(output.data(), output.size(), 0);
if (received < 0) {
return {
errno == EINTR
? secure_random_chunk_status::interrupted
: secure_random_chunk_status::failure,
0U};
}
return {
secure_random_chunk_status::success,
static_cast<std::size_t>(received)};
#else
if (getentropy(output.data(), output.size()) != 0) {
return {secure_random_chunk_status::failure, 0U};
}
return {secure_random_chunk_status::success, output.size()};
#endif
}

}

bool fill_secure_random_from_source(
std::span<std::uint8_t> output,
std::size_t maximum_chunk_size,
secure_random_chunk_source source,
void* context) noexcept
{
if (output.empty()) return true;
if (maximum_chunk_size == 0U || source == nullptr) return false;
while (!output.empty()) {
const auto requested = output.first(
std::min(output.size(), maximum_chunk_size));
const auto result = source(context, requested);
if (result.status == secure_random_chunk_status::interrupted) continue;
if (result.status != secure_random_chunk_status::success
|| result.size == 0U
|| result.size > requested.size()) {
return false;
}
output = output.subspan(result.size);
}
return true;
}

bool fill_secure_random(std::span<std::uint8_t> output) noexcept
{
#if defined(_WIN32)
constexpr auto maximum_chunk = static_cast<std::size_t>(
std::numeric_limits<ULONG>::max());
#elif defined(WEBSCENE_USE_GETRANDOM)
constexpr auto maximum_chunk = std::numeric_limits<std::size_t>::max();
#else
// getentropy() rejects requests larger than 256 bytes.
constexpr std::size_t maximum_chunk = 256U;
#endif
return fill_secure_random_from_source(
output, maximum_chunk, operating_system_chunk, nullptr);
}

std::array<char, 36> format_uuid_v4(
std::array<std::uint8_t, 16> bytes) noexcept
{
bytes[6] = static_cast<std::uint8_t>((bytes[6] & 0x0fU) | 0x40U);
bytes[8] = static_cast<std::uint8_t>((bytes[8] & 0x3fU) | 0x80U);
constexpr char digits[] = "0123456789abcdef";
std::array<char, 36> uuid{};
std::size_t output = 0U;
for (std::size_t index = 0; index < bytes.size(); ++index) {
if (index == 4U || index == 6U || index == 8U || index == 10U) {
uuid[output++] = '-';
}
uuid[output++] = digits[bytes[index] >> 4U];
uuid[output++] = digits[bytes[index] & 0x0fU];
}
return uuid;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#pragma once

#include <array>
#include <cstddef>
#include <cstdint>
#include <span>

namespace webscene_native {

inline constexpr std::size_t web_crypto_random_quota = 65'536U;

enum class secure_random_chunk_status : std::uint8_t {
success,
interrupted,
failure,
};

struct secure_random_chunk_result final {
secure_random_chunk_status status{secure_random_chunk_status::failure};
std::size_t size{0U};
};

using secure_random_chunk_source = secure_random_chunk_result (*)(
void* context,
std::span<std::uint8_t> output) noexcept;

// Drives a bounded chunk source until the complete span is filled. This is
// exposed inside the native component so failure, interruption, and partial
// reads can be tested without weakening the production operating-system path.
[[nodiscard]] bool fill_secure_random_from_source(
std::span<std::uint8_t> output,
std::size_t maximum_chunk_size,
secure_random_chunk_source source,
void* context) noexcept;

// Fills the complete span from the operating-system CSPRNG. A failure never
// falls back to a userspace pseudo-random generator.
[[nodiscard]] bool fill_secure_random(std::span<std::uint8_t> output) noexcept;

// Applies the UUIDv4 version and RFC variant bits and emits the canonical
// lower-case representation.
[[nodiscard]] std::array<char, 36> format_uuid_v4(
std::array<std::uint8_t, 16> bytes) noexcept;

}
147 changes: 132 additions & 15 deletions experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "webscene_performance_timeline_compatibility.h"
#include "webscene_file_reader_compatibility.h"
#include "webscene_stylesheet_cssom_compatibility.h"
#include "webscene_secure_random.h"

#include "webscene_native_dom.h"
#include "webscene_native_style_defaults.h"
Expand Down Expand Up @@ -3081,6 +3082,135 @@ struct v8_dom_runtime::implementation final {
}
}

static bool is_web_crypto_integer_array(v8::Local<v8::Value> value)
{
return value->IsInt8Array()
|| value->IsUint8Array()
|| value->IsUint8ClampedArray()
|| value->IsInt16Array()
|| value->IsUint16Array()
|| value->IsInt32Array()
|| value->IsUint32Array()
|| value->IsBigInt64Array()
|| value->IsBigUint64Array();
}

static bool require_crypto_receiver(const v8::FunctionCallbackInfo<v8::Value>& info)
{
if (info.Data()->IsObject() && info.This() == info.Data().As<v8::Object>()) return true;
info.GetIsolate()->ThrowException(v8::Exception::TypeError(
js_string(info.GetIsolate(), "Illegal invocation")));
return false;
}

static void crypto_get_random_values(const v8::FunctionCallbackInfo<v8::Value>& info)
{
auto* isolate = info.GetIsolate();
if (!require_crypto_receiver(info)) return;
if (info.Length() < 1 || !is_web_crypto_integer_array(info[0])) {
isolate->ThrowException(v8::Exception::TypeError(
js_string(isolate, "Crypto.getRandomValues requires an integer TypedArray")));
return;
}
auto view = info[0].As<v8::ArrayBufferView>();
auto buffer = view->Buffer();
if (buffer->WasDetached()) {
isolate->ThrowException(v8::Exception::TypeError(
js_string(isolate, "Crypto.getRandomValues cannot fill a detached TypedArray")));
return;
}
if (view->ByteLength() > web_crypto_random_quota) {
throw_dom_exception(
info,
"Crypto.getRandomValues requests are limited to 65,536 bytes",
"QuotaExceededError");
return;
}
auto backing = buffer->GetBackingStore();
auto output = std::span<std::uint8_t>{};
if (view->ByteLength() != 0U) {
auto* destination = static_cast<std::uint8_t*>(backing->Data()) + view->ByteOffset();
output = {destination, view->ByteLength()};
}
if (!fill_secure_random(output)) {
throw_dom_exception(
info,
"The operating-system secure random source failed",
"OperationError");
return;
}
if (auto* self = current(isolate); self != nullptr) {
self->record_feature(
"web-api",
"Crypto.getRandomValues",
"supported",
"integer TypedArray filling from the operating-system CSPRNG",
"native-binding");
}
info.GetReturnValue().Set(info[0]);
}

static void crypto_random_uuid(const v8::FunctionCallbackInfo<v8::Value>& info)
{
auto* isolate = info.GetIsolate();
if (!require_crypto_receiver(info)) return;
std::array<std::uint8_t, 16> bytes{};
if (!fill_secure_random(bytes)) {
throw_dom_exception(
info,
"The operating-system secure random source failed",
"OperationError");
return;
}
const auto uuid = format_uuid_v4(bytes);
if (auto* self = current(isolate); self != nullptr) {
self->record_feature(
"web-api",
"Crypto.randomUUID",
"supported",
"RFC 4122 version 4 UUIDs from the operating-system CSPRNG",
"native-binding");
}
info.GetReturnValue().Set(v8::String::NewFromUtf8(
isolate,
uuid.data(),
v8::NewStringType::kNormal,
static_cast<int>(uuid.size())).ToLocalChecked());
}

void install_crypto_globals(
v8::Local<v8::Context> local_context,
v8::Local<v8::Object> global)
{
auto crypto = v8::Object::New(isolate);
auto get_random_values = v8::Function::New(
local_context, crypto_get_random_values, crypto, 1).ToLocalChecked();
auto random_uuid = v8::Function::New(
local_context, crypto_random_uuid, crypto, 0).ToLocalChecked();
get_random_values->SetName(js_string(isolate, "getRandomValues"));
random_uuid->SetName(js_string(isolate, "randomUUID"));
crypto->DefineOwnProperty(
local_context,
js_string(isolate, "getRandomValues"),
get_random_values,
v8::DontEnum).Check();
crypto->DefineOwnProperty(
local_context,
js_string(isolate, "randomUUID"),
random_uuid,
v8::DontEnum).Check();
crypto->DefineOwnProperty(
local_context,
v8::Symbol::GetToStringTag(isolate),
js_string(isolate, "Crypto"),
static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontEnum)).Check();
global->DefineOwnProperty(
local_context,
js_string(isolate, "crypto"),
crypto,
static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontEnum)).Check();
}

void install_globals(v8::Local<v8::Context> local_context)
{
auto global = local_context->Global();
Expand Down Expand Up @@ -3723,21 +3853,7 @@ struct v8_dom_runtime::implementation final {
URL: { value: WebSceneURL, configurable: true },
URLSearchParams: { value: WebSceneURLSearchParams, configurable: true },
FormData: { value: WebSceneFormData, configurable: true },
DOMException: { value: WebSceneDOMException, configurable: true },
crypto: { value: {
getRandomValues(array) {
__webSceneRecordWebApi(
'Crypto.getRandomValues', 'partially-supported',
'integer TypedArray filling without a cryptographic entropy guarantee');
if (!ArrayBuffer.isView(array) || array instanceof DataView) {
throw new TypeError('Expected an integer TypedArray');
}
for (let index = 0; index < array.length; index++) {
array[index] = Math.floor(Math.random() * 256);
}
return array;
}
}, configurable: true }
DOMException: { value: WebSceneDOMException, configurable: true }
});
)JS"};
std::string crypto_source;
Expand All @@ -3746,6 +3862,7 @@ struct v8_dom_runtime::implementation final {
local_context,
js_string(isolate, std::string(crypto_source).c_str())).ToLocalChecked();
crypto_script->Run(local_context).ToLocalChecked();
install_crypto_globals(local_context, local_context->Global());
local_context->Global()->Set(local_context, js_string(isolate, "structuredClone"),
v8::Function::New(local_context, structured_clone, {}, 1).ToLocalChecked()).Check();
install_performance_timeline(local_context);
Expand Down
Loading
Loading