diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index f2ec997e..3877871a 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -949,6 +949,7 @@ pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/File.zig"),
@import("../webapi/Console.zig"),
@import("../webapi/Crypto.zig"),
+ @import("../webapi/SubtleCrypto.zig"),
@import("../webapi/net/FormData.zig"),
@import("../webapi/net/Headers.zig"),
@import("../webapi/net/Request.zig"),
diff --git a/src/browser/tests/crypto-worker.js b/src/browser/tests/crypto-worker.js
new file mode 100644
index 00000000..90d948d0
--- /dev/null
+++ b/src/browser/tests/crypto-worker.js
@@ -0,0 +1,92 @@
+// Exercises crypto APIs inside a worker. Posts 'ready' once the message
+// handler is wired so the page knows it can send a command without racing
+// worker startup. Receives the command, runs the crypto operation, and
+// posts the result back.
+self.onmessage = async function(e) {
+ const cmd = e.data;
+ try {
+ if (cmd.kind === 'getRandomValues') {
+ const ta = new Uint8Array(32);
+ const same = crypto.getRandomValues(ta) === ta;
+ const uniq = new Set(Array.from(ta));
+ postMessage({ ok: true, same, looks_random: uniq.size > 8 });
+ return;
+ }
+
+ if (cmd.kind === 'randomUUID') {
+ const uuid = crypto.randomUUID();
+ const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+ postMessage({ ok: true, type: typeof uuid, length: uuid.length, valid: regex.test(uuid) });
+ return;
+ }
+
+ if (cmd.kind === 'digest') {
+ const buffer = await crypto.subtle.digest('sha-256', new TextEncoder().encode('over 9000'));
+ const hex = [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
+ postMessage({ ok: true, hex });
+ return;
+ }
+
+ if (cmd.kind === 'hmac') {
+ const key = await crypto.subtle.generateKey(
+ { name: 'HMAC', hash: { name: 'SHA-512' } },
+ true,
+ ['sign', 'verify'],
+ );
+ const raw = await crypto.subtle.exportKey('raw', key);
+ const encoder = new TextEncoder();
+ const signature = await crypto.subtle.sign('HMAC', key, encoder.encode('Hello, world!'));
+ const verified = await crypto.subtle.verify(
+ { name: 'HMAC' },
+ key,
+ signature,
+ encoder.encode('Hello, world!'),
+ );
+ postMessage({
+ ok: true,
+ key_type: typeof key,
+ raw_byte_length: raw.byteLength,
+ is_array_buffer: signature instanceof ArrayBuffer,
+ verified,
+ });
+ return;
+ }
+
+ if (cmd.kind === 'x25519') {
+ const { privateKey, publicKey } = await crypto.subtle.generateKey(
+ { name: 'X25519' },
+ true,
+ ['deriveBits'],
+ );
+ const sharedKey = await crypto.subtle.deriveBits(
+ { name: 'X25519', public: publicKey },
+ privateKey,
+ 128,
+ );
+ postMessage({
+ ok: true,
+ private_key_type: typeof privateKey,
+ public_key_type: typeof publicKey,
+ shared_byte_length: sharedKey.byteLength,
+ });
+ return;
+ }
+
+ if (cmd.kind === 'generateKey-rejection') {
+ let err_name = null;
+ try {
+ await crypto.subtle.generateKey({ name: 'AES-CBC', length: 128 }, true, ['sign']);
+ } catch (err) {
+ err_name = err.name;
+ }
+ postMessage({ ok: true, err_name });
+ return;
+ }
+
+ postMessage({ ok: false, err: 'unknown command' });
+ } catch (err) {
+ postMessage({ ok: false, err: String(err), stack: err.stack });
+ }
+};
+
+postMessage({ ready: true });
diff --git a/src/browser/tests/crypto.html b/src/browser/tests/crypto.html
index 59d6522e..f588b743 100644
--- a/src/browser/tests/crypto.html
+++ b/src/browser/tests/crypto.html
@@ -261,3 +261,96 @@
testing.expectEqual("6cad17e6f3f76680d6dd18ed043b75b4f6e1aa1d08b917294942e882fb6466c3510948c34af8b903ed0725b582b3b39c0e485ae2c1b7dfdb192ee38b79c782b6", await hash('sha-512', 'over 9000'));
});
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/SubtleCrypto.zig b/src/browser/webapi/SubtleCrypto.zig
index f6b45ccf..3220352c 100644
--- a/src/browser/webapi/SubtleCrypto.zig
+++ b/src/browser/webapi/SubtleCrypto.zig
@@ -20,7 +20,6 @@ const std = @import("std");
const lp = @import("lightpanda");
const crypto = @import("../../sys/libcrypto.zig");
-const Frame = @import("../Frame.zig");
const js = @import("../js/js.zig");
const CryptoKey = @import("CryptoKey.zig");
@@ -34,6 +33,7 @@ const X25519 = @import("crypto/X25519.zig");
const log = lp.log;
const String = lp.String;
+const Execution = js.Execution;
/// The SubtleCrypto interface of the Web Crypto API provides a number of low-level
/// cryptographic functions.
@@ -49,11 +49,11 @@ pub fn generateKey(
algo: algorithm.Init,
extractable: bool,
key_usages: []const []const u8,
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
- const local = frame.js.local.?;
+ const local = exec.context.local.?;
switch (algo) {
- .hmac_key_gen => |params| return HMAC.init(params, extractable, key_usages, frame),
+ .hmac_key_gen => |params| return HMAC.init(params, extractable, key_usages, exec),
.aes_key_gen => |params| {
AES.validate(params, key_usages) catch |err| {
return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
@@ -72,8 +72,8 @@ pub fn generateKey(
};
log.warn(.not_implemented, "generateKey", .{ .name = params.name });
},
- .name => |js_name| return generateKeyFromName(try js_name.toSSO(false), extractable, key_usages, frame),
- .object => |object| return generateKeyFromName(try object.name.toSSO(false), extractable, key_usages, frame),
+ .name => |js_name| return generateKeyFromName(try js_name.toSSO(false), extractable, key_usages, exec),
+ .object => |object| return generateKeyFromName(try object.name.toSSO(false), extractable, key_usages, exec),
.invalid => return local.rejectPromise(.{ .type_error = "invalid algorithm" }),
}
@@ -84,10 +84,10 @@ fn generateKeyFromName(
name: String,
extractable: bool,
key_usages: []const []const u8,
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
- return _generateKeyFromName(name, extractable, key_usages, frame) catch |err| {
- return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = err } });
+ return _generateKeyFromName(name, extractable, key_usages, exec) catch |err| {
+ return exec.context.local.?.rejectPromise(.{ .dom_exception = .{ .err = err } });
};
}
@@ -95,10 +95,10 @@ fn _generateKeyFromName(
name: String,
extractable: bool,
key_usages: []const []const u8,
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
if (name.eql(comptime .wrap("X25519"))) {
- return X25519.init(extractable, key_usages, frame);
+ return X25519.init(extractable, key_usages, exec);
}
{
@@ -145,14 +145,15 @@ pub fn exportKey(
_: *const SubtleCrypto,
format: []const u8,
key: *CryptoKey,
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
+ const local = exec.context.local.?;
if (!key.canExportKey()) {
- return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
}
if (std.mem.eql(u8, format, "raw")) {
- return frame.js.local.?.resolvePromise(js.ArrayBuffer{ .values = key._key });
+ return local.resolvePromise(js.ArrayBuffer{ .values = key._key });
}
const is_unsupported = std.mem.eql(u8, format, "pkcs8") or
@@ -160,10 +161,10 @@ pub fn exportKey(
if (is_unsupported) {
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
- return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
}
- return frame.js.local.?.rejectPromise(.{ .type_error = "invalid format" });
+ return local.rejectPromise(.{ .type_error = "invalid format" });
}
/// Derive a secret key from a master key.
@@ -172,27 +173,28 @@ pub fn deriveBits(
algo: algorithm.Derive,
base_key: *const CryptoKey, // Private key.
length: usize,
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
+ const local = exec.context.local.?;
return switch (algo) {
.ecdh_or_x25519 => |params| {
const name = params.name;
if (std.mem.eql(u8, name, "X25519")) {
- const result = X25519.deriveBits(base_key, params.public, length, frame) catch |err| switch (err) {
- error.InvalidAccessError => return frame.js.local.?.rejectPromise(.{
+ const result = X25519.deriveBits(base_key, params.public, length, exec) catch |err| switch (err) {
+ error.InvalidAccessError => return local.rejectPromise(.{
.dom_exception = .{ .err = error.InvalidAccessError },
}),
else => return err,
};
- return frame.js.local.?.resolvePromise(result);
+ return local.resolvePromise(result);
}
if (std.mem.eql(u8, name, "ECDH")) {
log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
}
- return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
},
};
}
@@ -204,14 +206,14 @@ pub fn sign(
algo: algorithm.Sign,
key: *CryptoKey,
data: []const u8, // ArrayBuffer.
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
return switch (key._type) {
// Call sign for HMAC.
- .hmac => return HMAC.sign(algo, key, data, frame),
+ .hmac => return HMAC.sign(algo, key, data, exec),
else => {
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
- return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
+ return exec.context.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
},
};
}
@@ -223,33 +225,34 @@ pub fn verify(
key: *const CryptoKey,
signature: []const u8, // ArrayBuffer.
data: []const u8, // ArrayBuffer.
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
+ const local = exec.context.local.?;
if (!algo.isHMAC()) {
- return frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
}
return switch (key._type) {
- .hmac => HMAC.verify(key, signature, data, frame),
- else => frame.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
+ .hmac => HMAC.verify(key, signature, data, exec),
+ else => local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
};
}
/// Generates a digest of the given data, using the specified hash function.
-pub fn digest(_: *const SubtleCrypto, algo: []const u8, data: js.TypedArray(u8), frame: *Frame) !js.Promise {
- const local = frame.js.local.?;
+pub fn digest(_: *const SubtleCrypto, algo: []const u8, data: js.TypedArray(u8), exec: *const Execution) !js.Promise {
+ const local = exec.context.local.?;
if (algo.len > 10) {
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
}
- const normalized = std.ascii.upperString(&frame.buf, algo);
+ const normalized = std.ascii.upperString(exec.buf, algo);
const digest_type = crypto.findDigest(normalized) catch {
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
};
const bytes = data.values;
- const out = frame.buf[0..crypto.EVP_MAX_MD_SIZE];
+ const out = exec.buf[0..crypto.EVP_MAX_MD_SIZE];
var out_size: c_uint = 0;
const result = crypto.EVP_Digest(bytes.ptr, bytes.len, out, &out_size, digest_type, null);
lp.assert(result == 1, "SubtleCrypto.digest", .{ .algo = algo });
diff --git a/src/browser/webapi/crypto/HMAC.zig b/src/browser/webapi/crypto/HMAC.zig
index 9d0d4335..3ab0e507 100644
--- a/src/browser/webapi/crypto/HMAC.zig
+++ b/src/browser/webapi/crypto/HMAC.zig
@@ -22,19 +22,20 @@ const std = @import("std");
const lp = @import("lightpanda");
const crypto = @import("../../../sys/libcrypto.zig");
-const Frame = @import("../../Frame.zig");
const js = @import("../../js/js.zig");
const algorithm = @import("algorithm.zig");
const CryptoKey = @import("../CryptoKey.zig");
+const Execution = js.Execution;
+
pub fn init(
params: algorithm.Init.HmacKeyGen,
extractable: bool,
key_usages: []const []const u8,
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
- const local = frame.js.local.?;
+ const local = exec.context.local.?;
// Per spec, an unrecognized hash is caught during algorithm normalization
// and surfaces as NotSupportedError.
const digest = crypto.findDigest(switch (params.hash) {
@@ -74,14 +75,14 @@ pub fn init(
};
// Should we reject this in promise too?
- const key = try frame.arena.alloc(u8, block_size);
- errdefer frame.arena.free(key);
+ const key = try exec.arena.alloc(u8, block_size);
+ errdefer exec.arena.free(key);
// HMAC is simply CSPRNG.
const res = crypto.RAND_bytes(key.ptr, key.len);
lp.assert(res == 1, "HMAC.init", .{ .res = res });
- const crypto_key = try frame._factory.create(CryptoKey{
+ const crypto_key = try exec._factory.create(CryptoKey{
._type = .hmac,
._extractable = extractable,
._usages = mask,
@@ -96,16 +97,16 @@ pub fn sign(
algo: algorithm.Sign,
crypto_key: *const CryptoKey,
data: []const u8,
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
- var resolver = frame.js.local.?.createPromiseResolver();
+ var resolver = exec.context.local.?.createPromiseResolver();
if (!algo.isHMAC() or !crypto_key.canSign()) {
resolver.rejectError("HMAC.sign", .{ .dom_exception = .{ .err = error.InvalidAccessError } });
return resolver.promise();
}
- const buffer = try frame.call_arena.alloc(u8, crypto.EVP_MD_size(crypto_key.getDigest()));
+ const buffer = try exec.call_arena.alloc(u8, crypto.EVP_MD_size(crypto_key.getDigest()));
var out_len: u32 = 0;
// Try to sign.
_ = crypto.HMAC(
@@ -117,7 +118,7 @@ pub fn sign(
buffer.ptr,
&out_len,
) orelse {
- frame.call_arena.free(buffer);
+ exec.call_arena.free(buffer);
// Failure.
resolver.rejectError("HMAC.sign", .{ .dom_exception = .{ .err = error.InvalidAccessError } });
return resolver.promise();
@@ -132,9 +133,9 @@ pub fn verify(
crypto_key: *const CryptoKey,
signature: []const u8,
data: []const u8,
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
- var resolver = frame.js.local.?.createPromiseResolver();
+ var resolver = exec.context.local.?.createPromiseResolver();
if (!crypto_key.canVerify()) {
resolver.rejectError("HMAC.verify", .{ .dom_exception = .{ .err = error.InvalidAccessError } });
diff --git a/src/browser/webapi/crypto/X25519.zig b/src/browser/webapi/crypto/X25519.zig
index 968d1833..aaa1798c 100644
--- a/src/browser/webapi/crypto/X25519.zig
+++ b/src/browser/webapi/crypto/X25519.zig
@@ -22,21 +22,22 @@ const std = @import("std");
const lp = @import("lightpanda");
const crypto = @import("../../../sys/libcrypto.zig");
-const Frame = @import("../../Frame.zig");
const js = @import("../../js/js.zig");
const CryptoKey = @import("../CryptoKey.zig");
+const Execution = js.Execution;
+
pub fn init(
extractable: bool,
key_usages: []const []const u8,
- frame: *Frame,
+ exec: *const Execution,
) !js.Promise {
// This code has too many allocations here and there, might be nice to
// gather them together with a single alloc call. Not sure if factory
// pattern is suitable for it though.
- const local = frame.js.local.?;
+ const local = exec.context.local.?;
// Calculate usages; only matters for private key.
// Only deriveKey() and deriveBits() be used for X25519.
@@ -59,11 +60,11 @@ pub fn init(
});
}
- const public_value = try frame.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN);
- errdefer frame.arena.free(public_value);
+ const public_value = try exec.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN);
+ errdefer exec.arena.free(public_value);
- const private_key = try frame.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN);
- errdefer frame.arena.free(private_key);
+ const private_key = try exec.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN);
+ errdefer exec.arena.free(private_key);
// There's no info about whether this can fail; so I assume it cannot.
crypto.X25519_keypair(@ptrCast(public_value), @ptrCast(private_key));
@@ -90,16 +91,16 @@ pub fn init(
private_key.len,
) orelse return error.OutOfMemory;
- const private = try frame._factory.create(CryptoKey{
+ const private = try exec._factory.create(CryptoKey{
._type = .x25519,
._extractable = extractable,
._usages = mask,
._key = private_key,
._vary = .{ .pkey = private_pkey },
});
- errdefer frame._factory.destroy(private);
+ errdefer exec._factory.destroy(private);
- const public = try frame._factory.create(CryptoKey{
+ const public = try exec._factory.create(CryptoKey{
._type = .x25519,
// Public keys are always extractable.
._extractable = true,
@@ -116,7 +117,7 @@ pub fn deriveBits(
private: *const CryptoKey,
public: *const CryptoKey,
length_in_bits: usize,
- frame: *Frame,
+ exec: *const Execution,
) !js.ArrayBuffer {
if (!private.canDeriveBits()) {
return error.InvalidAccessError;
@@ -137,8 +138,8 @@ pub fn deriveBits(
return error.Internal;
}
- const derived_key = try frame.call_arena.alloc(u8, 32);
- errdefer frame.call_arena.free(derived_key);
+ const derived_key = try exec.call_arena.alloc(u8, 32);
+ errdefer exec.call_arena.free(derived_key);
var out_key_len: usize = derived_key.len;
const result = crypto.EVP_PKEY_derive(ctx, derived_key.ptr, &out_key_len);