mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge branch 'main' into agent
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -16,32 +16,44 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! 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);
|
||||
}
|
||||
|
||||
@@ -16,43 +16,69 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! 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);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
|
||||
124
src/browser/webapi/crypto/KDF.zig
Normal file
124
src/browser/webapi/crypto/KDF.zig
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! 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;
|
||||
}
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
100
src/browser/webapi/crypto/common.zig
Normal file
100
src/browser/webapi/crypto/common.zig
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user