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);