diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 50ce89dd..e63caf8d 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -899,6 +899,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/canvas/OffscreenCanvas.zig"), @import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"), @import("../webapi/SubtleCrypto.zig"), + @import("../webapi/CryptoKey.zig"), @import("../webapi/Selection.zig"), @import("../webapi/ImageData.zig"), }); diff --git a/src/browser/webapi/CryptoKey.zig b/src/browser/webapi/CryptoKey.zig new file mode 100644 index 00000000..d9b014cb --- /dev/null +++ b/src/browser/webapi/CryptoKey.zig @@ -0,0 +1,107 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const crypto = @import("../../sys/libcrypto.zig"); + +const js = @import("../js/js.zig"); + +/// Represents a cryptographic key obtained from one of the SubtleCrypto methods +/// generateKey(), deriveKey(), importKey(), or unwrapKey(). +const CryptoKey = @This(); + +/// Algorithm being used. +_type: Type, +/// Whether the key is extractable. +_extractable: bool, +/// Bit flags of `usages`; see `Usages` type. +_usages: u8, +/// Raw bytes of key. +_key: []const u8, +/// Different algorithms may use different data structures; +/// this union can be used for such situations. Active field is understood +/// from `_type`. +_vary: extern union { + /// Used by HMAC. + digest: *const crypto.EVP_MD, + /// Used by asymmetric algorithms (X25519, Ed25519). + pkey: *crypto.EVP_PKEY, +}, + +/// https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair +pub const Pair = struct { + privateKey: *CryptoKey, + publicKey: *CryptoKey, +}; + +/// Key-creating functions expect this format. +pub const KeyOrPair = union(enum) { key: *CryptoKey, pair: Pair }; + +pub const Type = enum(u8) { hmac, rsa, x25519 }; + +/// Changing the names of fields would affect bitmask creation. +pub const Usages = struct { + // zig fmt: off + pub const encrypt = 0x001; + pub const decrypt = 0x002; + pub const sign = 0x004; + pub const verify = 0x008; + pub const deriveKey = 0x010; + pub const deriveBits = 0x020; + pub const wrapKey = 0x040; + pub const unwrapKey = 0x080; + // zig fmt: on +}; + +pub inline fn canSign(self: *const CryptoKey) bool { + return self._usages & Usages.sign != 0; +} + +pub inline fn canVerify(self: *const CryptoKey) bool { + return self._usages & Usages.verify != 0; +} + +pub inline fn canDeriveBits(self: *const CryptoKey) bool { + return self._usages & Usages.deriveBits != 0; +} + +pub inline fn canExportKey(self: *const CryptoKey) bool { + return self._extractable; +} + +/// Only valid for HMAC. +pub inline fn getDigest(self: *const CryptoKey) *const crypto.EVP_MD { + return self._vary.digest; +} + +/// Only valid for asymmetric algorithms (X25519, Ed25519). +pub inline fn getKeyObject(self: *const CryptoKey) *crypto.EVP_PKEY { + return self._vary.pkey; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CryptoKey); + + pub const Meta = struct { + pub const name = "CryptoKey"; + + pub var class_id: bridge.ClassId = undefined; + pub const prototype_chain = bridge.prototypeChain(); + }; +}; diff --git a/src/browser/webapi/SubtleCrypto.zig b/src/browser/webapi/SubtleCrypto.zig index 7d6df2e3..6094b72b 100644 --- a/src/browser/webapi/SubtleCrypto.zig +++ b/src/browser/webapi/SubtleCrypto.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -19,16 +19,16 @@ const std = @import("std"); const lp = @import("lightpanda"); const log = @import("../../log.zig"); - -const crypto = @import("../../crypto.zig"); -const DOMException = @import("DOMException.zig"); +const crypto = @import("../../sys/libcrypto.zig"); const Page = @import("../Page.zig"); const js = @import("../js/js.zig"); -pub fn registerTypes() []const type { - return &.{ SubtleCrypto, CryptoKey }; -} +const CryptoKey = @import("CryptoKey.zig"); + +const algorithm = @import("crypto/algorithm.zig"); +const HMAC = @import("crypto/HMAC.zig"); +const X25519 = @import("crypto/X25519.zig"); /// The SubtleCrypto interface of the Web Crypto API provides a number of low-level /// cryptographic functions. @@ -38,69 +38,36 @@ const SubtleCrypto = @This(); /// Don't optimize away the type. _pad: bool = false, -const Algorithm = union(enum) { - /// For RSASSA-PKCS1-v1_5, RSA-PSS, or RSA-OAEP: pass an RsaHashedKeyGenParams object. - rsa_hashed_key_gen: RsaHashedKeyGen, - /// For HMAC: pass an HmacKeyGenParams object. - hmac_key_gen: HmacKeyGen, - /// Can be Ed25519 or X25519. - name: []const u8, - /// Can be Ed25519 or X25519. - object: struct { name: []const u8 }, - - /// https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams - const RsaHashedKeyGen = struct { - name: []const u8, - /// This should be at least 2048. - /// Some organizations are now recommending that it should be 4096. - modulusLength: u32, - publicExponent: js.TypedArray(u8), - hash: union(enum) { - string: []const u8, - object: struct { name: []const u8 }, - }, - }; - - /// https://developer.mozilla.org/en-US/docs/Web/API/HmacKeyGenParams - const HmacKeyGen = struct { - /// Always HMAC. - name: []const u8, - /// Its also possible to pass this in an object. - hash: union(enum) { - string: []const u8, - object: struct { name: []const u8 }, - }, - /// If omitted, default is the block size of the chosen hash function. - length: ?usize, - }; - /// Alias. - const HmacImport = HmacKeyGen; - - const EcdhKeyDeriveParams = struct { - /// Can be Ed25519 or X25519. - name: []const u8, - public: *const CryptoKey, - }; - - /// Algorithm for deriveBits() and deriveKey(). - const DeriveBits = union(enum) { - ecdh_or_x25519: EcdhKeyDeriveParams, - }; -}; - /// Generate a new key (for symmetric algorithms) or key pair (for public-key algorithms). pub fn generateKey( _: *const SubtleCrypto, - algorithm: Algorithm, + algo: algorithm.Init, extractable: bool, key_usages: []const []const u8, page: *Page, ) !js.Promise { - const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch { - return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } }); - }; + switch (algo) { + .hmac_key_gen => |params| return HMAC.init(params, extractable, key_usages, page), + .name => |name| { + if (std.mem.eql(u8, "X25519", name)) { + return X25519.init(extractable, key_usages, page); + } - return page.js.local.?.resolvePromise(key_or_pair); + log.warn(.not_implemented, "generateKey", .{ .name = name }); + }, + .object => |object| { + // Ditto. + const name = object.name; + if (std.mem.eql(u8, "X25519", name)) { + return X25519.init(extractable, key_usages, page); + } + + log.warn(.not_implemented, "generateKey", .{ .name = name }); + }, + else => log.warn(.not_implemented, "generateKey", .{}), + } + + return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } }); } /// Exports a key: that is, it takes as input a CryptoKey object and gives you @@ -133,16 +100,23 @@ pub fn exportKey( /// Derive a secret key from a master key. pub fn deriveBits( _: *const SubtleCrypto, - algorithm: Algorithm.DeriveBits, + algo: algorithm.Derive, base_key: *const CryptoKey, // Private key. length: usize, page: *Page, ) !js.Promise { - return switch (algorithm) { - .ecdh_or_x25519 => |p| { - const name = p.name; + return switch (algo) { + .ecdh_or_x25519 => |params| { + const name = params.name; if (std.mem.eql(u8, name, "X25519")) { - return page.js.local.?.resolvePromise(base_key.deriveBitsX25519(p.public, length, page)); + const result = X25519.deriveBits(base_key, params.public, length, page) catch |err| switch (err) { + error.InvalidAccessError => return page.js.local.?.rejectPromise(.{ + .dom_exception = .{ .err = error.InvalidAccessError }, + }), + else => return err, + }; + + return page.js.local.?.resolvePromise(result); } if (std.mem.eql(u8, name, "ECDH")) { @@ -154,48 +128,18 @@ pub fn deriveBits( }; } -const SignatureAlgorithm = union(enum) { - string: []const u8, - object: struct { name: []const u8 }, - - pub fn isHMAC(self: SignatureAlgorithm) bool { - const name = switch (self) { - .string => |string| string, - .object => |object| object.name, - }; - - if (name.len < 4) return false; - const hmac: u32 = @bitCast([4]u8{ 'H', 'M', 'A', 'C' }); - return @as(u32, @bitCast(name[0..4].*)) == hmac; - } -}; - /// Generate a digital signature. pub fn sign( _: *const SubtleCrypto, - /// This can either be provided as string or object. - /// We can't use the `Algorithm` type defined before though since there - /// are couple of changes between the two. /// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#algorithm - algorithm: SignatureAlgorithm, + algo: algorithm.Sign, key: *CryptoKey, data: []const u8, // ArrayBuffer. page: *Page, ) !js.Promise { return switch (key._type) { - .hmac => { - // Verify algorithm. - if (!algorithm.isHMAC()) { - return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); - } - - // Call sign for HMAC. - const result = key.signHMAC(data, page) catch { - return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); - }; - - return page.js.local.?.resolvePromise(result); - }, + // Call sign for HMAC. + .hmac => return HMAC.sign(algo, key, data, page), else => { log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type }); return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); @@ -206,452 +150,43 @@ pub fn sign( /// Verify a digital signature. pub fn verify( _: *const SubtleCrypto, - algorithm: SignatureAlgorithm, + algo: algorithm.Sign, key: *const CryptoKey, signature: []const u8, // ArrayBuffer. data: []const u8, // ArrayBuffer. page: *Page, ) !js.Promise { - if (!algorithm.isHMAC()) { + if (!algo.isHMAC()) { return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }); } return switch (key._type) { - .hmac => key.verifyHMAC(signature, data, page), + .hmac => HMAC.verify(key, signature, data, page), else => page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }), }; } -pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise { +/// Generates a digest of the given data, using the specified hash function. +pub fn digest(_: *const SubtleCrypto, algo: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise { const local = page.js.local.?; - if (algorithm.len > 10) { + + if (algo.len > 10) { return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }); } - const normalized = std.ascii.lowerString(&page.buf, algorithm); - if (std.mem.eql(u8, normalized, "sha-1")) { - const Sha1 = std.crypto.hash.Sha1; - Sha1.hash(data.values, page.buf[0..Sha1.digest_length], .{}); - return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha1.digest_length] }); - } - if (std.mem.eql(u8, normalized, "sha-256")) { - const Sha256 = std.crypto.hash.sha2.Sha256; - Sha256.hash(data.values, page.buf[0..Sha256.digest_length], .{}); - return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha256.digest_length] }); - } - if (std.mem.eql(u8, normalized, "sha-384")) { - const Sha384 = std.crypto.hash.sha2.Sha384; - Sha384.hash(data.values, page.buf[0..Sha384.digest_length], .{}); - return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha384.digest_length] }); - } - if (std.mem.eql(u8, normalized, "sha-512")) { - const Sha512 = std.crypto.hash.sha2.Sha512; - Sha512.hash(data.values, page.buf[0..Sha512.digest_length], .{}); - return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha512.digest_length] }); - } - return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }); -} -/// Returns the desired digest by its name. -fn findDigest(name: []const u8) error{Invalid}!*const crypto.EVP_MD { - if (std.mem.eql(u8, "SHA-256", name)) { - return crypto.EVP_sha256(); - } - - if (std.mem.eql(u8, "SHA-384", name)) { - return crypto.EVP_sha384(); - } - - if (std.mem.eql(u8, "SHA-512", name)) { - return crypto.EVP_sha512(); - } - - if (std.mem.eql(u8, "SHA-1", name)) { - return crypto.EVP_sha1(); - } - - return error.Invalid; -} - -const KeyOrPair = union(enum) { key: *CryptoKey, pair: CryptoKeyPair }; - -/// https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair -const CryptoKeyPair = struct { - privateKey: *CryptoKey, - publicKey: *CryptoKey, -}; - -/// Represents a cryptographic key obtained from one of the SubtleCrypto methods -/// generateKey(), deriveKey(), importKey(), or unwrapKey(). -pub const CryptoKey = struct { - /// Algorithm being used. - _type: Type, - /// Whether the key is extractable. - _extractable: bool, - /// Bit flags of `usages`; see `Usages` type. - _usages: u8, - /// Raw bytes of key. - _key: []const u8, - /// Different algorithms may use different data structures; - /// this union can be used for such situations. Active field is understood - /// from `_type`. - _vary: extern union { - /// Used by HMAC. - digest: *const crypto.EVP_MD, - /// Used by asymmetric algorithms (X25519, Ed25519). - pkey: *crypto.EVP_PKEY, - }, - - pub const Type = enum(u8) { hmac, rsa, x25519 }; - - /// Changing the names of fields would affect bitmask creation. - pub const Usages = struct { - // zig fmt: off - pub const encrypt = 0x001; - pub const decrypt = 0x002; - pub const sign = 0x004; - pub const verify = 0x008; - pub const deriveKey = 0x010; - pub const deriveBits = 0x020; - pub const wrapKey = 0x040; - pub const unwrapKey = 0x080; - // zig fmt: on + const normalized = std.ascii.upperString(&page.buf, algo); + const digest_type = crypto.findDigest(normalized) catch { + return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }); }; - pub fn init( - algorithm: Algorithm, - extractable: bool, - key_usages: []const []const u8, - page: *Page, - ) !KeyOrPair { - return switch (algorithm) { - .hmac_key_gen => |hmac| initHMAC(hmac, extractable, key_usages, page), - .name => |name| { - if (std.mem.eql(u8, "X25519", name)) { - return initX25519(extractable, key_usages, page); - } - log.warn(.not_implemented, "CryptoKey.init", .{ .name = name }); - return error.NotSupported; - }, - .object => |object| { - // Ditto. - const name = object.name; - if (std.mem.eql(u8, "X25519", name)) { - return initX25519(extractable, key_usages, page); - } - log.warn(.not_implemented, "CryptoKey.init", .{ .name = name }); - return error.NotSupported; - }, - else => { - log.warn(.not_implemented, "CryptoKey.init", .{ .algorithm = algorithm }); - return error.NotSupported; - }, - }; - } + const bytes = data.values; + const out = page.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 }); - inline fn canSign(self: *const CryptoKey) bool { - return self._usages & Usages.sign != 0; - } - - inline fn canVerify(self: *const CryptoKey) bool { - return self._usages & Usages.verify != 0; - } - - inline fn canDeriveBits(self: *const CryptoKey) bool { - return self._usages & Usages.deriveBits != 0; - } - - inline fn canExportKey(self: *const CryptoKey) bool { - return self._extractable; - } - - /// Only valid for HMAC. - inline fn getDigest(self: *const CryptoKey) *const crypto.EVP_MD { - return self._vary.digest; - } - - /// Only valid for asymmetric algorithms (X25519, Ed25519). - inline fn getKeyObject(self: *const CryptoKey) *crypto.EVP_PKEY { - return self._vary.pkey; - } - - // HMAC. - - fn initHMAC( - algorithm: Algorithm.HmacKeyGen, - extractable: bool, - key_usages: []const []const u8, - page: *Page, - ) !KeyOrPair { - const hash = switch (algorithm.hash) { - .string => |str| str, - .object => |obj| obj.name, - }; - // Find digest. - const d = try findDigest(hash); - - // We need at least a single usage. - if (key_usages.len == 0) { - return error.SyntaxError; - } - // Calculate usages mask. - const decls = @typeInfo(Usages).@"struct".decls; - var usages_mask: u8 = 0; - iter_usages: for (key_usages) |usage| { - inline for (decls) |decl| { - if (std.mem.eql(u8, decl.name, usage)) { - usages_mask |= @field(Usages, decl.name); - continue :iter_usages; - } - } - // Unknown usage if got here. - return error.SyntaxError; - } - - const block_size: usize = blk: { - // Caller provides this in bits, not bytes. - if (algorithm.length) |length| { - break :blk length / 8; - } - // Prefer block size of the hash function instead. - break :blk crypto.EVP_MD_block_size(d); - }; - - const key = try page.arena.alloc(u8, block_size); - errdefer page.arena.free(key); - - // HMAC is simply CSPRNG. - const res = crypto.RAND_bytes(key.ptr, key.len); - lp.assert(res == 1, "SubtleCrypto.initHMAC", .{ .res = res }); - - const crypto_key = try page._factory.create(CryptoKey{ - ._type = .hmac, - ._extractable = extractable, - ._usages = usages_mask, - ._key = key, - ._vary = .{ .digest = d }, - }); - - return .{ .key = crypto_key }; - } - - fn signHMAC(self: *const CryptoKey, data: []const u8, page: *Page) !js.ArrayBuffer { - if (!self.canSign()) { - return error.InvalidAccessError; - } - - const buffer = try page.call_arena.alloc(u8, crypto.EVP_MD_size(self.getDigest())); - errdefer page.call_arena.free(buffer); - var out_len: u32 = 0; - // Try to sign. - const signed = crypto.HMAC( - self.getDigest(), - @ptrCast(self._key.ptr), - self._key.len, - data.ptr, - data.len, - buffer.ptr, - &out_len, - ); - - if (signed != null) { - return js.ArrayBuffer{ .values = buffer[0..out_len] }; - } - - // Not DOM exception, failed on our side. - return error.Invalid; - } - - fn verifyHMAC( - self: *const CryptoKey, - signature: []const u8, - data: []const u8, - page: *Page, - ) !js.Promise { - if (!self.canVerify()) { - return error.InvalidAccessError; - } - - var buffer: [crypto.EVP_MAX_MD_BLOCK_SIZE]u8 = undefined; - var out_len: u32 = 0; - // Try to sign. - const signed = crypto.HMAC( - self.getDigest(), - @ptrCast(self._key.ptr), - self._key.len, - data.ptr, - data.len, - &buffer, - &out_len, - ); - - if (signed != null) { - // CRYPTO_memcmp compare in constant time so prohibits time-based attacks. - const res = crypto.CRYPTO_memcmp(signed, @ptrCast(signature.ptr), signature.len); - return page.js.local.?.resolvePromise(res == 0); - } - - return page.js.local.?.resolvePromise(false); - } - - // X25519. - - /// Create a pair of X25519. - fn initX25519( - extractable: bool, - key_usages: []const []const u8, - page: *Page, - ) !KeyOrPair { - // 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. - - // Calculate usages; only matters for private key. - // Only deriveKey() and deriveBits() be used for X25519. - if (key_usages.len == 0) { - return error.SyntaxError; - } - var mask: u8 = 0; - iter_usages: for (key_usages) |usage| { - inline for ([_][]const u8{ "deriveKey", "deriveBits" }) |name| { - if (std.mem.eql(u8, name, usage)) { - mask |= @field(Usages, name); - continue :iter_usages; - } - } - // Unknown usage if got here. - return error.SyntaxError; - } - - const public_value = try page.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN); - errdefer page.arena.free(public_value); - - const private_key = try page.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN); - errdefer page.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)); - - // Create EVP_PKEY for public key. - // Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome - // prefer not to, yet BoringSSL added it and recommends instead of what - // we're doing currently. - const public_pkey = crypto.EVP_PKEY_new_raw_public_key( - crypto.EVP_PKEY_X25519, - null, - public_value.ptr, - public_value.len, - ); - if (public_pkey == null) { - return error.OutOfMemory; - } - - // Create EVP_PKEY for private key. - // Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome - // prefer not to, yet BoringSSL added it and recommends instead of what - // we're doing currently. - const private_pkey = crypto.EVP_PKEY_new_raw_private_key( - crypto.EVP_PKEY_X25519, - null, - private_key.ptr, - private_key.len, - ); - if (private_pkey == null) { - return error.OutOfMemory; - } - - const private = try page._factory.create(CryptoKey{ - ._type = .x25519, - ._extractable = extractable, - ._usages = mask, - ._key = private_key, - ._vary = .{ .pkey = private_pkey.? }, - }); - errdefer page._factory.destroy(private); - - const public = try page._factory.create(CryptoKey{ - ._type = .x25519, - // Public keys are always extractable. - ._extractable = true, - // Always empty for public key. - ._usages = 0, - ._key = public_value, - ._vary = .{ .pkey = public_pkey.? }, - }); - errdefer page._factory.destroy(public); - - return .{ .pair = .{ .privateKey = private, .publicKey = public } }; - } - - fn deriveBitsX25519( - private: *const CryptoKey, - public: *const CryptoKey, - length_in_bits: usize, - page: *Page, - ) !js.ArrayBuffer { - if (!private.canDeriveBits()) { - return error.InvalidAccessError; - } - - const maybe_ctx = crypto.EVP_PKEY_CTX_new(private.getKeyObject(), null); - if (maybe_ctx) |ctx| { - // Context is valid, free it on failure. - errdefer crypto.EVP_PKEY_CTX_free(ctx); - - // Init derive operation and set public key as peer. - if (crypto.EVP_PKEY_derive_init(ctx) != 1 or - crypto.EVP_PKEY_derive_set_peer(ctx, public.getKeyObject()) != 1) - { - // Failed on our end. - return error.Internal; - } - - const derived_key = try page.call_arena.alloc(u8, 32); - errdefer page.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); - if (result != 1) { - // Failed on our end. - return error.Internal; - } - // Sanity check. - lp.assert(derived_key.len == out_key_len, "SubtleCrypto.deriveBitsX25519", .{}); - - // Length is in bits, convert to byte length. - const length = (length_in_bits / 8) + (7 + (length_in_bits % 8)) / 8; - // Truncate the slice to specified length. - // Same as `derived_key`. - const tailored = blk: { - if (length > derived_key.len) { - return error.LengthTooLong; - } - break :blk derived_key[0..length]; - }; - - // Zero any "unused bits" in the final byte. - const remainder_bits: u3 = @intCast(length_in_bits % 8); - if (remainder_bits != 0) { - tailored[tailored.len - 1] &= ~(@as(u8, 0xFF) >> remainder_bits); - } - - return js.ArrayBuffer{ .values = tailored }; - } - - // Failed on our end. - return error.Internal; - } - - pub const JsApi = struct { - pub const bridge = js.Bridge(CryptoKey); - - pub const Meta = struct { - pub const name = "CryptoKey"; - - pub var class_id: bridge.ClassId = undefined; - pub const prototype_chain = bridge.prototypeChain(); - }; - }; -}; + return local.resolvePromise(js.ArrayBuffer{ .values = out[0..out_size] }); +} pub const JsApi = struct { pub const bridge = js.Bridge(SubtleCrypto); diff --git a/src/browser/webapi/crypto/HMAC.zig b/src/browser/webapi/crypto/HMAC.zig new file mode 100644 index 00000000..3fc6b37e --- /dev/null +++ b/src/browser/webapi/crypto/HMAC.zig @@ -0,0 +1,165 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Interprets `CryptoKey` for HMAC. + +const std = @import("std"); +const lp = @import("lightpanda"); +const log = @import("../../../log.zig"); +const crypto = @import("../../../sys/libcrypto.zig"); + +const Page = @import("../../Page.zig"); +const js = @import("../../js/js.zig"); +const algorithm = @import("algorithm.zig"); + +const CryptoKey = @import("../CryptoKey.zig"); + +pub fn init( + params: algorithm.Init.HmacKeyGen, + extractable: bool, + key_usages: []const []const u8, + page: *Page, +) !js.Promise { + const local = page.js.local.?; + // Find digest. + const digest = crypto.findDigest(switch (params.hash) { + .string => |str| str, + .object => |obj| obj.name, + }) catch return local.rejectPromise(.{ + .dom_exception = .{ .err = error.SyntaxError }, + }); + + // Calculate usages mask. + if (key_usages.len == 0) { + return local.rejectPromise(.{ + .dom_exception = .{ .err = error.SyntaxError }, + }); + } + const decls = @typeInfo(CryptoKey.Usages).@"struct".decls; + var mask: u8 = 0; + iter_usages: for (key_usages) |usage| { + inline for (decls) |decl| { + if (std.mem.eql(u8, decl.name, usage)) { + mask |= @field(CryptoKey.Usages, decl.name); + continue :iter_usages; + } + } + // Unknown usage if got here. + return local.rejectPromise(.{ + .dom_exception = .{ .err = error.SyntaxError }, + }); + } + + const block_size: usize = blk: { + // Caller provides this in bits, not bytes. + if (params.length) |length| { + break :blk length >> 3; + } + // Prefer block size of the hash function instead. + break :blk crypto.EVP_MD_block_size(digest); + }; + + // Should we reject this in promise too? + const key = try page.arena.alloc(u8, block_size); + errdefer page.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 page._factory.create(CryptoKey{ + ._type = .hmac, + ._extractable = extractable, + ._usages = mask, + ._key = key, + ._vary = .{ .digest = digest }, + }); + + return local.resolvePromise(crypto_key); +} + +pub fn sign( + algo: algorithm.Sign, + crypto_key: *const CryptoKey, + data: []const u8, + page: *Page, +) !js.Promise { + var resolver = page.js.local.?.createPromiseResolver(); + + if (!algo.isHMAC() or !crypto_key.canSign()) { + resolver.rejectError("HMAC.sign", .{ .dom_exception = .{ .err = error.InvalidAccessError } }); + return resolver.promise(); + } + + const buffer = try page.call_arena.alloc(u8, crypto.EVP_MD_size(crypto_key.getDigest())); + var out_len: u32 = 0; + // Try to sign. + _ = crypto.HMAC( + crypto_key.getDigest(), + @ptrCast(crypto_key._key.ptr), + crypto_key._key.len, + data.ptr, + data.len, + buffer.ptr, + &out_len, + ) orelse { + page.call_arena.free(buffer); + // Failure. + resolver.rejectError("HMAC.sign", .{ .dom_exception = .{ .err = error.InvalidAccessError } }); + return resolver.promise(); + }; + + // Success. + resolver.resolve("HMAC.sign", js.ArrayBuffer{ .values = buffer[0..out_len] }); + return resolver.promise(); +} + +pub fn verify( + crypto_key: *const CryptoKey, + signature: []const u8, + data: []const u8, + page: *Page, +) !js.Promise { + var resolver = page.js.local.?.createPromiseResolver(); + + if (!crypto_key.canVerify()) { + resolver.rejectError("HMAC.verify", .{ .dom_exception = .{ .err = error.InvalidAccessError } }); + return resolver.promise(); + } + + var buffer: [crypto.EVP_MAX_MD_BLOCK_SIZE]u8 = undefined; + var out_len: u32 = 0; + // Try to sign. + const signed = crypto.HMAC( + crypto_key.getDigest(), + @ptrCast(crypto_key._key.ptr), + crypto_key._key.len, + data.ptr, + data.len, + &buffer, + &out_len, + ) orelse { + resolver.resolve("HMAC.verify", false); + return resolver.promise(); + }; + + // CRYPTO_memcmp compare in constant time so prohibits time-based attacks. + const res = crypto.CRYPTO_memcmp(signed, @ptrCast(signature.ptr), signature.len); + resolver.resolve("HMAC.verify", res == 0); + return resolver.promise(); +} diff --git a/src/browser/webapi/crypto/X25519.zig b/src/browser/webapi/crypto/X25519.zig new file mode 100644 index 00000000..a509a22d --- /dev/null +++ b/src/browser/webapi/crypto/X25519.zig @@ -0,0 +1,172 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Interprets `CryptoKey` for X25519. + +const std = @import("std"); +const lp = @import("lightpanda"); +const log = @import("../../../log.zig"); +const crypto = @import("../../../sys/libcrypto.zig"); + +const Page = @import("../../Page.zig"); +const js = @import("../../js/js.zig"); +const Algorithm = @import("algorithm.zig").Algorithm; + +const CryptoKey = @import("../CryptoKey.zig"); + +pub fn init( + extractable: bool, + key_usages: []const []const u8, + page: *Page, +) !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 = page.js.local.?; + + // Calculate usages; only matters for private key. + // Only deriveKey() and deriveBits() be used for X25519. + if (key_usages.len == 0) { + return local.rejectPromise(.{ + .dom_exception = .{ .err = error.SyntaxError }, + }); + } + var mask: u8 = 0; + iter_usages: for (key_usages) |usage| { + inline for ([_][]const u8{ "deriveKey", "deriveBits" }) |name| { + if (std.mem.eql(u8, name, usage)) { + mask |= @field(CryptoKey.Usages, name); + continue :iter_usages; + } + } + // Unknown usage if got here. + return local.rejectPromise(.{ + .dom_exception = .{ .err = error.SyntaxError }, + }); + } + + const public_value = try page.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN); + errdefer page.arena.free(public_value); + + const private_key = try page.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN); + errdefer page.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)); + + // Create EVP_PKEY for public key. + // Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome + // prefer not to, yet BoringSSL added it and recommends instead of what + // we're doing currently. + const public_pkey = crypto.EVP_PKEY_new_raw_public_key( + crypto.EVP_PKEY_X25519, + null, + public_value.ptr, + public_value.len, + ) orelse return error.OutOfMemory; + + // Create EVP_PKEY for private key. + // Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome + // prefer not to, yet BoringSSL added it and recommends instead of what + // we're doing currently. + const private_pkey = crypto.EVP_PKEY_new_raw_private_key( + crypto.EVP_PKEY_X25519, + null, + private_key.ptr, + private_key.len, + ) orelse return error.OutOfMemory; + + const private = try page._factory.create(CryptoKey{ + ._type = .x25519, + ._extractable = extractable, + ._usages = mask, + ._key = private_key, + ._vary = .{ .pkey = private_pkey }, + }); + errdefer page._factory.destroy(private); + + const public = try page._factory.create(CryptoKey{ + ._type = .x25519, + // Public keys are always extractable. + ._extractable = true, + // Always empty for public key. + ._usages = 0, + ._key = public_value, + ._vary = .{ .pkey = public_pkey }, + }); + + return local.resolvePromise(CryptoKey.Pair{ .privateKey = private, .publicKey = public }); +} + +pub fn deriveBits( + private: *const CryptoKey, + public: *const CryptoKey, + length_in_bits: usize, + page: *Page, +) !js.ArrayBuffer { + if (!private.canDeriveBits()) { + return error.InvalidAccessError; + } + + const ctx = crypto.EVP_PKEY_CTX_new(private.getKeyObject(), null) orelse { + // Failed on our end. + return error.Internal; + }; + // Context is valid, free it on failure. + errdefer crypto.EVP_PKEY_CTX_free(ctx); + + // Init derive operation and set public key as peer. + if (crypto.EVP_PKEY_derive_init(ctx) != 1 or + crypto.EVP_PKEY_derive_set_peer(ctx, public.getKeyObject()) != 1) + { + // Failed on our end. + return error.Internal; + } + + const derived_key = try page.call_arena.alloc(u8, 32); + errdefer page.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); + if (result != 1) { + // Failed on our end. + return error.Internal; + } + // Sanity check. + lp.assert(derived_key.len == out_key_len, "X25519.deriveBits", .{}); + + // Length is in bits, convert to byte length. + const length = (length_in_bits / 8) + (7 + (length_in_bits % 8)) / 8; + // Truncate the slice to specified length. + // Same as `derived_key`. + const tailored = blk: { + if (length > derived_key.len) { + return error.LengthTooLong; + } + break :blk derived_key[0..length]; + }; + + // Zero any "unused bits" in the final byte. + const remainder_bits: u3 = @intCast(length_in_bits % 8); + if (remainder_bits != 0) { + tailored[tailored.len - 1] &= ~(@as(u8, 0xFF) >> remainder_bits); + } + + return js.ArrayBuffer{ .values = tailored }; +} diff --git a/src/browser/webapi/crypto/algorithm.zig b/src/browser/webapi/crypto/algorithm.zig new file mode 100644 index 00000000..c0a7caa5 --- /dev/null +++ b/src/browser/webapi/crypto/algorithm.zig @@ -0,0 +1,91 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! This file provides various arguments needed for crypto APIs. + +const js = @import("../../js/js.zig"); + +const CryptoKey = @import("../CryptoKey.zig"); + +/// Passed for `generateKey()`. +pub const Init = union(enum) { + /// For RSASSA-PKCS1-v1_5, RSA-PSS, or RSA-OAEP: pass an RsaHashedKeyGenParams object. + rsa_hashed_key_gen: RsaHashedKeyGen, + /// For HMAC: pass an HmacKeyGenParams object. + hmac_key_gen: HmacKeyGen, + /// Can be Ed25519 or X25519. + name: []const u8, + /// Can be Ed25519 or X25519. + object: struct { name: []const u8 }, + + /// https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams + pub const RsaHashedKeyGen = struct { + name: []const u8, + /// This should be at least 2048. + /// Some organizations are now recommending that it should be 4096. + modulusLength: u32, + publicExponent: js.TypedArray(u8), + hash: union(enum) { + string: []const u8, + object: struct { name: []const u8 }, + }, + }; + + /// https://developer.mozilla.org/en-US/docs/Web/API/HmacKeyGenParams + pub const HmacKeyGen = struct { + /// Always HMAC. + name: []const u8, + /// Its also possible to pass this in an object. + hash: union(enum) { + string: []const u8, + object: struct { name: []const u8 }, + }, + /// If omitted, default is the block size of the chosen hash function. + length: ?usize, + }; + /// Alias. + pub const HmacImport = HmacKeyGen; + + pub const EcdhKeyDeriveParams = struct { + /// Can be Ed25519 or X25519. + name: []const u8, + public: *const CryptoKey, + }; +}; + +/// Algorithm for deriveBits() and deriveKey(). +pub const Derive = union(enum) { + ecdh_or_x25519: Init.EcdhKeyDeriveParams, +}; + +/// For `sign()` functionality. +pub const Sign = union(enum) { + string: []const u8, + object: struct { name: []const u8 }, + + pub fn isHMAC(self: Sign) bool { + const name = switch (self) { + .string => |string| string, + .object => |object| object.name, + }; + + if (name.len < 4) return false; + const hmac: u32 = @bitCast([4]u8{ 'H', 'M', 'A', 'C' }); + return @as(u32, @bitCast(name[0..4].*)) == hmac; + } +}; diff --git a/src/network/WebBotAuth.zig b/src/network/WebBotAuth.zig index 418ee3cb..d0f3e1f1 100644 --- a/src/network/WebBotAuth.zig +++ b/src/network/WebBotAuth.zig @@ -17,7 +17,7 @@ // along with this program. If not, see . const std = @import("std"); -const crypto = @import("../crypto.zig"); +const crypto = @import("../sys/libcrypto.zig"); const Http = @import("../network/http.zig"); diff --git a/src/sys/libcrypto.zig b/src/sys/libcrypto.zig index 6b5657f6..f1833268 100644 --- a/src/sys/libcrypto.zig +++ b/src/sys/libcrypto.zig @@ -221,6 +221,7 @@ pub extern fn EVP_sha256() *const EVP_MD; pub extern fn EVP_sha384() *const EVP_MD; pub extern fn EVP_sha512() *const EVP_MD; +pub const EVP_MAX_MD_SIZE = 64; pub const EVP_MAX_MD_BLOCK_SIZE = 128; pub extern fn EVP_MD_size(md: ?*const EVP_MD) usize; @@ -260,7 +261,29 @@ pub extern fn EVP_PKEY_free(pkey: ?*EVP_PKEY) void; pub extern fn EVP_DigestSignInit(ctx: ?*EVP_MD_CTX, pctx: ?*?*EVP_PKEY_CTX, typ: ?*const EVP_MD, e: ?*ENGINE, pkey: ?*EVP_PKEY) c_int; pub extern fn EVP_DigestSign(ctx: ?*EVP_MD_CTX, sig: [*c]u8, sig_len: *usize, data: [*c]const u8, data_len: usize) c_int; +pub extern fn EVP_Digest(data: ?*const anyopaque, len: usize, md_out: [*c]u8, md_out_size: [*c]c_uint, @"type": ?*const EVP_MD, impl: ?*ENGINE) c_int; pub extern fn EVP_MD_CTX_new() ?*EVP_MD_CTX; pub extern fn EVP_MD_CTX_free(ctx: ?*EVP_MD_CTX) void; pub const struct_evp_md_ctx_st = opaque {}; pub const EVP_MD_CTX = struct_evp_md_ctx_st; + +/// Returns the desired digest by its name. +pub fn findDigest(name: []const u8) error{Invalid}!*const EVP_MD { + if (std.mem.eql(u8, "SHA-256", name)) { + return EVP_sha256(); + } + + if (std.mem.eql(u8, "SHA-384", name)) { + return EVP_sha384(); + } + + if (std.mem.eql(u8, "SHA-512", name)) { + return EVP_sha512(); + } + + if (std.mem.eql(u8, "SHA-1", name)) { + return EVP_sha1(); + } + + return error.Invalid; +}