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;