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