Merge pull request #1983 from lightpanda-io/nikneym/crypto-changes

Small `SubtleCrypto` refactor
This commit is contained in:
Halil Durak
2026-03-25 11:23:13 +03:00
committed by GitHub
8 changed files with 639 additions and 527 deletions

View File

@@ -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"),
});

View File

@@ -0,0 +1,107 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
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();
};
};

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -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);
},
.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,453 +150,44 @@ 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] });
}
const normalized = std.ascii.upperString(&page.buf, algo);
const digest_type = crypto.findDigest(normalized) catch {
return local.rejectPromise(.{ .dom_exception = .{ .err = 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 });
return local.resolvePromise(js.ArrayBuffer{ .values = out[0..out_size] });
}
/// 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
};
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;
},
};
}
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();
};
};
};
pub const JsApi = struct {
pub const bridge = js.Bridge(SubtleCrypto);

View File

@@ -0,0 +1,165 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
//! 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();
}

View File

@@ -0,0 +1,172 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
//! 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 };
}

View File

@@ -0,0 +1,91 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
//! 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;
}
};

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const crypto = @import("../crypto.zig");
const crypto = @import("../sys/libcrypto.zig");
const Http = @import("../network/http.zig");

View File

@@ -1,3 +1,21 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
//! libcrypto utilities we use throughout browser.
const std = @import("std");
@@ -203,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;
@@ -242,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;
}