mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 15:40:04 +00:00
Merge pull request #1983 from lightpanda-io/nikneym/crypto-changes
Small `SubtleCrypto` refactor
This commit is contained in:
@@ -899,6 +899,7 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/canvas/OffscreenCanvas.zig"),
|
@import("../webapi/canvas/OffscreenCanvas.zig"),
|
||||||
@import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"),
|
@import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"),
|
||||||
@import("../webapi/SubtleCrypto.zig"),
|
@import("../webapi/SubtleCrypto.zig"),
|
||||||
|
@import("../webapi/CryptoKey.zig"),
|
||||||
@import("../webapi/Selection.zig"),
|
@import("../webapi/Selection.zig"),
|
||||||
@import("../webapi/ImageData.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>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -19,16 +19,16 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const lp = @import("lightpanda");
|
const lp = @import("lightpanda");
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
const crypto = @import("../../sys/libcrypto.zig");
|
||||||
const crypto = @import("../../crypto.zig");
|
|
||||||
const DOMException = @import("DOMException.zig");
|
|
||||||
|
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
|
|
||||||
pub fn registerTypes() []const type {
|
const CryptoKey = @import("CryptoKey.zig");
|
||||||
return &.{ SubtleCrypto, CryptoKey };
|
|
||||||
}
|
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
|
/// The SubtleCrypto interface of the Web Crypto API provides a number of low-level
|
||||||
/// cryptographic functions.
|
/// cryptographic functions.
|
||||||
@@ -38,69 +38,36 @@ const SubtleCrypto = @This();
|
|||||||
/// Don't optimize away the type.
|
/// Don't optimize away the type.
|
||||||
_pad: bool = false,
|
_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).
|
/// Generate a new key (for symmetric algorithms) or key pair (for public-key algorithms).
|
||||||
pub fn generateKey(
|
pub fn generateKey(
|
||||||
_: *const SubtleCrypto,
|
_: *const SubtleCrypto,
|
||||||
algorithm: Algorithm,
|
algo: algorithm.Init,
|
||||||
extractable: bool,
|
extractable: bool,
|
||||||
key_usages: []const []const u8,
|
key_usages: []const []const u8,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch {
|
switch (algo) {
|
||||||
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } });
|
.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
|
/// 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.
|
/// Derive a secret key from a master key.
|
||||||
pub fn deriveBits(
|
pub fn deriveBits(
|
||||||
_: *const SubtleCrypto,
|
_: *const SubtleCrypto,
|
||||||
algorithm: Algorithm.DeriveBits,
|
algo: algorithm.Derive,
|
||||||
base_key: *const CryptoKey, // Private key.
|
base_key: *const CryptoKey, // Private key.
|
||||||
length: usize,
|
length: usize,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
return switch (algorithm) {
|
return switch (algo) {
|
||||||
.ecdh_or_x25519 => |p| {
|
.ecdh_or_x25519 => |params| {
|
||||||
const name = p.name;
|
const name = params.name;
|
||||||
if (std.mem.eql(u8, name, "X25519")) {
|
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")) {
|
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.
|
/// Generate a digital signature.
|
||||||
pub fn sign(
|
pub fn sign(
|
||||||
_: *const SubtleCrypto,
|
_: *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
|
/// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#algorithm
|
||||||
algorithm: SignatureAlgorithm,
|
algo: algorithm.Sign,
|
||||||
key: *CryptoKey,
|
key: *CryptoKey,
|
||||||
data: []const u8, // ArrayBuffer.
|
data: []const u8, // ArrayBuffer.
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
return switch (key._type) {
|
return switch (key._type) {
|
||||||
.hmac => {
|
// Call sign for HMAC.
|
||||||
// Verify algorithm.
|
.hmac => return HMAC.sign(algo, key, data, page),
|
||||||
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);
|
|
||||||
},
|
|
||||||
else => {
|
else => {
|
||||||
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
|
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
|
||||||
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
@@ -206,452 +150,43 @@ pub fn sign(
|
|||||||
/// Verify a digital signature.
|
/// Verify a digital signature.
|
||||||
pub fn verify(
|
pub fn verify(
|
||||||
_: *const SubtleCrypto,
|
_: *const SubtleCrypto,
|
||||||
algorithm: SignatureAlgorithm,
|
algo: algorithm.Sign,
|
||||||
key: *const CryptoKey,
|
key: *const CryptoKey,
|
||||||
signature: []const u8, // ArrayBuffer.
|
signature: []const u8, // ArrayBuffer.
|
||||||
data: []const u8, // ArrayBuffer.
|
data: []const u8, // ArrayBuffer.
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
if (!algorithm.isHMAC()) {
|
if (!algo.isHMAC()) {
|
||||||
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return switch (key._type) {
|
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 } }),
|
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.?;
|
const local = page.js.local.?;
|
||||||
if (algorithm.len > 10) {
|
|
||||||
|
if (algo.len > 10) {
|
||||||
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
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.
|
const normalized = std.ascii.upperString(&page.buf, algo);
|
||||||
fn findDigest(name: []const u8) error{Invalid}!*const crypto.EVP_MD {
|
const digest_type = crypto.findDigest(normalized) catch {
|
||||||
if (std.mem.eql(u8, "SHA-256", name)) {
|
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
return crypto.EVP_sha256();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, "SHA-384", name)) {
|
|
||||||
return crypto.EVP_sha384();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, "SHA-512", name)) {
|
|
||||||
return crypto.EVP_sha512();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, "SHA-1", name)) {
|
|
||||||
return crypto.EVP_sha1();
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
const KeyOrPair = union(enum) { key: *CryptoKey, pair: CryptoKeyPair };
|
|
||||||
|
|
||||||
/// https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair
|
|
||||||
const CryptoKeyPair = struct {
|
|
||||||
privateKey: *CryptoKey,
|
|
||||||
publicKey: *CryptoKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Represents a cryptographic key obtained from one of the SubtleCrypto methods
|
|
||||||
/// generateKey(), deriveKey(), importKey(), or unwrapKey().
|
|
||||||
pub const CryptoKey = struct {
|
|
||||||
/// Algorithm being used.
|
|
||||||
_type: Type,
|
|
||||||
/// Whether the key is extractable.
|
|
||||||
_extractable: bool,
|
|
||||||
/// Bit flags of `usages`; see `Usages` type.
|
|
||||||
_usages: u8,
|
|
||||||
/// Raw bytes of key.
|
|
||||||
_key: []const u8,
|
|
||||||
/// Different algorithms may use different data structures;
|
|
||||||
/// this union can be used for such situations. Active field is understood
|
|
||||||
/// from `_type`.
|
|
||||||
_vary: extern union {
|
|
||||||
/// Used by HMAC.
|
|
||||||
digest: *const crypto.EVP_MD,
|
|
||||||
/// Used by asymmetric algorithms (X25519, Ed25519).
|
|
||||||
pkey: *crypto.EVP_PKEY,
|
|
||||||
},
|
|
||||||
|
|
||||||
pub const Type = enum(u8) { hmac, rsa, x25519 };
|
|
||||||
|
|
||||||
/// Changing the names of fields would affect bitmask creation.
|
|
||||||
pub const Usages = struct {
|
|
||||||
// zig fmt: off
|
|
||||||
pub const encrypt = 0x001;
|
|
||||||
pub const decrypt = 0x002;
|
|
||||||
pub const sign = 0x004;
|
|
||||||
pub const verify = 0x008;
|
|
||||||
pub const deriveKey = 0x010;
|
|
||||||
pub const deriveBits = 0x020;
|
|
||||||
pub const wrapKey = 0x040;
|
|
||||||
pub const unwrapKey = 0x080;
|
|
||||||
// zig fmt: on
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(
|
const bytes = data.values;
|
||||||
algorithm: Algorithm,
|
const out = page.buf[0..crypto.EVP_MAX_MD_SIZE];
|
||||||
extractable: bool,
|
var out_size: c_uint = 0;
|
||||||
key_usages: []const []const u8,
|
const result = crypto.EVP_Digest(bytes.ptr, bytes.len, out, &out_size, digest_type, null);
|
||||||
page: *Page,
|
lp.assert(result == 1, "SubtleCrypto.digest", .{ .algo = algo });
|
||||||
) !KeyOrPair {
|
|
||||||
return switch (algorithm) {
|
|
||||||
.hmac_key_gen => |hmac| initHMAC(hmac, extractable, key_usages, page),
|
|
||||||
.name => |name| {
|
|
||||||
if (std.mem.eql(u8, "X25519", name)) {
|
|
||||||
return initX25519(extractable, key_usages, page);
|
|
||||||
}
|
|
||||||
log.warn(.not_implemented, "CryptoKey.init", .{ .name = name });
|
|
||||||
return error.NotSupported;
|
|
||||||
},
|
|
||||||
.object => |object| {
|
|
||||||
// Ditto.
|
|
||||||
const name = object.name;
|
|
||||||
if (std.mem.eql(u8, "X25519", name)) {
|
|
||||||
return initX25519(extractable, key_usages, page);
|
|
||||||
}
|
|
||||||
log.warn(.not_implemented, "CryptoKey.init", .{ .name = name });
|
|
||||||
return error.NotSupported;
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
log.warn(.not_implemented, "CryptoKey.init", .{ .algorithm = algorithm });
|
|
||||||
return error.NotSupported;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn canSign(self: *const CryptoKey) bool {
|
return local.resolvePromise(js.ArrayBuffer{ .values = out[0..out_size] });
|
||||||
return self._usages & Usages.sign != 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
inline fn canVerify(self: *const CryptoKey) bool {
|
|
||||||
return self._usages & Usages.verify != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn canDeriveBits(self: *const CryptoKey) bool {
|
|
||||||
return self._usages & Usages.deriveBits != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn canExportKey(self: *const CryptoKey) bool {
|
|
||||||
return self._extractable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Only valid for HMAC.
|
|
||||||
inline fn getDigest(self: *const CryptoKey) *const crypto.EVP_MD {
|
|
||||||
return self._vary.digest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Only valid for asymmetric algorithms (X25519, Ed25519).
|
|
||||||
inline fn getKeyObject(self: *const CryptoKey) *crypto.EVP_PKEY {
|
|
||||||
return self._vary.pkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMAC.
|
|
||||||
|
|
||||||
fn initHMAC(
|
|
||||||
algorithm: Algorithm.HmacKeyGen,
|
|
||||||
extractable: bool,
|
|
||||||
key_usages: []const []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !KeyOrPair {
|
|
||||||
const hash = switch (algorithm.hash) {
|
|
||||||
.string => |str| str,
|
|
||||||
.object => |obj| obj.name,
|
|
||||||
};
|
|
||||||
// Find digest.
|
|
||||||
const d = try findDigest(hash);
|
|
||||||
|
|
||||||
// We need at least a single usage.
|
|
||||||
if (key_usages.len == 0) {
|
|
||||||
return error.SyntaxError;
|
|
||||||
}
|
|
||||||
// Calculate usages mask.
|
|
||||||
const decls = @typeInfo(Usages).@"struct".decls;
|
|
||||||
var usages_mask: u8 = 0;
|
|
||||||
iter_usages: for (key_usages) |usage| {
|
|
||||||
inline for (decls) |decl| {
|
|
||||||
if (std.mem.eql(u8, decl.name, usage)) {
|
|
||||||
usages_mask |= @field(Usages, decl.name);
|
|
||||||
continue :iter_usages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Unknown usage if got here.
|
|
||||||
return error.SyntaxError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const block_size: usize = blk: {
|
|
||||||
// Caller provides this in bits, not bytes.
|
|
||||||
if (algorithm.length) |length| {
|
|
||||||
break :blk length / 8;
|
|
||||||
}
|
|
||||||
// Prefer block size of the hash function instead.
|
|
||||||
break :blk crypto.EVP_MD_block_size(d);
|
|
||||||
};
|
|
||||||
|
|
||||||
const key = try page.arena.alloc(u8, block_size);
|
|
||||||
errdefer page.arena.free(key);
|
|
||||||
|
|
||||||
// HMAC is simply CSPRNG.
|
|
||||||
const res = crypto.RAND_bytes(key.ptr, key.len);
|
|
||||||
lp.assert(res == 1, "SubtleCrypto.initHMAC", .{ .res = res });
|
|
||||||
|
|
||||||
const crypto_key = try page._factory.create(CryptoKey{
|
|
||||||
._type = .hmac,
|
|
||||||
._extractable = extractable,
|
|
||||||
._usages = usages_mask,
|
|
||||||
._key = key,
|
|
||||||
._vary = .{ .digest = d },
|
|
||||||
});
|
|
||||||
|
|
||||||
return .{ .key = crypto_key };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn signHMAC(self: *const CryptoKey, data: []const u8, page: *Page) !js.ArrayBuffer {
|
|
||||||
if (!self.canSign()) {
|
|
||||||
return error.InvalidAccessError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = try page.call_arena.alloc(u8, crypto.EVP_MD_size(self.getDigest()));
|
|
||||||
errdefer page.call_arena.free(buffer);
|
|
||||||
var out_len: u32 = 0;
|
|
||||||
// Try to sign.
|
|
||||||
const signed = crypto.HMAC(
|
|
||||||
self.getDigest(),
|
|
||||||
@ptrCast(self._key.ptr),
|
|
||||||
self._key.len,
|
|
||||||
data.ptr,
|
|
||||||
data.len,
|
|
||||||
buffer.ptr,
|
|
||||||
&out_len,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (signed != null) {
|
|
||||||
return js.ArrayBuffer{ .values = buffer[0..out_len] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not DOM exception, failed on our side.
|
|
||||||
return error.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verifyHMAC(
|
|
||||||
self: *const CryptoKey,
|
|
||||||
signature: []const u8,
|
|
||||||
data: []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !js.Promise {
|
|
||||||
if (!self.canVerify()) {
|
|
||||||
return error.InvalidAccessError;
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer: [crypto.EVP_MAX_MD_BLOCK_SIZE]u8 = undefined;
|
|
||||||
var out_len: u32 = 0;
|
|
||||||
// Try to sign.
|
|
||||||
const signed = crypto.HMAC(
|
|
||||||
self.getDigest(),
|
|
||||||
@ptrCast(self._key.ptr),
|
|
||||||
self._key.len,
|
|
||||||
data.ptr,
|
|
||||||
data.len,
|
|
||||||
&buffer,
|
|
||||||
&out_len,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (signed != null) {
|
|
||||||
// CRYPTO_memcmp compare in constant time so prohibits time-based attacks.
|
|
||||||
const res = crypto.CRYPTO_memcmp(signed, @ptrCast(signature.ptr), signature.len);
|
|
||||||
return page.js.local.?.resolvePromise(res == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return page.js.local.?.resolvePromise(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// X25519.
|
|
||||||
|
|
||||||
/// Create a pair of X25519.
|
|
||||||
fn initX25519(
|
|
||||||
extractable: bool,
|
|
||||||
key_usages: []const []const u8,
|
|
||||||
page: *Page,
|
|
||||||
) !KeyOrPair {
|
|
||||||
// This code has too many allocations here and there, might be nice to
|
|
||||||
// gather them together with a single alloc call. Not sure if factory
|
|
||||||
// pattern is suitable for it though.
|
|
||||||
|
|
||||||
// Calculate usages; only matters for private key.
|
|
||||||
// Only deriveKey() and deriveBits() be used for X25519.
|
|
||||||
if (key_usages.len == 0) {
|
|
||||||
return error.SyntaxError;
|
|
||||||
}
|
|
||||||
var mask: u8 = 0;
|
|
||||||
iter_usages: for (key_usages) |usage| {
|
|
||||||
inline for ([_][]const u8{ "deriveKey", "deriveBits" }) |name| {
|
|
||||||
if (std.mem.eql(u8, name, usage)) {
|
|
||||||
mask |= @field(Usages, name);
|
|
||||||
continue :iter_usages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Unknown usage if got here.
|
|
||||||
return error.SyntaxError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const public_value = try page.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN);
|
|
||||||
errdefer page.arena.free(public_value);
|
|
||||||
|
|
||||||
const private_key = try page.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN);
|
|
||||||
errdefer page.arena.free(private_key);
|
|
||||||
|
|
||||||
// There's no info about whether this can fail; so I assume it cannot.
|
|
||||||
crypto.X25519_keypair(@ptrCast(public_value), @ptrCast(private_key));
|
|
||||||
|
|
||||||
// Create EVP_PKEY for public key.
|
|
||||||
// Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome
|
|
||||||
// prefer not to, yet BoringSSL added it and recommends instead of what
|
|
||||||
// we're doing currently.
|
|
||||||
const public_pkey = crypto.EVP_PKEY_new_raw_public_key(
|
|
||||||
crypto.EVP_PKEY_X25519,
|
|
||||||
null,
|
|
||||||
public_value.ptr,
|
|
||||||
public_value.len,
|
|
||||||
);
|
|
||||||
if (public_pkey == null) {
|
|
||||||
return error.OutOfMemory;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create EVP_PKEY for private key.
|
|
||||||
// Seems we can use `EVP_PKEY_from_raw_private_key` for this, Chrome
|
|
||||||
// prefer not to, yet BoringSSL added it and recommends instead of what
|
|
||||||
// we're doing currently.
|
|
||||||
const private_pkey = crypto.EVP_PKEY_new_raw_private_key(
|
|
||||||
crypto.EVP_PKEY_X25519,
|
|
||||||
null,
|
|
||||||
private_key.ptr,
|
|
||||||
private_key.len,
|
|
||||||
);
|
|
||||||
if (private_pkey == null) {
|
|
||||||
return error.OutOfMemory;
|
|
||||||
}
|
|
||||||
|
|
||||||
const private = try page._factory.create(CryptoKey{
|
|
||||||
._type = .x25519,
|
|
||||||
._extractable = extractable,
|
|
||||||
._usages = mask,
|
|
||||||
._key = private_key,
|
|
||||||
._vary = .{ .pkey = private_pkey.? },
|
|
||||||
});
|
|
||||||
errdefer page._factory.destroy(private);
|
|
||||||
|
|
||||||
const public = try page._factory.create(CryptoKey{
|
|
||||||
._type = .x25519,
|
|
||||||
// Public keys are always extractable.
|
|
||||||
._extractable = true,
|
|
||||||
// Always empty for public key.
|
|
||||||
._usages = 0,
|
|
||||||
._key = public_value,
|
|
||||||
._vary = .{ .pkey = public_pkey.? },
|
|
||||||
});
|
|
||||||
errdefer page._factory.destroy(public);
|
|
||||||
|
|
||||||
return .{ .pair = .{ .privateKey = private, .publicKey = public } };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deriveBitsX25519(
|
|
||||||
private: *const CryptoKey,
|
|
||||||
public: *const CryptoKey,
|
|
||||||
length_in_bits: usize,
|
|
||||||
page: *Page,
|
|
||||||
) !js.ArrayBuffer {
|
|
||||||
if (!private.canDeriveBits()) {
|
|
||||||
return error.InvalidAccessError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maybe_ctx = crypto.EVP_PKEY_CTX_new(private.getKeyObject(), null);
|
|
||||||
if (maybe_ctx) |ctx| {
|
|
||||||
// Context is valid, free it on failure.
|
|
||||||
errdefer crypto.EVP_PKEY_CTX_free(ctx);
|
|
||||||
|
|
||||||
// Init derive operation and set public key as peer.
|
|
||||||
if (crypto.EVP_PKEY_derive_init(ctx) != 1 or
|
|
||||||
crypto.EVP_PKEY_derive_set_peer(ctx, public.getKeyObject()) != 1)
|
|
||||||
{
|
|
||||||
// Failed on our end.
|
|
||||||
return error.Internal;
|
|
||||||
}
|
|
||||||
|
|
||||||
const derived_key = try page.call_arena.alloc(u8, 32);
|
|
||||||
errdefer page.call_arena.free(derived_key);
|
|
||||||
|
|
||||||
var out_key_len: usize = derived_key.len;
|
|
||||||
const result = crypto.EVP_PKEY_derive(ctx, derived_key.ptr, &out_key_len);
|
|
||||||
if (result != 1) {
|
|
||||||
// Failed on our end.
|
|
||||||
return error.Internal;
|
|
||||||
}
|
|
||||||
// Sanity check.
|
|
||||||
lp.assert(derived_key.len == out_key_len, "SubtleCrypto.deriveBitsX25519", .{});
|
|
||||||
|
|
||||||
// Length is in bits, convert to byte length.
|
|
||||||
const length = (length_in_bits / 8) + (7 + (length_in_bits % 8)) / 8;
|
|
||||||
// Truncate the slice to specified length.
|
|
||||||
// Same as `derived_key`.
|
|
||||||
const tailored = blk: {
|
|
||||||
if (length > derived_key.len) {
|
|
||||||
return error.LengthTooLong;
|
|
||||||
}
|
|
||||||
break :blk derived_key[0..length];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Zero any "unused bits" in the final byte.
|
|
||||||
const remainder_bits: u3 = @intCast(length_in_bits % 8);
|
|
||||||
if (remainder_bits != 0) {
|
|
||||||
tailored[tailored.len - 1] &= ~(@as(u8, 0xFF) >> remainder_bits);
|
|
||||||
}
|
|
||||||
|
|
||||||
return js.ArrayBuffer{ .values = tailored };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failed on our end.
|
|
||||||
return error.Internal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const JsApi = struct {
|
|
||||||
pub const bridge = js.Bridge(CryptoKey);
|
|
||||||
|
|
||||||
pub const Meta = struct {
|
|
||||||
pub const name = "CryptoKey";
|
|
||||||
|
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(SubtleCrypto);
|
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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const crypto = @import("../crypto.zig");
|
const crypto = @import("../sys/libcrypto.zig");
|
||||||
|
|
||||||
const Http = @import("../network/http.zig");
|
const Http = @import("../network/http.zig");
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
//! libcrypto utilities we use throughout browser.
|
//! libcrypto utilities we use throughout browser.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
@@ -203,6 +221,7 @@ pub extern fn EVP_sha256() *const EVP_MD;
|
|||||||
pub extern fn EVP_sha384() *const EVP_MD;
|
pub extern fn EVP_sha384() *const EVP_MD;
|
||||||
pub extern fn EVP_sha512() *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 const EVP_MAX_MD_BLOCK_SIZE = 128;
|
||||||
|
|
||||||
pub extern fn EVP_MD_size(md: ?*const EVP_MD) usize;
|
pub extern fn EVP_MD_size(md: ?*const EVP_MD) usize;
|
||||||
@@ -242,7 +261,29 @@ pub extern fn EVP_PKEY_free(pkey: ?*EVP_PKEY) void;
|
|||||||
|
|
||||||
pub extern fn EVP_DigestSignInit(ctx: ?*EVP_MD_CTX, pctx: ?*?*EVP_PKEY_CTX, typ: ?*const EVP_MD, e: ?*ENGINE, pkey: ?*EVP_PKEY) c_int;
|
pub extern fn EVP_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_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_new() ?*EVP_MD_CTX;
|
||||||
pub extern fn EVP_MD_CTX_free(ctx: ?*EVP_MD_CTX) void;
|
pub extern fn EVP_MD_CTX_free(ctx: ?*EVP_MD_CTX) void;
|
||||||
pub const struct_evp_md_ctx_st = opaque {};
|
pub const struct_evp_md_ctx_st = opaque {};
|
||||||
pub const EVP_MD_CTX = struct_evp_md_ctx_st;
|
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