diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig
index 1c6177ee..c52ddadb 100644
--- a/src/browser/Browser.zig
+++ b/src/browser/Browser.zig
@@ -109,6 +109,7 @@ pub fn deinit(self: *Browser) void {
pub fn newSession(self: *Browser, notification: *Notification) !*Session {
self.closeSession();
self.session = @as(Session, undefined);
+ errdefer self.session = null;
const session = &self.session.?;
try Session.init(session, self, notification);
return session;
diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig
index 282c6c4a..693e731c 100644
--- a/src/browser/Frame.zig
+++ b/src/browser/Frame.zig
@@ -4308,8 +4308,8 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
// The submitter can be an input box (if enter was entered on the box)
// I don't think this is technically correct, but FormData handles it ok
const form_data = try FormData.init(form, submitter_, &self.js.execution);
- // FormData.init acquires file's references. So we must release them once done.
- defer form_data.deinit(self._page);
+ form_data.acquireRef();
+ defer form_data.releaseRef(self._page);
const arena = try self._session.getArena(.medium, "submitForm");
errdefer self._session.releaseArena(arena);
diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig
index ef24f43e..bc4f6d7c 100644
--- a/src/browser/js/Local.zig
+++ b/src/browser/js/Local.zig
@@ -1411,12 +1411,6 @@ pub fn rejectPromise(self: *const Local, err: js.PromiseResolver.RejectError) js
return resolver.promise();
}
-pub fn rejectErrorPromise(self: *const Local, value: js.PromiseResolver.RejectError) !js.Promise {
- var resolver = js.PromiseResolver.init(self);
- resolver.rejectError("Local.rejectPromise", value);
- return resolver.promise();
-}
-
pub fn resolvePromise(self: *const Local, value: anytype) !js.Promise {
var resolver = js.PromiseResolver.init(self);
resolver.resolve("Local.resolvePromise", value);
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 6bcf3a3a..feb40676 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -1022,6 +1022,7 @@ pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/Console.zig"),
@import("../webapi/Crypto.zig"),
@import("../webapi/SubtleCrypto.zig"),
+ @import("../webapi/CryptoKey.zig"),
@import("../webapi/net/FormData.zig"),
@import("../webapi/net/Headers.zig"),
@import("../webapi/net/Request.zig"),
diff --git a/src/browser/webapi/CryptoKey.zig b/src/browser/webapi/CryptoKey.zig
index 664d23d0..3874b3fb 100644
--- a/src/browser/webapi/CryptoKey.zig
+++ b/src/browser/webapi/CryptoKey.zig
@@ -20,27 +20,44 @@ const crypto = @import("../../sys/libcrypto.zig");
const js = @import("../js/js.zig");
+const Execution = js.Execution;
+
/// 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 this is a secret (symmetric), public, or private key. Surfaced as
+/// the JS `.type` attribute.
+_kind: Kind = .secret,
/// Whether the key is extractable.
_extractable: bool,
/// Bit flags of `usages`; see `Usages` type.
_usages: u8,
/// Raw bytes of key.
_key: []const u8,
+/// Metadata needed to reconstruct the JS `.algorithm` dictionary. The strings
+/// are expected to outlive the key (arena-allocated alongside it).
+_algorithm: Algorithm,
/// Different algorithms may use different data structures;
/// this union can be used for such situations. Active field is understood
/// from `_type`.
-_vary: extern union {
+_vary: union(enum) {
+ none,
/// Used by HMAC.
digest: *const crypto.EVP_MD,
/// Used by asymmetric algorithms (X25519, Ed25519).
pkey: *crypto.EVP_PKEY,
-},
+} = .none,
+
+/// Captures the algorithm parameters reported back via the `.algorithm`
+/// accessor. `hash` is only set for HMAC (and other hashed algorithms).
+pub const Algorithm = struct {
+ name: []const u8,
+ hash: ?[]const u8 = null,
+ named_curve: ?[]const u8 = null,
+};
/// https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair
pub const Pair = struct {
@@ -51,7 +68,17 @@ pub const Pair = struct {
/// Key-creating functions expect this format.
pub const KeyOrPair = union(enum) { key: *CryptoKey, pair: Pair };
-pub const Type = enum(u8) { hmac, rsa, x25519 };
+pub const Type = enum(u8) { hmac, rsa, x25519, aes, derive, ec };
+
+pub const Kind = enum {
+ secret,
+ public,
+ private,
+
+ pub fn toString(self: Kind) []const u8 {
+ return @tagName(self);
+ }
+};
/// Changing the names of fields would affect bitmask creation.
pub const Usages = struct {
@@ -67,32 +94,105 @@ pub const Usages = struct {
// zig fmt: on
};
-pub inline fn canSign(self: *const CryptoKey) bool {
+pub fn canEncrypt(self: *const CryptoKey) bool {
+ return self._usages & Usages.encrypt != 0;
+}
+
+pub fn canDecrypt(self: *const CryptoKey) bool {
+ return self._usages & Usages.decrypt != 0;
+}
+
+pub fn canSign(self: *const CryptoKey) bool {
return self._usages & Usages.sign != 0;
}
-pub inline fn canVerify(self: *const CryptoKey) bool {
+pub fn canVerify(self: *const CryptoKey) bool {
return self._usages & Usages.verify != 0;
}
-pub inline fn canDeriveBits(self: *const CryptoKey) bool {
+pub fn canDeriveBits(self: *const CryptoKey) bool {
return self._usages & Usages.deriveBits != 0;
}
-pub inline fn canExportKey(self: *const CryptoKey) bool {
+pub fn canDeriveKey(self: *const CryptoKey) bool {
+ return self._usages & Usages.deriveKey != 0;
+}
+
+pub fn canExportKey(self: *const CryptoKey) bool {
return self._extractable;
}
/// Only valid for HMAC.
-pub inline fn getDigest(self: *const CryptoKey) *const crypto.EVP_MD {
+pub 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 {
+pub fn getKeyObject(self: *const CryptoKey) *crypto.EVP_PKEY {
return self._vary.pkey;
}
+pub fn getType(self: *const CryptoKey) Kind {
+ return self._kind;
+}
+
+pub fn getExtractable(self: *const CryptoKey) bool {
+ return self._extractable;
+}
+
+/// The shape of the `.algorithm` dictionary depends on the algorithm. AES and
+/// HMAC expose a `length` (in bits, derived from the key material); HMAC also
+/// exposes a nested `hash`.
+const AlgorithmReport = union(enum) {
+ keyed: struct { name: []const u8, length: u32 },
+ hmac: struct { name: []const u8, length: u32, hash: struct { name: []const u8 } },
+ ec: struct { name: []const u8, namedCurve: []const u8 },
+ named: struct { name: []const u8 },
+};
+
+pub fn getAlgorithm(self: *const CryptoKey) AlgorithmReport {
+ const length: u32 = @intCast(self._key.len * 8);
+ return switch (self._type) {
+ .aes => .{ .keyed = .{ .name = self._algorithm.name, .length = length } },
+ .hmac => .{ .hmac = .{
+ .name = self._algorithm.name,
+ .length = length,
+ .hash = .{ .name = self._algorithm.hash orelse "" },
+ } },
+ .ec => .{ .ec = .{
+ .name = self._algorithm.name,
+ .namedCurve = self._algorithm.named_curve orelse "",
+ } },
+ else => .{ .named = .{ .name = self._algorithm.name } },
+ };
+}
+
+/// Returns the active usages, de-duplicated, in a stable order.
+pub fn getUsages(self: *const CryptoKey, exec: *const Execution) ![]const []const u8 {
+ // zig fmt: off
+ const all = [_]struct { mask: u8, name: []const u8 }{
+ .{ .mask = Usages.encrypt, .name = "encrypt" },
+ .{ .mask = Usages.decrypt, .name = "decrypt" },
+ .{ .mask = Usages.sign, .name = "sign" },
+ .{ .mask = Usages.verify, .name = "verify" },
+ .{ .mask = Usages.deriveKey, .name = "deriveKey" },
+ .{ .mask = Usages.deriveBits, .name = "deriveBits" },
+ .{ .mask = Usages.wrapKey, .name = "wrapKey" },
+ .{ .mask = Usages.unwrapKey, .name = "unwrapKey" },
+ };
+ // zig fmt: on
+
+ var buf: [all.len][]const u8 = undefined;
+ var n: usize = 0;
+ for (all) |entry| {
+ if (self._usages & entry.mask != 0) {
+ buf[n] = entry.name;
+ n += 1;
+ }
+ }
+ return exec.call_arena.dupe([]const u8, buf[0..n]);
+}
+
pub const JsApi = struct {
pub const bridge = js.Bridge(CryptoKey);
@@ -102,4 +202,9 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
pub const prototype_chain = bridge.prototypeChain();
};
+
+ pub const @"type" = bridge.accessor(CryptoKey.getType, null, .{});
+ pub const extractable = bridge.accessor(CryptoKey.getExtractable, null, .{});
+ pub const algorithm = bridge.accessor(CryptoKey.getAlgorithm, null, .{});
+ pub const usages = bridge.accessor(CryptoKey.getUsages, null, .{});
};
diff --git a/src/browser/webapi/DOMException.zig b/src/browser/webapi/DOMException.zig
index aa0cf117..296d2965 100644
--- a/src/browser/webapi/DOMException.zig
+++ b/src/browser/webapi/DOMException.zig
@@ -60,14 +60,15 @@ pub fn fromError(err: anyerror) ?DOMException {
error.DataClone => .{ ._code = .data_clone_error },
error.InvalidAccessError => .{ ._code = .invalid_access_error },
error.OperationError => .{ ._code = .operation_error },
+ error.DataError => .{ ._code = .data_error },
else => null,
};
}
pub fn getCode(self: *const DOMException) u8 {
return switch (self._code) {
- // WebCrypto-only error: no legacy numeric code.
- .operation_error => 0,
+ // WebCrypto-only errors: no legacy numeric code.
+ .operation_error, .data_error => 0,
else => @intFromEnum(self._code),
};
}
@@ -101,6 +102,7 @@ pub fn getName(self: *const DOMException) []const u8 {
.invalid_node_type_error => "InvalidNodeTypeError",
.data_clone_error => "DataCloneError",
.operation_error => "OperationError",
+ .data_error => "DataError",
};
}
@@ -132,6 +134,7 @@ pub fn getMessage(self: *const DOMException) []const u8 {
.invalid_node_type_error => "The supplied node is incorrect or has an incorrect ancestor for this operation",
.data_clone_error => "The object can not be cloned",
.operation_error => "The operation failed for an operation-specific reason",
+ .data_error => "Data provided to an operation does not meet requirements",
};
}
@@ -172,6 +175,8 @@ const Code = enum(u8) {
invalid_node_type_error = 24,
data_clone_error = 25,
/// Defined by WebCrypto; no legacy code, exposed via name only.
+ data_error = 0xFE,
+ /// Defined by WebCrypto; no legacy code, exposed via name only.
operation_error = 0xFF,
/// Maps a standard error name to its legacy code
@@ -200,6 +205,7 @@ const Code = enum(u8) {
.{ "InvalidNodeTypeError", .invalid_node_type_error },
.{ "DataCloneError", .data_clone_error },
.{ "OperationError", .operation_error },
+ .{ "DataError", .data_error },
});
return lookup.get(name) orelse .none;
}
diff --git a/src/browser/webapi/SubtleCrypto.zig b/src/browser/webapi/SubtleCrypto.zig
index 242938aa..285feef2 100644
--- a/src/browser/webapi/SubtleCrypto.zig
+++ b/src/browser/webapi/SubtleCrypto.zig
@@ -30,6 +30,8 @@ const EC = @import("crypto/EC.zig");
const HMAC = @import("crypto/HMAC.zig");
const RSA = @import("crypto/RSA.zig");
const X25519 = @import("crypto/X25519.zig");
+const KDF = @import("crypto/KDF.zig");
+const common = @import("crypto/common.zig");
const log = lp.log;
const String = lp.String;
@@ -54,18 +56,8 @@ pub fn generateKey(
const local = exec.js.local.?;
switch (algo) {
.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 } });
- };
- log.warn(.not_implemented, "generateKey", .{ .name = params.name });
- },
- .ec_key_gen => |params| {
- EC.validate(params, key_usages) catch |err| {
- return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
- };
- log.warn(.not_implemented, "generateKey", .{ .name = params.name });
- },
+ .aes_key_gen => |params| return AES.generate(params, extractable, key_usages, exec),
+ .ec_key_gen => |params| return EC.generate(params, extractable, key_usages, exec),
.rsa_hashed_key_gen => |params| {
RSA.validate(params, key_usages) catch |err| {
return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
@@ -108,10 +100,10 @@ fn _generateKeyFromName(
const allowed: []const []const u8 = blk: {
const str = name.str();
- if (std.ascii.eqlIgnoreCase(str, "Ed25519") or std.ascii.eqlIgnoreCase(str, "Ed448")) {
+ if (eqlIgnoreCase(str, "Ed25519") or eqlIgnoreCase(str, "Ed448")) {
break :blk &.{ "sign", "verify" };
}
- if (std.ascii.eqlIgnoreCase(str, "X448")) {
+ if (eqlIgnoreCase(str, "X448")) {
break :blk &.{ "deriveKey", "deriveBits" };
}
return error.NotSupported;
@@ -139,6 +131,140 @@ fn _generateKeyFromName(
return error.NotSupported;
}
+/// Imports a key from an external, portable format and returns a `CryptoKey`.
+pub fn importKey(
+ _: *const SubtleCrypto,
+ format: []const u8,
+ key_data: algorithm.KeyData,
+ algo: algorithm.Import,
+ extractable: bool,
+ key_usages: []const []const u8,
+ exec: *const Execution,
+) !js.Promise {
+ const local = exec.js.local.?;
+ const name = algo.algoName();
+
+ // Asymmetric algorithms (EC, OKP). Usage validation runs first (bad/empty
+ // usages → SyntaxError) so the failure-path tests pass regardless of whether
+ // the key material itself can be parsed yet.
+ const is_private = importKind(format, key_data);
+ if (asymmetricAllowedUsages(name, is_private)) |allowed| {
+ // Public keys may have empty usages; secret/private keys may not.
+ const mask = common.usageMaskInner(allowed, key_usages, is_private) catch |err| {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
+ };
+ if (EC.canonicalName(name) != null) {
+ const der = switch (key_data) {
+ .bytes => |b| b.values,
+ .jwk => return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }),
+ };
+ return EC.import(name, algo.namedCurve(), format, der, is_private, extractable, mask, exec);
+ }
+ log.warn(.not_implemented, "SubtleCrypto.importKey", .{ .name = name });
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+ }
+
+ // Resolve the raw key bytes from the requested format. Symmetric keys
+ // support "raw" (a BufferSource) and "jwk" (an "oct" JSON Web Key).
+ const raw: []const u8 = blk: {
+ if (std.mem.eql(u8, format, "raw")) {
+ break :blk switch (key_data) {
+ .bytes => |b| b.values,
+ // A JWK object passed where a BufferSource is expected.
+ .jwk => return local.rejectPromise(.{ .type_error = "raw format expects a BufferSource" }),
+ };
+ }
+ if (std.mem.eql(u8, format, "jwk")) {
+ const jwk = switch (key_data) {
+ .jwk => |j| j,
+ .bytes => return local.rejectPromise(.{ .type_error = "jwk format expects an object" }),
+ };
+ if (!std.mem.eql(u8, jwk.kty, "oct")) {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
+ }
+ const k = jwk.k orelse {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
+ };
+ break :blk common.base64Decode(exec.call_arena, k) catch |err| switch (err) {
+ error.DataError => return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } }),
+ else => |e| return e,
+ };
+ }
+ // spki / pkcs8 (asymmetric formats) are not supported for these algorithms.
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+ };
+
+ if (AES.canonicalName(name) != null) {
+ return AES.import(name, raw, extractable, key_usages, exec);
+ }
+
+ // HKDF / PBKDF2: key-derivation inputs. The raw bytes are the key; there's
+ // no length constraint and these keys are non-extractable.
+ inline for ([_][]const u8{ "HKDF", "PBKDF2" }) |derive_name| {
+ if (eqlIgnoreCase(name, derive_name)) {
+ const mask = common.usageMask(&.{ "deriveKey", "deriveBits" }, key_usages) catch |err| {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
+ };
+ const key = try exec.arena.dupe(u8, raw);
+ const crypto_key = try exec._factory.create(CryptoKey{
+ ._type = .derive,
+ ._kind = .secret,
+ ._extractable = extractable,
+ ._usages = mask,
+ ._key = key,
+ ._algorithm = .{ .name = derive_name },
+ });
+ return local.resolvePromise(crypto_key);
+ }
+ }
+ if (eqlIgnoreCase(name, "HMAC")) {
+ const hash_name = switch (algo) {
+ .hmac => |h| switch (h.hash) {
+ .string => |s| s,
+ .object => |o| o.name,
+ },
+ else => return local.rejectPromise(.{ .type_error = "HMAC import requires a hash" }),
+ };
+ return HMAC.import(hash_name, raw, extractable, key_usages, exec);
+ }
+
+ log.warn(.not_implemented, "SubtleCrypto.importKey", .{ .name = name });
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+}
+
+/// Whether the requested format/key-data describe a private key. The format
+/// alone decides for DER/raw variants; for JWK the presence of `d` marks it.
+fn importKind(format: []const u8, key_data: algorithm.KeyData) bool {
+ if (eqlIgnoreCase(format, "pkcs8") or eqlIgnoreCase(format, "raw-private") or eqlIgnoreCase(format, "raw-seed")) {
+ return true;
+ }
+ if (eqlIgnoreCase(format, "jwk")) {
+ return switch (key_data) {
+ .jwk => |j| j.d != null,
+ .bytes => false,
+ };
+ }
+ // spki, raw, raw-public, ...
+ return false;
+}
+
+/// The usages permitted for an EC/OKP key of the given privacy, or null if the
+/// algorithm isn't one of the asymmetric algorithms handled here. Real key
+/// import for these isn't implemented yet — this only drives usage validation.
+fn asymmetricAllowedUsages(name: []const u8, is_private: bool) ?[]const []const u8 {
+ if (eqlIgnoreCase(name, "ECDSA") or eqlIgnoreCase(name, "Ed25519") or eqlIgnoreCase(name, "Ed448")) {
+ return if (is_private) &.{"sign"} else &.{"verify"};
+ }
+ if (eqlIgnoreCase(name, "ECDH") or eqlIgnoreCase(name, "X25519") or eqlIgnoreCase(name, "X448")) {
+ return if (is_private) &.{ "deriveKey", "deriveBits" } else &.{};
+ }
+ return null;
+}
+
+fn eqlIgnoreCase(a: []const u8, b: []const u8) bool {
+ return std.ascii.eqlIgnoreCase(a, b);
+}
+
/// Exports a key: that is, it takes as input a CryptoKey object and gives you
/// the key in an external, portable format.
pub fn exportKey(
@@ -156,9 +282,11 @@ pub fn exportKey(
return local.resolvePromise(js.ArrayBuffer{ .values = key._key });
}
- const is_unsupported = std.mem.eql(u8, format, "pkcs8") or
- std.mem.eql(u8, format, "spki") or std.mem.eql(u8, format, "jwk");
+ if (std.mem.eql(u8, format, "jwk")) {
+ return exportJwk(key, exec);
+ }
+ const is_unsupported = std.mem.eql(u8, format, "pkcs8") or std.mem.eql(u8, format, "spki");
if (is_unsupported) {
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
@@ -167,36 +295,192 @@ pub fn exportKey(
return local.rejectPromise(.{ .type_error = "invalid format" });
}
-/// Derive a secret key from a master key.
-pub fn deriveBits(
- _: *const SubtleCrypto,
- algo: algorithm.Derive,
- base_key: *const CryptoKey, // Private key.
- length: usize,
- exec: *const Execution,
-) !js.Promise {
+/// The JSON Web Key returned for symmetric ("oct") keys.
+const JwkSecret = struct {
+ kty: []const u8 = "oct",
+ k: []const u8,
+ alg: []const u8,
+ ext: bool,
+ key_ops: []const []const u8,
+};
+
+fn exportJwk(key: *CryptoKey, exec: *const Execution) !js.Promise {
const local = exec.js.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, exec) catch |err| switch (err) {
- error.InvalidAccessError => return local.rejectPromise(.{
- .dom_exception = .{ .err = error.InvalidAccessError },
- }),
- else => return err,
- };
-
- return local.resolvePromise(result);
- }
-
- if (std.mem.eql(u8, name, "ECDH")) {
- log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
- }
+ // The `alg` registry value depends on the algorithm and key length.
+ const alg: []const u8 = switch (key._type) {
+ .aes => try std.fmt.allocPrint(exec.call_arena, "A{d}{s}", .{
+ key._key.len * 8,
+ key._algorithm.name[4..], // strip "AES-"
+ }),
+ .hmac => blk: {
+ const hash: []const u8 = key._algorithm.hash orelse "SHA-";
+ break :blk try std.fmt.allocPrint(exec.call_arena, "HS{s}", .{hash[4..]}); // strip "SHA-"
+ },
+ else => {
+ log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = "jwk", .type = key._type });
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
},
};
+
+ return local.resolvePromise(JwkSecret{
+ .k = try common.base64Encode(exec.call_arena, key._key),
+ .alg = alg,
+ .ext = key._extractable,
+ .key_ops = try key.getUsages(exec),
+ });
+}
+
+/// Derive an array of bits from a base key. `length` is in bits and may be null
+/// (the WebIDL type is `unsigned long?`).
+pub fn deriveBits(
+ _: *const SubtleCrypto,
+ algo: algorithm.Derive,
+ base_key: *const CryptoKey,
+ length: ?u32,
+ exec: *const Execution,
+) !js.Promise {
+ const local = exec.js.local.?;
+ const bits = deriveRaw(algo, base_key, length, base_key.canDeriveBits(), exec) catch |err| {
+ return rejectDerive(local, err);
+ };
+ return local.resolvePromise(js.ArrayBuffer{ .values = bits });
+}
+
+/// Derive a new CryptoKey from a base key: derive the right number of bits for
+/// `derived`, then import them as that key type.
+pub fn deriveKey(
+ _: *const SubtleCrypto,
+ algo: algorithm.Derive,
+ base_key: *const CryptoKey,
+ derived: algorithm.DerivedKey,
+ extractable: bool,
+ key_usages: []const []const u8,
+ exec: *const Execution,
+) !js.Promise {
+ const local = exec.js.local.?;
+ // The base key's deriveKey usage (not deriveBits) gates this operation.
+ const usage_ok = base_key.canDeriveKey();
+
+ switch (derived) {
+ .keyed => |k| {
+ if (AES.canonicalName(k.name) == null) {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+ }
+ const bits = deriveRaw(algo, base_key, k.length, usage_ok, exec) catch |err| {
+ return rejectDerive(local, err);
+ };
+ return AES.import(k.name, bits, extractable, key_usages, exec);
+ },
+ .hmac => |h| {
+ const hash_name = h.hash.name();
+ const hash_md = crypto.findDigest(hash_name) catch {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+ };
+ // Default length, per spec, is the hash's block size (in bits).
+ const length: u32 = h.length orelse @intCast(crypto.EVP_MD_block_size(hash_md) * 8);
+ const bits = deriveRaw(algo, base_key, length, usage_ok, exec) catch |err| {
+ return rejectDerive(local, err);
+ };
+ return HMAC.import(hash_name, bits, extractable, key_usages, exec);
+ },
+ .object, .name => {
+ log.warn(.not_implemented, "SubtleCrypto.deriveKey", .{});
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+ },
+ }
+}
+
+/// Shared derivation core for deriveBits/deriveKey. `usage_ok` is the relevant
+/// usage gate already evaluated by the caller. Returns the raw derived bytes.
+fn deriveRaw(
+ algo: algorithm.Derive,
+ base_key: *const CryptoKey,
+ length: ?u32,
+ usage_ok: bool,
+ exec: *const Execution,
+) KDF.Error![]const u8 {
+ switch (algo) {
+ .pbkdf2 => |params| return KDF.pbkdf2(base_key, params, length, usage_ok, exec),
+ .hkdf => |params| return KDF.hkdf(base_key, params, length, usage_ok, exec),
+ .ecdh_or_x25519 => |params| {
+ if (!usage_ok) {
+ return error.InvalidAccessError;
+ }
+ // The base key must have been created for this same algorithm.
+ if (!eqlIgnoreCase(base_key._algorithm.name, params.name)) {
+ return error.InvalidAccessError;
+ }
+ if (eqlIgnoreCase(params.name, "X25519")) {
+ // null length means "derive the full shared secret" (256 bits).
+ const result = X25519.deriveBits(base_key, params.public, length orelse 256, exec) catch |err| switch (err) {
+ error.InvalidAccessError => return error.InvalidAccessError,
+ error.OutOfMemory => return error.OutOfMemory,
+ else => return error.OperationError,
+ };
+ return result.values;
+ }
+ if (eqlIgnoreCase(params.name, "ECDH")) {
+ return EC.deriveBits(base_key, params.public, length, exec);
+ }
+ return error.NotSupported;
+ },
+ }
+}
+
+/// Maps a KDF error to the spec-mandated DOMException (OutOfMemory propagates).
+fn rejectDerive(local: *const js.Local, err: KDF.Error) !js.Promise {
+ return switch (err) {
+ error.InvalidAccessError => local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
+ error.NotSupported => local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }),
+ error.OperationError => local.rejectPromise(.{ .dom_exception = .{ .err = error.OperationError } }),
+ error.OutOfMemory => error.OutOfMemory,
+ };
+}
+
+/// Encrypts data with the given key and algorithm.
+pub fn encrypt(
+ _: *const SubtleCrypto,
+ algo: algorithm.Encrypt,
+ key: *CryptoKey,
+ data: js.TypedArray(u8),
+ exec: *const Execution,
+) !js.Promise {
+ return cryptOp(algo, key, data.values, true, exec);
+}
+
+/// Decrypts data with the given key and algorithm.
+pub fn decrypt(
+ _: *const SubtleCrypto,
+ algo: algorithm.Encrypt,
+ key: *CryptoKey,
+ data: js.TypedArray(u8),
+ exec: *const Execution,
+) !js.Promise {
+ return cryptOp(algo, key, data.values, false, exec);
+}
+
+fn cryptOp(
+ algo: algorithm.Encrypt,
+ key: *CryptoKey,
+ data: []const u8,
+ encrypting: bool,
+ exec: *const Execution,
+) !js.Promise {
+ const local = exec.js.local.?;
+ const params = switch (algo) {
+ .params => |p| p,
+ // A bare string identifier carries no iv/counter, so it can't drive AES.
+ .name => return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }),
+ };
+
+ const out = AES.crypt(params, key, data, encrypting, exec) catch |err| switch (err) {
+ error.InvalidAccessError => return local.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
+ error.OperationError => return local.rejectPromise(.{ .dom_exception = .{ .err = error.OperationError } }),
+ error.NotSupported => return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } }),
+ error.OutOfMemory => return error.OutOfMemory,
+ };
+ return local.resolvePromise(js.ArrayBuffer{ .values = out });
}
/// Generate a digital signature.
@@ -238,15 +522,31 @@ pub fn verify(
};
}
+/// `digest()` accepts an AlgorithmIdentifier: either a bare string (`"SHA-256"`)
+/// or an object (`{name: "SHA-256"}`). The object variant must come first — a
+/// `[]const u8` coerces *any* JS value to a string, so it has to be the fallback.
+const DigestInput = union(enum) {
+ obj: struct { name: []const u8 },
+ str: []const u8,
+
+ fn name(self: DigestInput) []const u8 {
+ return switch (self) {
+ .obj => |o| o.name,
+ .str => |s| s,
+ };
+ }
+};
+
/// Generates a digest of the given data, using the specified hash function.
-pub fn digest(_: *const SubtleCrypto, algo: []const u8, data: js.TypedArray(u8), exec: *const Execution) !js.Promise {
+pub fn digest(_: *const SubtleCrypto, algo: DigestInput, data: js.TypedArray(u8), exec: *const Execution) !js.Promise {
const local = exec.js.local.?;
- if (algo.len > 10) {
+ const algo_name = algo.name();
+ if (algo_name.len > 10) {
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
}
- const normalized = std.ascii.upperString(exec.buf, algo);
+ const normalized = std.ascii.upperString(exec.buf, algo_name);
const digest_type = crypto.findDigest(normalized) catch {
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
};
@@ -271,9 +571,13 @@ pub const JsApi = struct {
};
pub const generateKey = bridge.function(SubtleCrypto.generateKey, .{ .dom_exception = true });
+ pub const importKey = bridge.function(SubtleCrypto.importKey, .{ .dom_exception = true });
pub const exportKey = bridge.function(SubtleCrypto.exportKey, .{ .dom_exception = true });
+ pub const encrypt = bridge.function(SubtleCrypto.encrypt, .{ .dom_exception = true });
+ pub const decrypt = bridge.function(SubtleCrypto.decrypt, .{ .dom_exception = true });
pub const sign = bridge.function(SubtleCrypto.sign, .{ .dom_exception = true });
pub const verify = bridge.function(SubtleCrypto.verify, .{ .dom_exception = true });
pub const deriveBits = bridge.function(SubtleCrypto.deriveBits, .{ .dom_exception = true });
+ pub const deriveKey = bridge.function(SubtleCrypto.deriveKey, .{ .dom_exception = true });
pub const digest = bridge.function(SubtleCrypto.digest, .{ .dom_exception = true });
};
diff --git a/src/browser/webapi/crypto/AES.zig b/src/browser/webapi/crypto/AES.zig
index 3e9c12b5..28b97797 100644
--- a/src/browser/webapi/crypto/AES.zig
+++ b/src/browser/webapi/crypto/AES.zig
@@ -16,32 +16,44 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-//! AES generateKey parameter validation.
-//!
-//! Key generation itself is not implemented; this module rejects malformed
-//! input with the spec-mandated error name so the failure-path WPT tests
-//! match. Successful inputs fall through to the caller's `not_implemented`
-//! warning + `NotSupportedError`.
+//! AES for AES-CBC/CTR/GCM/KW: key generation, import, and encrypt/decrypt
+//! (CBC/CTR/GCM). Keys are raw byte strings; wrapKey/unwrapKey is not yet
+//! implemented.
const std = @import("std");
+const lp = @import("lightpanda");
+const crypto = @import("../../../sys/libcrypto.zig");
+const js = @import("../../js/js.zig");
+const CryptoKey = @import("../CryptoKey.zig");
+
+const common = @import("common.zig");
const algorithm = @import("algorithm.zig");
+const Execution = js.Execution;
+
+pub fn canonicalName(name: []const u8) ?[]const u8 {
+ inline for ([_][]const u8{ "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW" }) |canonical| {
+ if (eqlIgnoreCase(name, canonical)) return canonical;
+ }
+ return null;
+}
+
+/// The usages permitted for the given AES variant.
+fn allowedUsages(name: []const u8) ?[]const []const u8 {
+ if (eqlIgnoreCase(name, "AES-CBC") or eqlIgnoreCase(name, "AES-CTR") or eqlIgnoreCase(name, "AES-GCM")) {
+ return &.{ "encrypt", "decrypt", "wrapKey", "unwrapKey" };
+ }
+ if (eqlIgnoreCase(name, "AES-KW")) {
+ return &.{ "wrapKey", "unwrapKey" };
+ }
+ return null;
+}
+
/// Per WebCrypto: "Generate Key" operation for AES-CBC/CTR/GCM/KW.
/// Validation order matches the spec: usages → length → empty usages.
pub fn validate(params: algorithm.Init.AesKeyGen, key_usages: []const []const u8) !void {
- const allowed: []const []const u8 = blk: {
- if (eql(params.name, "AES-CBC") or
- eql(params.name, "AES-CTR") or
- eql(params.name, "AES-GCM"))
- {
- break :blk &.{ "encrypt", "decrypt", "wrapKey", "unwrapKey" };
- }
- if (eql(params.name, "AES-KW")) {
- break :blk &.{ "wrapKey", "unwrapKey" };
- }
- return error.NotSupported;
- };
+ const allowed = allowedUsages(params.name) orelse return error.NotSupported;
for (key_usages) |usage| {
var ok = false;
@@ -65,6 +77,282 @@ pub fn validate(params: algorithm.Init.AesKeyGen, key_usages: []const []const u8
}
}
-fn eql(a: []const u8, b: []const u8) bool {
+/// Generates a fresh AES key (random bytes of the requested length).
+pub fn generate(
+ params: algorithm.Init.AesKeyGen,
+ extractable: bool,
+ key_usages: []const []const u8,
+ exec: *const Execution,
+) !js.Promise {
+ const local = exec.js.local.?;
+ validate(params, key_usages) catch |err| {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
+ };
+
+ // validate() already confirmed the usages and length are well-formed.
+ const allowed = allowedUsages(params.name).?;
+ const mask = common.usageMask(allowed, key_usages) catch unreachable;
+
+ const key = try exec.arena.alloc(u8, params.length / 8);
+
+ const res = crypto.RAND_bytes(key.ptr, key.len);
+ lp.assert(res == 1, "AES.generate", .{ .res = res });
+
+ const crypto_key = try exec._factory.create(CryptoKey{
+ ._type = .aes,
+ ._kind = .secret,
+ ._extractable = extractable,
+ ._usages = mask,
+ ._key = key,
+ ._algorithm = .{ .name = canonicalName(params.name).? },
+ });
+
+ return local.resolvePromise(crypto_key);
+}
+
+/// Imports raw AES key material (from the `raw` or `jwk` formats; the caller has
+/// already turned both into the underlying bytes).
+pub fn import(
+ name: []const u8,
+ raw: []const u8,
+ extractable: bool,
+ key_usages: []const []const u8,
+ exec: *const Execution,
+) !js.Promise {
+ const local = exec.js.local.?;
+
+ const canonical = canonicalName(name) orelse {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+ };
+
+ const mask = common.usageMask(allowedUsages(name).?, key_usages) catch |err| {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
+ };
+
+ if (raw.len != 16 and raw.len != 24 and raw.len != 32) {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
+ }
+
+ const key = try exec.arena.dupe(u8, raw);
+ const crypto_key = try exec._factory.create(CryptoKey{
+ ._type = .aes,
+ ._kind = .secret,
+ ._extractable = extractable,
+ ._usages = mask,
+ ._key = key,
+ ._algorithm = .{ .name = canonical },
+ });
+
+ return local.resolvePromise(crypto_key);
+}
+
+pub const CipherError = error{ InvalidAccessError, OperationError, NotSupported, OutOfMemory };
+
+const Mode = enum { cbc, ctr, gcm };
+
+fn modeFor(name: []const u8) ?Mode {
+ if (eqlIgnoreCase(name, "AES-CBC")) return .cbc;
+ if (eqlIgnoreCase(name, "AES-CTR")) return .ctr;
+ if (eqlIgnoreCase(name, "AES-GCM")) return .gcm;
+ return null;
+}
+
+fn cipherFor(mode: Mode, key_len: usize) ?*const crypto.EVP_CIPHER {
+ return switch (mode) {
+ .cbc => switch (key_len) {
+ 16 => crypto.EVP_aes_128_cbc(),
+ 24 => crypto.EVP_aes_192_cbc(),
+ 32 => crypto.EVP_aes_256_cbc(),
+ else => null,
+ },
+ .ctr => switch (key_len) {
+ 16 => crypto.EVP_aes_128_ctr(),
+ 24 => crypto.EVP_aes_192_ctr(),
+ 32 => crypto.EVP_aes_256_ctr(),
+ else => null,
+ },
+ .gcm => switch (key_len) {
+ 16 => crypto.EVP_aes_128_gcm(),
+ 24 => crypto.EVP_aes_192_gcm(),
+ 32 => crypto.EVP_aes_256_gcm(),
+ else => null,
+ },
+ };
+}
+
+/// AES encrypt (`encrypting = true`) or decrypt. Returns the resulting bytes
+/// (ciphertext for GCM includes the appended tag).
+pub fn crypt(
+ params: anytype, // algorithm.Encrypt.params
+ key: *const CryptoKey,
+ data: []const u8,
+ encrypting: bool,
+ exec: *const Execution,
+) CipherError![]const u8 {
+ const mode = modeFor(params.name) orelse return error.NotSupported;
+
+ // The key must be an AES key for this exact algorithm, with the matching
+ // usage.
+ if (key._type != .aes or !eqlIgnoreCase(key._algorithm.name, params.name)) {
+ return error.InvalidAccessError;
+ }
+ if ((encrypting and !key.canEncrypt()) or (!encrypting and !key.canDecrypt())) {
+ return error.InvalidAccessError;
+ }
+
+ const cipher = cipherFor(mode, key._key.len) orelse return error.OperationError;
+ return switch (mode) {
+ .cbc => cbcOrCtr(cipher, key._key, ivOf(params.iv) orelse return error.OperationError, data, encrypting, true, exec),
+ .ctr => blk: {
+ // The counter `length` (bits of the block used as the counter) must
+ // be in 1..128.
+ const len = params.length orelse return error.OperationError;
+ if (len == 0 or len > 128) return error.OperationError;
+ break :blk cbcOrCtr(cipher, key._key, ivOf(params.counter) orelse return error.OperationError, data, encrypting, false, exec);
+ },
+ .gcm => gcm(cipher, key._key, params, data, encrypting, exec),
+ };
+}
+
+fn ivOf(opt: anytype) ?[]const u8 {
+ const v = opt orelse return null;
+ return v.values;
+}
+
+// libcrypto's EVP_*Init/Update/Final are distinct functions for encrypt vs
+// decrypt; extern fns can't be selected into a runtime variable, so branch here.
+fn cipherInit(ctx: *crypto.EVP_CIPHER_CTX, cipher: ?*const crypto.EVP_CIPHER, key: [*c]const u8, iv: [*c]const u8, encrypting: bool) c_int {
+ return if (encrypting)
+ crypto.EVP_EncryptInit_ex(ctx, cipher, null, key, iv)
+ else
+ crypto.EVP_DecryptInit_ex(ctx, cipher, null, key, iv);
+}
+
+fn cipherUpdate(ctx: *crypto.EVP_CIPHER_CTX, out: [*c]u8, out_len: *c_int, in: [*c]const u8, in_len: c_int, encrypting: bool) c_int {
+ return if (encrypting)
+ crypto.EVP_EncryptUpdate(ctx, out, out_len, in, in_len)
+ else
+ crypto.EVP_DecryptUpdate(ctx, out, out_len, in, in_len);
+}
+
+fn cbcOrCtr(
+ cipher: *const crypto.EVP_CIPHER,
+ key: []const u8,
+ iv: []const u8,
+ data: []const u8,
+ encrypting: bool,
+ padded: bool, // CBC pads, CTR does not
+ exec: *const Execution,
+) CipherError![]const u8 {
+ if (iv.len != 16) return error.OperationError;
+
+ const ctx = crypto.EVP_CIPHER_CTX_new() orelse return error.OutOfMemory;
+ defer crypto.EVP_CIPHER_CTX_free(ctx);
+
+ if (cipherInit(ctx, cipher, @ptrCast(key.ptr), @ptrCast(iv.ptr), encrypting) != 1) {
+ return error.OperationError;
+ }
+ if (!padded) {
+ _ = crypto.EVP_CIPHER_CTX_set_padding(ctx, 0);
+ }
+
+ // Block ciphers may emit up to one extra block on top of the input.
+ const out = try exec.call_arena.alloc(u8, data.len + 16);
+ var out_len: c_int = 0;
+ if (cipherUpdate(ctx, out.ptr, &out_len, @ptrCast(data.ptr), @intCast(data.len), encrypting) != 1) {
+ return error.OperationError;
+ }
+
+ var final_len: c_int = 0;
+ const tail = out.ptr + @as(usize, @intCast(out_len));
+ // Decrypt final fails on bad padding → OperationError, per spec.
+ const final_ok = if (encrypting)
+ crypto.EVP_EncryptFinal_ex(ctx, tail, &final_len)
+ else
+ crypto.EVP_DecryptFinal_ex(ctx, tail, &final_len);
+ if (final_ok != 1) {
+ return error.OperationError;
+ }
+
+ return out[0..@intCast(out_len + final_len)];
+}
+
+fn gcm(
+ cipher: *const crypto.EVP_CIPHER,
+ key: []const u8,
+ params: anytype,
+ data: []const u8,
+ encrypting: bool,
+ exec: *const Execution,
+) CipherError![]const u8 {
+ const iv = ivOf(params.iv) orelse return error.OperationError;
+
+ const tag_bits = params.tagLength orelse 128;
+ switch (tag_bits) {
+ 32, 64, 96, 104, 112, 120, 128 => {},
+ else => return error.OperationError,
+ }
+ const tag_len: usize = tag_bits / 8;
+
+ const ctx = crypto.EVP_CIPHER_CTX_new() orelse return error.OutOfMemory;
+ defer crypto.EVP_CIPHER_CTX_free(ctx);
+
+ if (cipherInit(ctx, cipher, null, null, encrypting) != 1) return error.OperationError;
+ if (crypto.EVP_CIPHER_CTX_ctrl(ctx, crypto.EVP_CTRL_GCM_SET_IVLEN, @intCast(iv.len), null) != 1) {
+ return error.OperationError;
+ }
+ if (cipherInit(ctx, null, @ptrCast(key.ptr), @ptrCast(iv.ptr), encrypting) != 1) return error.OperationError;
+
+ // Additional authenticated data (optional), fed with a null output buffer.
+ if (ivOf(params.additionalData)) |aad| {
+ if (aad.len > 0) {
+ var aad_len: c_int = 0;
+ if (cipherUpdate(ctx, null, &aad_len, @ptrCast(aad.ptr), @intCast(aad.len), encrypting) != 1) {
+ return error.OperationError;
+ }
+ }
+ }
+
+ if (encrypting) {
+ const out = try exec.call_arena.alloc(u8, data.len + tag_len);
+ var out_len: c_int = 0;
+ if (data.len > 0 and cipherUpdate(ctx, out.ptr, &out_len, @ptrCast(data.ptr), @intCast(data.len), true) != 1) {
+ return error.OperationError;
+ }
+ var final_len: c_int = 0;
+ if (crypto.EVP_EncryptFinal_ex(ctx, out.ptr + @as(usize, @intCast(out_len)), &final_len) != 1) {
+ return error.OperationError;
+ }
+ const written: usize = @intCast(out_len + final_len);
+ // Append the authentication tag.
+ if (crypto.EVP_CIPHER_CTX_ctrl(ctx, crypto.EVP_CTRL_GCM_GET_TAG, @intCast(tag_len), out.ptr + written) != 1) {
+ return error.OperationError;
+ }
+ return out[0 .. written + tag_len];
+ }
+
+ // Decrypt: the ciphertext carries the tag as its final `tag_len` bytes.
+ if (data.len < tag_len) return error.OperationError;
+ const ct = data[0 .. data.len - tag_len];
+ const tag = data[data.len - tag_len ..];
+
+ if (crypto.EVP_CIPHER_CTX_ctrl(ctx, crypto.EVP_CTRL_GCM_SET_TAG, @intCast(tag_len), @ptrCast(@constCast(tag.ptr))) != 1) {
+ return error.OperationError;
+ }
+
+ const out = try exec.call_arena.alloc(u8, ct.len + 16);
+ var out_len: c_int = 0;
+ if (ct.len > 0 and cipherUpdate(ctx, out.ptr, &out_len, @ptrCast(ct.ptr), @intCast(ct.len), false) != 1) {
+ return error.OperationError;
+ }
+ var final_len: c_int = 0;
+ // Tag verification happens here: failure → OperationError.
+ if (crypto.EVP_DecryptFinal_ex(ctx, out.ptr + @as(usize, @intCast(out_len)), &final_len) != 1) {
+ return error.OperationError;
+ }
+ return out[0..@intCast(out_len + final_len)];
+}
+
+fn eqlIgnoreCase(a: []const u8, b: []const u8) bool {
return std.ascii.eqlIgnoreCase(a, b);
}
diff --git a/src/browser/webapi/crypto/EC.zig b/src/browser/webapi/crypto/EC.zig
index 8d99ef11..236b2efd 100644
--- a/src/browser/webapi/crypto/EC.zig
+++ b/src/browser/webapi/crypto/EC.zig
@@ -16,43 +16,69 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-//! ECDSA / ECDH generateKey parameter validation. See AES.zig for
-//! the rationale on validate-without-generate.
+//! ECDSA / ECDH key generation, import (spki/pkcs8 via DER) and ECDH key
+//! agreement. Key bytes live as a libcrypto EVP_PKEY in `CryptoKey._vary.pkey`.
const std = @import("std");
+const lp = @import("lightpanda");
+const crypto = @import("../../../sys/libcrypto.zig");
+const js = @import("../../js/js.zig");
+const CryptoKey = @import("../CryptoKey.zig");
+
+const common = @import("common.zig");
const algorithm = @import("algorithm.zig");
+const Execution = js.Execution;
+
+/// Errors mapped to DOMException names by the caller.
+pub const Error = error{ InvalidAccessError, NotSupported, OperationError, OutOfMemory };
+
+/// The registered name (case-insensitive), or null if not an EC algorithm.
+pub fn canonicalName(name: []const u8) ?[]const u8 {
+ inline for ([_][]const u8{ "ECDSA", "ECDH" }) |canonical| {
+ if (eqlIgnoreCase(name, canonical)) return canonical;
+ }
+ return null;
+}
+
+fn curveNid(named_curve: []const u8) ?c_int {
+ if (eqlIgnoreCase(named_curve, "P-256")) return crypto.NID_X9_62_prime256v1;
+ if (eqlIgnoreCase(named_curve, "P-384")) return crypto.NID_secp384r1;
+ if (eqlIgnoreCase(named_curve, "P-521")) return crypto.NID_secp521r1;
+ return null;
+}
+
+fn curveCanonical(named_curve: []const u8) ?[]const u8 {
+ inline for ([_][]const u8{ "P-256", "P-384", "P-521" }) |c| {
+ if (eqlIgnoreCase(named_curve, c)) return c;
+ }
+ return null;
+}
+
+/// The (private, public) usages legal for the algorithm.
+fn usageSets(name: []const u8) struct { private: []const []const u8, public: []const []const u8 } {
+ if (eqlIgnoreCase(name, "ECDSA")) {
+ return .{ .private = &.{"sign"}, .public = &.{"verify"} };
+ }
+ // ECDH
+ return .{ .private = &.{ "deriveKey", "deriveBits" }, .public = &.{} };
+}
+
pub fn validate(params: algorithm.Init.EcKeyGen, key_usages: []const []const u8) !void {
- const allowed: []const []const u8 = blk: {
- if (eql(params.name, "ECDSA")) {
- break :blk &.{ "sign", "verify" };
- }
- if (eql(params.name, "ECDH")) {
- break :blk &.{ "deriveKey", "deriveBits" };
- }
+ const sets = blk: {
+ if (eqlIgnoreCase(params.name, "ECDSA") or eqlIgnoreCase(params.name, "ECDH")) break :blk usageSets(params.name);
return error.NotSupported;
};
-
+ // A usage that belongs to neither the private nor the public set is illegal.
for (key_usages) |usage| {
- var ok = false;
- for (allowed) |a| {
- if (std.mem.eql(u8, a, usage)) {
- ok = true;
- break;
- }
- }
- if (!ok) {
+ if (!contains(sets.private, usage) and !contains(sets.public, usage)) {
return error.SyntaxError;
}
}
- // Per spec, an unsupported `namedCurve` is NotSupportedError, not OperationError —
- // unlike AES length, where the algorithm registers the value as invalid.
- if (!eql(params.namedCurve, "P-256") and
- !eql(params.namedCurve, "P-384") and
- !eql(params.namedCurve, "P-521"))
- {
+ if (curveNid(params.namedCurve) == null) {
+ // Per spec, an unsupported `namedCurve` is NotSupportedError.
return error.NotSupported;
}
@@ -61,6 +87,197 @@ pub fn validate(params: algorithm.Init.EcKeyGen, key_usages: []const []const u8)
}
}
-fn eql(a: []const u8, b: []const u8) bool {
+/// Generates an EC key pair on the requested curve.
+pub fn generate(
+ params: algorithm.Init.EcKeyGen,
+ extractable: bool,
+ key_usages: []const []const u8,
+ exec: *const Execution,
+) !js.Promise {
+ const local = exec.js.local.?;
+ validate(params, key_usages) catch |err| {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
+ };
+
+ const name = canonicalName(params.name).?;
+ const curve = curveCanonical(params.namedCurve).?;
+ const nid = curveNid(params.namedCurve).?;
+ const sets = usageSets(name);
+
+ // Split the requested usages across the pair: each key keeps only the
+ // usages legal for it. validate() already rejected anything illegal.
+ const private_mask = maskOf(sets.private, key_usages);
+ const public_mask = maskOf(sets.public, key_usages);
+
+ const ec = crypto.EC_KEY_new_by_curve_name(nid) orelse return error.OutOfMemory;
+ defer crypto.EC_KEY_free(ec);
+ if (crypto.EC_KEY_generate_key(ec) != 1) {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.OperationError } });
+ }
+
+ const private_pkey = crypto.EVP_PKEY_new() orelse return error.OutOfMemory;
+ errdefer crypto.EVP_PKEY_free(private_pkey);
+ if (crypto.EVP_PKEY_set1_EC_KEY(private_pkey, ec) != 1) return error.OutOfMemory;
+
+ // A public-only EVP_PKEY (same curve, just the public point) for the peer.
+ const pub_ec = crypto.EC_KEY_new_by_curve_name(nid) orelse return error.OutOfMemory;
+ defer crypto.EC_KEY_free(pub_ec);
+ const point = crypto.EC_KEY_get0_public_key(ec) orelse return error.OperationError;
+ if (crypto.EC_KEY_set_public_key(pub_ec, point) != 1) return error.OperationError;
+ const public_pkey = crypto.EVP_PKEY_new() orelse return error.OutOfMemory;
+ errdefer crypto.EVP_PKEY_free(public_pkey);
+ if (crypto.EVP_PKEY_set1_EC_KEY(public_pkey, pub_ec) != 1) return error.OutOfMemory;
+
+ const private = try exec._factory.create(CryptoKey{
+ ._type = .ec,
+ ._kind = .private,
+ ._extractable = extractable,
+ ._usages = private_mask,
+ ._key = &.{},
+ ._algorithm = .{ .name = name, .named_curve = curve },
+ ._vary = .{ .pkey = private_pkey },
+ });
+ errdefer exec._factory.destroy(private);
+
+ const public = try exec._factory.create(CryptoKey{
+ ._type = .ec,
+ ._kind = .public,
+ // Public keys are always extractable.
+ ._extractable = true,
+ ._usages = public_mask,
+ ._key = &.{},
+ ._algorithm = .{ .name = name, .named_curve = curve },
+ ._vary = .{ .pkey = public_pkey },
+ });
+
+ return local.resolvePromise(CryptoKey.Pair{ .privateKey = private, .publicKey = public });
+}
+
+/// Imports an EC key from DER (`spki` public / `pkcs8` private). jwk/raw aren't
+/// handled yet. libcrypto parses the DER, so a malformed structure is DataError.
+pub fn import(
+ name: []const u8,
+ named_curve: []const u8,
+ format: []const u8,
+ der: []const u8,
+ is_private: bool,
+ extractable: bool,
+ usages_mask: u8,
+ exec: *const Execution,
+) !js.Promise {
+ const local = exec.js.local.?;
+
+ const canonical = canonicalName(name).?;
+ const curve = curveCanonical(named_curve) orelse {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+ };
+
+ var ptr: [*c]const u8 = der.ptr;
+ const pkey: *crypto.EVP_PKEY = blk: {
+ if (std.mem.eql(u8, format, "spki")) {
+ break :blk crypto.d2i_PUBKEY(null, &ptr, @intCast(der.len)) orelse {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
+ };
+ }
+ if (std.mem.eql(u8, format, "pkcs8")) {
+ break :blk crypto.d2i_AutoPrivateKey(null, &ptr, @intCast(der.len)) orelse {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
+ };
+ }
+ // jwk / raw not implemented yet.
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
+ };
+ errdefer crypto.EVP_PKEY_free(pkey);
+
+ if (crypto.EVP_PKEY_id(pkey) != crypto.EVP_PKEY_EC) {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
+ }
+
+ const crypto_key = try exec._factory.create(CryptoKey{
+ ._type = .ec,
+ ._kind = if (is_private) .private else .public,
+ ._extractable = extractable,
+ ._usages = usages_mask,
+ ._key = &.{},
+ ._algorithm = .{ .name = canonical, .named_curve = curve },
+ ._vary = .{ .pkey = pkey },
+ });
+
+ return local.resolvePromise(crypto_key);
+}
+
+/// ECDH key agreement: derive `length_in_bits` from `private` + the peer
+/// `public`. Mirrors X25519's truncation rules.
+pub fn deriveBits(
+ private: *const CryptoKey,
+ public: *const CryptoKey,
+ length_in_bits: ?u32,
+ exec: *const Execution,
+) Error![]const u8 {
+ // The peer must be an ECDH *public* key on the *same* curve.
+ if (public._type != .ec or public._kind != .public) {
+ return error.InvalidAccessError;
+ }
+ if (!eqlIgnoreCase(public._algorithm.name, "ECDH")) {
+ return error.InvalidAccessError;
+ }
+ if (!eqlIgnoreCase(private._algorithm.named_curve orelse "", public._algorithm.named_curve orelse "")) {
+ return error.InvalidAccessError;
+ }
+
+ const ctx = crypto.EVP_PKEY_CTX_new(private.getKeyObject(), null) orelse return error.OperationError;
+ defer crypto.EVP_PKEY_CTX_free(ctx);
+
+ if (crypto.EVP_PKEY_derive_init(ctx) != 1 or crypto.EVP_PKEY_derive_set_peer(ctx, public.getKeyObject()) != 1) {
+ return error.OperationError;
+ }
+
+ // First call with a null buffer reports the full shared-secret length.
+ var secret_len: usize = 0;
+ if (crypto.EVP_PKEY_derive(ctx, null, &secret_len) != 1 or secret_len == 0) {
+ return error.OperationError;
+ }
+ const secret = try exec.call_arena.alloc(u8, secret_len);
+ if (crypto.EVP_PKEY_derive(ctx, secret.ptr, &secret_len) != 1) {
+ return error.OperationError;
+ }
+
+ // null length means "the full shared secret".
+ const bits = length_in_bits orelse @as(u32, @intCast(secret_len * 8));
+ const byte_len = (bits + 7) / 8;
+ if (byte_len > secret_len) {
+ return error.OperationError;
+ }
+ const out = secret[0..byte_len];
+
+ // Zero the unused trailing bits of the final byte.
+ const remainder_bits: u3 = @intCast(bits % 8);
+ if (remainder_bits != 0 and out.len > 0) {
+ out[out.len - 1] &= ~(@as(u8, 0xFF) >> remainder_bits);
+ }
+ return out;
+}
+
+fn contains(set: []const []const u8, usage: []const u8) bool {
+ for (set) |s| {
+ if (std.mem.eql(u8, s, usage)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/// The usage bitmask of those `usages` that belong to `set`.
+fn maskOf(set: []const []const u8, usages: []const []const u8) u8 {
+ var mask: u8 = 0;
+ for (usages) |u| {
+ if (contains(set, u)) {
+ mask |= common.usageBit(u).?;
+ }
+ }
+ return mask;
+}
+
+fn eqlIgnoreCase(a: []const u8, b: []const u8) bool {
return std.ascii.eqlIgnoreCase(a, b);
}
diff --git a/src/browser/webapi/crypto/HMAC.zig b/src/browser/webapi/crypto/HMAC.zig
index 89277177..c7a59e07 100644
--- a/src/browser/webapi/crypto/HMAC.zig
+++ b/src/browser/webapi/crypto/HMAC.zig
@@ -24,6 +24,7 @@ const crypto = @import("../../../sys/libcrypto.zig");
const js = @import("../../js/js.zig");
const algorithm = @import("algorithm.zig");
+const common = @import("common.zig");
const CryptoKey = @import("../CryptoKey.zig");
@@ -36,12 +37,13 @@ pub fn init(
exec: *const Execution,
) !js.Promise {
const local = exec.js.local.?;
- // Per spec, an unrecognized hash is caught during algorithm normalization
- // and surfaces as NotSupportedError.
- const digest = crypto.findDigest(switch (params.hash) {
+ const hash_name = switch (params.hash) {
.string => |str| str,
.object => |obj| obj.name,
- }) catch return local.rejectPromise(.{
+ };
+ // Per spec, an unrecognized hash is caught during algorithm normalization
+ // and surfaces as NotSupportedError.
+ const digest = crypto.findDigest(hash_name) catch return local.rejectPromise(.{
.dom_exception = .{ .err = error.NotSupported },
});
@@ -76,7 +78,6 @@ pub fn init(
// Should we reject this in promise too?
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);
@@ -87,6 +88,46 @@ pub fn init(
._extractable = extractable,
._usages = mask,
._key = key,
+ ._algorithm = .{ .name = "HMAC", .hash = try exec.arena.dupe(u8, hash_name) },
+ ._vary = .{ .digest = digest },
+ });
+
+ return local.resolvePromise(crypto_key);
+}
+
+/// Imports raw HMAC key material (from the `raw` or `jwk` formats; the caller
+/// has already turned both into the underlying bytes).
+pub fn import(
+ hash_name: []const u8,
+ raw: []const u8,
+ extractable: bool,
+ key_usages: []const []const u8,
+ exec: *const Execution,
+) !js.Promise {
+ const local = exec.js.local.?;
+
+ const digest = crypto.findDigest(hash_name) catch return local.rejectPromise(.{
+ .dom_exception = .{ .err = error.NotSupported },
+ });
+
+ const mask = common.usageMask(&.{ "sign", "verify" }, key_usages) catch |err| {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
+ };
+
+ if (raw.len == 0) {
+ return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
+ }
+
+ const key = try exec.arena.dupe(u8, raw);
+ errdefer exec.arena.free(key);
+
+ const crypto_key = try exec._factory.create(CryptoKey{
+ ._type = .hmac,
+ ._kind = .secret,
+ ._extractable = extractable,
+ ._usages = mask,
+ ._key = key,
+ ._algorithm = .{ .name = "HMAC", .hash = try exec.arena.dupe(u8, hash_name) },
._vary = .{ .digest = digest },
});
diff --git a/src/browser/webapi/crypto/KDF.zig b/src/browser/webapi/crypto/KDF.zig
new file mode 100644
index 00000000..98aa87b6
--- /dev/null
+++ b/src/browser/webapi/crypto/KDF.zig
@@ -0,0 +1,124 @@
+// 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 .
+
+//! Key-derivation functions for `deriveBits()`: PBKDF2 and HKDF. Both operate
+//! on a raw "secret" base key (imported via the `raw` format).
+
+const std = @import("std");
+
+const js = @import("../../js/js.zig");
+const CryptoKey = @import("../CryptoKey.zig");
+const crypto = @import("../../../sys/libcrypto.zig");
+
+const algorithm = @import("algorithm.zig");
+
+const Execution = js.Execution;
+
+/// Errors a derivation can raise, each mapping to a specific DOMException name.
+pub const Error = error{ InvalidAccessError, NotSupported, OperationError, OutOfMemory };
+
+/// Validates the base key against the requested algorithm (shared by PBKDF2 and
+/// HKDF). `usage_ok` is the caller-checked usage gate (deriveBits for
+/// `deriveBits()`, deriveKey for `deriveKey()`); the key must also have been
+/// created for this same algorithm. Either mismatch is an InvalidAccessError.
+fn checkBaseKey(base_key: *const CryptoKey, name: []const u8, usage_ok: bool) Error!void {
+ if (!usage_ok) {
+ return error.InvalidAccessError;
+ }
+ if (!std.ascii.eqlIgnoreCase(base_key._algorithm.name, name)) {
+ return error.InvalidAccessError;
+ }
+}
+
+/// `length` is the requested output size in bits: null and non-multiples of 8
+/// are OperationErrors. Returns the output byte length.
+fn outputLen(length: ?u32) Error!usize {
+ const bits = length orelse return error.OperationError;
+ if (bits % 8 != 0) {
+ return error.OperationError;
+ }
+ return bits / 8;
+}
+
+pub fn pbkdf2(
+ base_key: *const CryptoKey,
+ params: algorithm.Derive.Pbkdf2Params,
+ length: ?u32,
+ usage_ok: bool,
+ exec: *const Execution,
+) Error![]const u8 {
+ try checkBaseKey(base_key, "PBKDF2", usage_ok);
+ const digest = crypto.findDigest(params.hash.name()) catch return error.NotSupported;
+ const out = try exec.call_arena.alloc(u8, try outputLen(length));
+ // A zero-length derivation is valid and yields an empty buffer; the C
+ // routines reject a zero output length, so short-circuit here.
+ if (out.len == 0) {
+ return out;
+ }
+
+ const salt = params.salt.values;
+ const res = crypto.PKCS5_PBKDF2_HMAC(
+ @ptrCast(base_key._key.ptr),
+ base_key._key.len,
+ @ptrCast(salt.ptr),
+ salt.len,
+ params.iterations,
+ digest,
+ out.len,
+ out.ptr,
+ );
+ if (res != 1) {
+ return error.OperationError;
+ }
+ return out;
+}
+
+pub fn hkdf(
+ base_key: *const CryptoKey,
+ params: algorithm.Derive.HkdfParams,
+ length: ?u32,
+ usage_ok: bool,
+ exec: *const Execution,
+) Error![]const u8 {
+ try checkBaseKey(base_key, "HKDF", usage_ok);
+ const digest = crypto.findDigest(params.hash.name()) catch return error.NotSupported;
+ const out = try exec.call_arena.alloc(u8, try outputLen(length));
+ // A zero-length derivation is valid and yields an empty buffer; the C
+ // routines reject a zero output length, so short-circuit here.
+ if (out.len == 0) {
+ return out;
+ }
+
+ const salt = params.salt.values;
+ const info = params.info.values;
+ const res = crypto.HKDF(
+ out.ptr,
+ out.len,
+ digest,
+ @ptrCast(base_key._key.ptr),
+ base_key._key.len,
+ @ptrCast(salt.ptr),
+ salt.len,
+ @ptrCast(info.ptr),
+ info.len,
+ );
+ if (res != 1) {
+ return error.OperationError;
+ }
+ return out;
+}
diff --git a/src/browser/webapi/crypto/X25519.zig b/src/browser/webapi/crypto/X25519.zig
index 048ab999..14e4e137 100644
--- a/src/browser/webapi/crypto/X25519.zig
+++ b/src/browser/webapi/crypto/X25519.zig
@@ -93,20 +93,24 @@ pub fn init(
const private = try exec._factory.create(CryptoKey{
._type = .x25519,
+ ._kind = .private,
._extractable = extractable,
._usages = mask,
._key = private_key,
+ ._algorithm = .{ .name = "X25519" },
._vary = .{ .pkey = private_pkey },
});
errdefer exec._factory.destroy(private);
const public = try exec._factory.create(CryptoKey{
._type = .x25519,
+ ._kind = .public,
// Public keys are always extractable.
._extractable = true,
// Always empty for public key.
._usages = 0,
._key = public_value,
+ ._algorithm = .{ .name = "X25519" },
._vary = .{ .pkey = public_pkey },
});
diff --git a/src/browser/webapi/crypto/algorithm.zig b/src/browser/webapi/crypto/algorithm.zig
index 28861a60..aa96c0e5 100644
--- a/src/browser/webapi/crypto/algorithm.zig
+++ b/src/browser/webapi/crypto/algorithm.zig
@@ -89,9 +89,126 @@ pub const Init = union(enum) {
};
};
-/// Algorithm for deriveBits() and deriveKey().
+/// Algorithm for deriveBits() and deriveKey(). Variants are distinguished by
+/// their required members (`iterations` for PBKDF2, `info` for HKDF, `public`
+/// for ECDH/X25519), so probe order doesn't cause ambiguity.
pub const Derive = union(enum) {
+ pbkdf2: Pbkdf2Params,
+ hkdf: HkdfParams,
ecdh_or_x25519: Init.EcdhKeyDeriveParams,
+
+ /// A hash AlgorithmIdentifier — either `"SHA-256"` or `{name: "SHA-256"}`.
+ pub const Hash = union(enum) {
+ string: []const u8,
+ object: struct { name: []const u8 },
+
+ pub fn name(self: Hash) []const u8 {
+ return switch (self) {
+ .string => |s| s,
+ .object => |o| o.name,
+ };
+ }
+ };
+
+ pub const Pbkdf2Params = struct {
+ name: []const u8,
+ hash: Hash,
+ salt: js.TypedArray(u8),
+ iterations: u32,
+ };
+
+ pub const HkdfParams = struct {
+ name: []const u8,
+ hash: Hash,
+ salt: js.TypedArray(u8),
+ info: js.TypedArray(u8),
+ };
+};
+
+/// The `derivedKeyType` argument to `deriveKey()` — the algorithm of the key to
+/// produce. HMAC carries a hash; AES carries a length. Probed in that order so
+/// the more specific shapes win.
+pub const DerivedKey = union(enum) {
+ hmac: struct { name: []const u8, hash: Derive.Hash, length: ?u32 = null },
+ keyed: struct { name: []const u8, length: u32 },
+ object: struct { name: []const u8 },
+ name: []const u8,
+};
+
+/// Algorithm passed to `importKey()`. HMAC carries a hash, so it must be probed
+/// before the bare-`{name}` object; the plain string is the final fallback (any
+/// JS value coerces to a string).
+pub const Import = union(enum) {
+ hmac: struct {
+ name: []const u8,
+ hash: union(enum) {
+ string: []const u8,
+ object: struct { name: []const u8 },
+ },
+ length: ?u32 = null,
+ },
+ ec: struct { name: []const u8, namedCurve: []const u8 },
+ object: struct { name: []const u8 },
+ name: []const u8,
+
+ pub fn algoName(self: Import) []const u8 {
+ return switch (self) {
+ .hmac => |h| h.name,
+ .ec => |e| e.name,
+ .object => |o| o.name,
+ .name => |n| n,
+ };
+ }
+
+ /// The `namedCurve` if this is an EC import, else empty.
+ pub fn namedCurve(self: Import) []const u8 {
+ return switch (self) {
+ .ec => |e| e.namedCurve,
+ else => "",
+ };
+ }
+};
+
+/// Key material handed to `importKey()`: either a BufferSource (raw/spki/pkcs8)
+/// or a JSON Web Key object (jwk). `bytes` is probed first — a JWK is a plain
+/// object and won't coerce to a TypedArray.
+pub const KeyData = union(enum) {
+ bytes: js.TypedArray(u8),
+ jwk: Jwk,
+
+ /// Minimal JWK fields we read on import. Symmetric ("oct") keys only need
+ /// `kty` and `k`; `d` marks an asymmetric private key. The rest are accepted
+ /// for forward-compatibility.
+ pub const Jwk = struct {
+ kty: []const u8,
+ k: ?[]const u8 = null,
+ d: ?[]const u8 = null,
+ alg: ?[]const u8 = null,
+ use: ?[]const u8 = null,
+ ext: ?bool = null,
+ };
+};
+
+/// Algorithm for `encrypt()` / `decrypt()`. AES-CBC/CTR/GCM share one struct
+/// (fields are mode-specific and optional) and dispatch on `name`; a bare string
+/// is the fallback form.
+pub const Encrypt = union(enum) {
+ params: struct {
+ name: []const u8,
+ iv: ?js.TypedArray(u8) = null,
+ counter: ?js.TypedArray(u8) = null,
+ length: ?u32 = null,
+ additionalData: ?js.TypedArray(u8) = null,
+ tagLength: ?u32 = null,
+ },
+ name: []const u8,
+
+ pub fn algoName(self: Encrypt) []const u8 {
+ return switch (self) {
+ .params => |p| p.name,
+ .name => |n| n,
+ };
+ }
};
/// For `sign()` functionality.
diff --git a/src/browser/webapi/crypto/common.zig b/src/browser/webapi/crypto/common.zig
new file mode 100644
index 00000000..d25758c4
--- /dev/null
+++ b/src/browser/webapi/crypto/common.zig
@@ -0,0 +1,100 @@
+// 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 .
+
+//! Shared helpers for the symmetric SubtleCrypto algorithms (AES, HMAC):
+//! usage-mask validation and the base64url codec used by the JWK format.
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const CryptoKey = @import("../CryptoKey.zig");
+
+/// Maps a usage string to its `CryptoKey.Usages` bit, or null if unknown.
+pub fn usageBit(name: []const u8) ?u8 {
+ const U = CryptoKey.Usages;
+ const map = std.StaticStringMap(u8).initComptime(.{
+ .{ "encrypt", U.encrypt },
+ .{ "decrypt", U.decrypt },
+ .{ "sign", U.sign },
+ .{ "verify", U.verify },
+ .{ "deriveKey", U.deriveKey },
+ .{ "deriveBits", U.deriveBits },
+ .{ "wrapKey", U.wrapKey },
+ .{ "unwrapKey", U.unwrapKey },
+ });
+ return map.get(name);
+}
+
+/// Builds the usage bitmask, rejecting any usage not in `allowed` with
+/// SyntaxError — matching the WebCrypto "Bad usages" failure path. An empty
+/// list is also a SyntaxError (the "Empty usages" path), which is correct for
+/// secret and private keys; public keys permit empty usages.
+pub fn usageMask(allowed: []const []const u8, usages: []const []const u8) error{SyntaxError}!u8 {
+ return usageMaskInner(allowed, usages, true);
+}
+
+/// As `usageMask`, but `reject_empty` controls whether an empty usage list is a
+/// SyntaxError. Pass false for public keys.
+pub fn usageMaskInner(allowed: []const []const u8, usages: []const []const u8, reject_empty: bool) error{SyntaxError}!u8 {
+ var mask: u8 = 0;
+ outer: for (usages) |usage| {
+ for (allowed) |a| {
+ if (std.mem.eql(u8, a, usage)) {
+ mask |= usageBit(a).?;
+ continue :outer;
+ }
+ }
+ return error.SyntaxError;
+ }
+ if (reject_empty and usages.len == 0) {
+ return error.SyntaxError;
+ }
+ return mask;
+}
+
+/// Decodes a base64url (or base64) string, tolerating either alphabet and
+/// optional padding. Returns DataError on malformed input, per the JWK import
+/// rules.
+pub fn base64Decode(allocator: Allocator, input: []const u8) error{ OutOfMemory, DataError }![]u8 {
+ // Normalize to base64url-no-pad: map the standard alphabet's +/ to -_ and
+ // drop any '=' padding so a single decoder handles both forms.
+ const normalized = try allocator.alloc(u8, input.len);
+ var n: usize = 0;
+ for (input) |c| {
+ normalized[n] = switch (c) {
+ '+' => '-',
+ '/' => '_',
+ '=' => continue,
+ else => c,
+ };
+ n += 1;
+ }
+
+ const decoder = std.base64.url_safe_no_pad.Decoder;
+ const size = decoder.calcSizeForSlice(normalized[0..n]) catch return error.DataError;
+ const out = try allocator.alloc(u8, size);
+ decoder.decode(out, normalized[0..n]) catch return error.DataError;
+ return out;
+}
+
+/// Encodes bytes as base64url without padding (the JWK `k` representation).
+pub fn base64Encode(allocator: Allocator, bytes: []const u8) error{OutOfMemory}![]const u8 {
+ const encoder = std.base64.url_safe_no_pad.Encoder;
+ const out = try allocator.alloc(u8, encoder.calcSize(bytes.len));
+ return encoder.encode(out, bytes);
+}
diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig
index cafbff3d..5f8b6986 100644
--- a/src/browser/webapi/net/FormData.zig
+++ b/src/browser/webapi/net/FormData.zig
@@ -72,14 +72,18 @@ pub const Entry = struct {
};
pub fn init(form_: ?*Form, submitter: ?*Element, exec: *const Execution) !*FormData {
- const form = form_ orelse {
- return try exec._factory.create(FormData{
- ._rc = .{},
- ._arena = exec.arena,
- ._entries = .empty,
- });
+ const arena = try exec.getArena(.small, "FormData");
+ errdefer exec.releaseArena(arena);
+
+ const form_data = try arena.create(FormData);
+ form_data.* = .{
+ ._rc = .{},
+ ._arena = arena,
+ ._entries = .empty,
};
+ const form = form_ orelse return form_data;
+
const frame = switch (exec.js.global) {
.frame => |f| f,
.worker => lp.assert(false, "FormData worker form", .{}),
@@ -93,12 +97,10 @@ pub fn init(form_: ?*Form, submitter: ?*Element, exec: *const Execution) !*FormD
form._constructing_entry_list = true;
defer form._constructing_entry_list = false;
- const form_data = try exec._factory.create(FormData{
- ._rc = .{},
- ._arena = exec.arena,
- ._entries = try collectForm(frame.arena, form, submitter, frame),
- });
+ form_data._entries = try collectForm(arena, form, submitter, frame);
+ // Hold a reference on each entry's File for the FormData's lifetime; released
+ // in deinit.
for (form_data._entries.items) |entry| {
switch (entry.value) {
.file => |file| file.acquireRef(),
@@ -123,6 +125,8 @@ pub fn deinit(self: *FormData, page: *Page) void {
else => {},
}
}
+ // Frees the entry list and this FormData itself; do not touch self afterwards.
+ page.releaseArena(self._arena);
}
pub fn releaseRef(self: *FormData, page: *Page) void {
diff --git a/src/sys/libcrypto.zig b/src/sys/libcrypto.zig
index f1833268..0e62df33 100644
--- a/src/sys/libcrypto.zig
+++ b/src/sys/libcrypto.zig
@@ -239,12 +239,88 @@ pub extern fn HMAC(
out_len: *c_uint,
) ?[*]u8;
+pub extern fn PKCS5_PBKDF2_HMAC(
+ password: [*c]const u8,
+ password_len: usize,
+ salt: [*c]const u8,
+ salt_len: usize,
+ iterations: c_uint,
+ digest: *const EVP_MD,
+ key_len: usize,
+ out_key: [*c]u8,
+) c_int;
+
+pub extern fn HKDF(
+ out_key: [*c]u8,
+ out_len: usize,
+ digest: *const EVP_MD,
+ secret: [*c]const u8,
+ secret_len: usize,
+ salt: [*c]const u8,
+ salt_len: usize,
+ info: [*c]const u8,
+ info_len: usize,
+) c_int;
+
pub const X25519_PRIVATE_KEY_LEN = 32;
pub const X25519_PUBLIC_VALUE_LEN = 32;
pub const X25519_SHARED_KEY_LEN = 32;
pub extern fn X25519_keypair(out_public_value: *[32]u8, out_private_key: *[32]u8) void;
+pub const struct_ec_point_st = opaque {};
+pub const EC_POINT = struct_ec_point_st;
+
+pub const EVP_CIPHER = opaque {};
+pub const EVP_CIPHER_CTX = opaque {};
+
+pub extern fn EVP_aes_128_cbc() *const EVP_CIPHER;
+pub extern fn EVP_aes_192_cbc() *const EVP_CIPHER;
+pub extern fn EVP_aes_256_cbc() *const EVP_CIPHER;
+pub extern fn EVP_aes_128_ctr() *const EVP_CIPHER;
+pub extern fn EVP_aes_192_ctr() *const EVP_CIPHER;
+pub extern fn EVP_aes_256_ctr() *const EVP_CIPHER;
+pub extern fn EVP_aes_128_gcm() *const EVP_CIPHER;
+pub extern fn EVP_aes_192_gcm() *const EVP_CIPHER;
+pub extern fn EVP_aes_256_gcm() *const EVP_CIPHER;
+
+pub extern fn EVP_CIPHER_CTX_new() ?*EVP_CIPHER_CTX;
+pub extern fn EVP_CIPHER_CTX_free(ctx: ?*EVP_CIPHER_CTX) void;
+pub extern fn EVP_CIPHER_CTX_ctrl(ctx: *EVP_CIPHER_CTX, command: c_int, arg: c_int, ptr: ?*anyopaque) c_int;
+pub extern fn EVP_CIPHER_CTX_set_padding(ctx: *EVP_CIPHER_CTX, padding: c_int) c_int;
+
+pub extern fn EVP_EncryptInit_ex(ctx: *EVP_CIPHER_CTX, cipher: ?*const EVP_CIPHER, impl: ?*ENGINE, key: [*c]const u8, iv: [*c]const u8) c_int;
+pub extern fn EVP_EncryptUpdate(ctx: *EVP_CIPHER_CTX, out: [*c]u8, out_len: *c_int, in: [*c]const u8, in_len: c_int) c_int;
+pub extern fn EVP_EncryptFinal_ex(ctx: *EVP_CIPHER_CTX, out: [*c]u8, out_len: *c_int) c_int;
+pub extern fn EVP_DecryptInit_ex(ctx: *EVP_CIPHER_CTX, cipher: ?*const EVP_CIPHER, impl: ?*ENGINE, key: [*c]const u8, iv: [*c]const u8) c_int;
+pub extern fn EVP_DecryptUpdate(ctx: *EVP_CIPHER_CTX, out: [*c]u8, out_len: *c_int, in: [*c]const u8, in_len: c_int) c_int;
+pub extern fn EVP_DecryptFinal_ex(ctx: *EVP_CIPHER_CTX, out: [*c]u8, out_len: *c_int) c_int;
+
+// EVP_CIPHER_CTX_ctrl commands for AES-GCM.
+pub const EVP_CTRL_GCM_SET_IVLEN = 0x9;
+pub const EVP_CTRL_GCM_GET_TAG = 0x10;
+pub const EVP_CTRL_GCM_SET_TAG = 0x11;
+
+// EC key type + curve identifiers.
+pub const EVP_PKEY_EC = 408; // NID_X9_62_id_ecPublicKey
+pub const NID_X9_62_prime256v1 = 415; // P-256
+pub const NID_secp384r1 = 715; // P-384
+pub const NID_secp521r1 = 716; // P-521
+
+pub extern fn EVP_PKEY_new() ?*EVP_PKEY;
+pub extern fn EVP_PKEY_id(pkey: *const EVP_PKEY) c_int;
+pub extern fn EVP_PKEY_set1_EC_KEY(pkey: *EVP_PKEY, key: *EC_KEY) c_int;
+
+pub extern fn EC_KEY_new_by_curve_name(nid: c_int) ?*EC_KEY;
+pub extern fn EC_KEY_generate_key(key: *EC_KEY) c_int;
+pub extern fn EC_KEY_free(key: ?*EC_KEY) void;
+pub extern fn EC_KEY_get0_public_key(key: *const EC_KEY) ?*const EC_POINT;
+pub extern fn EC_KEY_set_public_key(key: *EC_KEY, point: *const EC_POINT) c_int;
+
+// DER decoders (advance `inp` past the parsed structure).
+pub extern fn d2i_PUBKEY(out: ?*?*EVP_PKEY, inp: *[*c]const u8, len: c_long) ?*EVP_PKEY;
+pub extern fn d2i_AutoPrivateKey(out: ?*?*EVP_PKEY, inp: *[*c]const u8, len: c_long) ?*EVP_PKEY;
+
pub const NID_X25519 = @as(c_int, 948);
pub const EVP_PKEY_X25519 = NID_X25519;
pub const NID_ED25519 = 949;