mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-25 16:41:44 +00:00
sys: add secure random override option with a platform default
This commit is contained in:
@@ -124,6 +124,24 @@ typedef bool (*GhosttySysDecodePngFn)(
|
||||
size_t data_len,
|
||||
GhosttySysImage* out);
|
||||
|
||||
/**
|
||||
* Callback type for secure random bytes.
|
||||
*
|
||||
* Fills @p buf with @p len cryptographically secure random bytes. The
|
||||
* library uses this for secrets, so it must be a real CSPRNG (getrandom,
|
||||
* arc4random_buf, BCryptGenRandom, crypto.getRandomValues, ...); a
|
||||
* predictable source is a security hole.
|
||||
*
|
||||
* @param userdata The userdata pointer set via GHOSTTY_SYS_OPT_USERDATA
|
||||
* @param buf Buffer to fill
|
||||
* @param len Number of bytes to fill
|
||||
* @return true if the buffer was filled, false if no entropy is available
|
||||
*/
|
||||
typedef bool (*GhosttySysRandomSecureFn)(
|
||||
void* userdata,
|
||||
uint8_t* buf,
|
||||
size_t len);
|
||||
|
||||
/**
|
||||
* System option identifiers for ghostty_sys_set().
|
||||
*/
|
||||
@@ -165,6 +183,21 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* Input type: GhosttySysLogFn (function pointer, or NULL)
|
||||
*/
|
||||
GHOSTTY_SYS_OPT_LOG = 2,
|
||||
|
||||
/**
|
||||
* Override the secure random source.
|
||||
*
|
||||
* By default the library draws secure random bytes from the
|
||||
* platform (getrandom or arc4random_buf on POSIX, CNG on Windows).
|
||||
* Targets without one, such as wasm32-freestanding, have no default
|
||||
* and operations that need entropy fail with GHOSTTY_IO_ERROR until
|
||||
* this is set. When set,
|
||||
* it is used instead of the platform source on every target. When
|
||||
* cleared (NULL value), the platform default is restored.
|
||||
*
|
||||
* Input type: GhosttySysRandomSecureFn (function pointer, or NULL)
|
||||
*/
|
||||
GHOSTTY_SYS_OPT_RANDOM_SECURE = 3,
|
||||
GHOSTTY_SYS_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySysOption;
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ const vtable: Io.VTable = if (!supported) std.Io.failing.vtable.* else .{
|
||||
.progressParentFile = Io.failingProgressParentFile,
|
||||
|
||||
.random = Io.noRandom,
|
||||
.randomSecure = Io.failingRandomSecure,
|
||||
.randomSecure = randomSecure,
|
||||
|
||||
.now = Io.noNow,
|
||||
.clockResolution = Io.failingClockResolution,
|
||||
@@ -212,6 +212,35 @@ fn swapCancelProtection(
|
||||
|
||||
fn checkCancel(_: ?*anyopaque) Io.Cancelable!void {}
|
||||
|
||||
fn randomSecure(_: ?*anyopaque, buffer: []u8) Io.RandomSecureError!void {
|
||||
if (buffer.len == 0) return;
|
||||
|
||||
// The same sources as `std.Io.Threaded.randomSecure` minus
|
||||
// cancelation and the /dev/urandom fallback: arc4random_buf where
|
||||
// libc provides it (all the BSDs and Darwin, glibc 2.36+), otherwise
|
||||
// the getrandom syscall on Linux. Anything else has no entropy.
|
||||
if (builtin.link_libc and @TypeOf(posix.system.arc4random_buf) != void) {
|
||||
posix.system.arc4random_buf(buffer.ptr, buffer.len);
|
||||
return;
|
||||
}
|
||||
|
||||
if (builtin.os.tag == .linux) {
|
||||
const linux = std.os.linux;
|
||||
var i: usize = 0;
|
||||
while (i < buffer.len) {
|
||||
const rc = linux.getrandom(buffer[i..].ptr, buffer.len - i, 0);
|
||||
switch (linux.errno(rc)) {
|
||||
.SUCCESS => i += rc,
|
||||
.INTR => continue,
|
||||
else => return error.EntropyUnavailable,
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return error.EntropyUnavailable;
|
||||
}
|
||||
|
||||
fn closeFd(fd: posix.fd_t) void {
|
||||
// Never retry close on EINTR: POSIX leaves the fd state unspecified
|
||||
// and Linux always closes it, so retrying risks closing an unrelated
|
||||
@@ -965,6 +994,28 @@ test "Io.Mutex through TinyIo" {
|
||||
test_io.vtable.futexWaitUncancelable(test_io.userdata, &word, 1);
|
||||
}
|
||||
|
||||
test "randomSecure fills with fresh entropy" {
|
||||
if (comptime !supported) return error.SkipZigTest;
|
||||
const tio: TinyIo = .init;
|
||||
const test_io = tio.io();
|
||||
const testing = std.testing;
|
||||
|
||||
var a: [32]u8 = @splat(0);
|
||||
var b: [32]u8 = @splat(0);
|
||||
try test_io.randomSecure(&a);
|
||||
try test_io.randomSecure(&b);
|
||||
|
||||
// Non-zero and non-repeating. A zero fill is what `random` does
|
||||
// without a source, which would make every one-time password the
|
||||
// same; identical draws would mean the same thing.
|
||||
try testing.expect(!std.mem.allEqual(u8, &a, 0));
|
||||
try testing.expect(!std.mem.allEqual(u8, &b, 0));
|
||||
try testing.expect(!std.mem.eql(u8, &a, &b));
|
||||
|
||||
// Zero-length is a no-op.
|
||||
try test_io.randomSecure(a[0..0]);
|
||||
}
|
||||
|
||||
test "unused operations fail gracefully" {
|
||||
if (comptime !supported) return error.SkipZigTest;
|
||||
const tio: TinyIo = .init;
|
||||
|
||||
@@ -23,6 +23,13 @@ pub const DecodePngFn = *const fn (
|
||||
*Image,
|
||||
) callconv(lib.calling_conv) bool;
|
||||
|
||||
/// C: GhosttySysRandomSecureFn
|
||||
pub const RandomSecureFn = *const fn (
|
||||
?*anyopaque,
|
||||
[*]u8,
|
||||
usize,
|
||||
) callconv(lib.calling_conv) bool;
|
||||
|
||||
/// C: GhosttySysLogLevel
|
||||
pub const LogLevel = enum(c_int) {
|
||||
@"error" = 0,
|
||||
@@ -55,12 +62,14 @@ pub const Option = enum(c_int) {
|
||||
userdata = 0,
|
||||
decode_png = 1,
|
||||
log = 2,
|
||||
random_secure = 3,
|
||||
|
||||
pub fn InType(comptime self: Option) type {
|
||||
return switch (self) {
|
||||
.userdata => ?*const anyopaque,
|
||||
.decode_png => ?DecodePngFn,
|
||||
.log => ?LogFn,
|
||||
.random_secure => ?RandomSecureFn,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -71,6 +80,7 @@ const Global = struct {
|
||||
userdata: ?*anyopaque = null,
|
||||
decode_png: ?DecodePngFn = null,
|
||||
log: ?LogFn = null,
|
||||
random_secure: ?RandomSecureFn = null,
|
||||
};
|
||||
|
||||
/// Global state for the C sys interface.
|
||||
@@ -98,6 +108,12 @@ fn decodePngWrapper(
|
||||
};
|
||||
}
|
||||
|
||||
/// Zig-compatible wrapper that calls through to the stored C callback.
|
||||
fn randomSecureWrapper(buffer: []u8) terminal_sys.RandomSecureError!void {
|
||||
const func = global.random_secure orelse return error.EntropyUnavailable;
|
||||
if (!func(global.userdata, buffer.ptr, buffer.len)) return error.EntropyUnavailable;
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
option: Option,
|
||||
value: ?*const anyopaque,
|
||||
@@ -127,6 +143,10 @@ fn setTyped(
|
||||
terminal_sys.decode_png = if (value != null) &decodePngWrapper else null;
|
||||
},
|
||||
.log => global.log = value,
|
||||
.random_secure => {
|
||||
global.random_secure = value;
|
||||
terminal_sys.random_secure = if (value != null) &randomSecureWrapper else null;
|
||||
},
|
||||
}
|
||||
return .success;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const clipboard_command = @import("clipboard_command.zig");
|
||||
const sys = @import("../sys.zig");
|
||||
|
||||
const max_pw_len = clipboard_command.max_pw_len;
|
||||
|
||||
@@ -113,13 +114,14 @@ pub const otp_alphabet = "23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVW
|
||||
/// Generate a one-time password for a paste event.
|
||||
///
|
||||
/// The password is a secret: a program that learns it can read the
|
||||
/// clipboard without a prompt.
|
||||
/// clipboard without a prompt. Entropy comes from `sys.random_secure`
|
||||
/// if set, otherwise from the Io; see `sys.randomSecure`.
|
||||
pub fn generateOtp(io: std.Io) std.Io.RandomSecureError![otp_len]u8 {
|
||||
var result: [otp_len]u8 = undefined;
|
||||
var len: usize = 0;
|
||||
while (len < result.len) {
|
||||
var raw: [2 * otp_len]u8 = undefined;
|
||||
try io.randomSecure(&raw);
|
||||
try sys.randomSecure(io, &raw);
|
||||
const limit = (std.math.maxInt(u8) + 1) / otp_alphabet.len * otp_alphabet.len;
|
||||
for (raw) |byte| {
|
||||
if (byte >= limit) continue;
|
||||
@@ -216,3 +218,21 @@ test "generateOtp: no entropy is an error, never a weak password" {
|
||||
const testing = std.testing;
|
||||
try testing.expectError(error.EntropyUnavailable, generateOtp(std.Io.failing));
|
||||
}
|
||||
|
||||
test "generateOtp: sys override supplies entropy without an Io source" {
|
||||
const testing = std.testing;
|
||||
const S = struct {
|
||||
var counter: u8 = 0;
|
||||
fn fill(buffer: []u8) sys.RandomSecureError!void {
|
||||
for (buffer) |*b| {
|
||||
b.* = counter;
|
||||
counter +%= 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
sys.random_secure = &S.fill;
|
||||
defer sys.random_secure = null;
|
||||
|
||||
const otp = try generateOtp(std.Io.failing);
|
||||
for (otp) |c| try testing.expect(std.mem.indexOfScalar(u8, otp_alphabet, c) != null);
|
||||
}
|
||||
|
||||
@@ -52,3 +52,51 @@ fn decodePngWuffs(
|
||||
.data = result.data,
|
||||
};
|
||||
}
|
||||
|
||||
/// Fill a buffer with cryptographically secure random bytes. If null,
|
||||
/// the terminal's `std.Io` (`randomSecure`) is used. This is an override
|
||||
/// for embedders whose Io has no entropy source (e.g. wasm32-freestanding,
|
||||
/// where TinyIo degrades to `std.Io.failing`) or that want to control
|
||||
/// the source; when set it is used on every target.
|
||||
///
|
||||
/// This is used for secrets, so it must be a real CSPRNG. An error
|
||||
/// makes the operation that needed the entropy fail; nothing falls back
|
||||
/// to weaker randomness.
|
||||
pub var random_secure: ?RandomSecureFn = null;
|
||||
|
||||
pub const RandomSecureError = error{EntropyUnavailable};
|
||||
pub const RandomSecureFn = *const fn ([]u8) RandomSecureError!void;
|
||||
|
||||
/// Fill `buffer` with secure random bytes from `random_secure` if set,
|
||||
/// otherwise from `io`. Every use of secure entropy in the terminal
|
||||
/// package goes through this so the override applies uniformly.
|
||||
pub fn randomSecure(io: std.Io, buffer: []u8) std.Io.RandomSecureError!void {
|
||||
if (random_secure) |func| return func(buffer);
|
||||
return io.randomSecure(buffer);
|
||||
}
|
||||
|
||||
test "randomSecure: override is preferred over the Io" {
|
||||
const testing = std.testing;
|
||||
const S = struct {
|
||||
fn fill(buffer: []u8) RandomSecureError!void {
|
||||
@memset(buffer, 0xAB);
|
||||
}
|
||||
fn fail(_: []u8) RandomSecureError!void {
|
||||
return error.EntropyUnavailable;
|
||||
}
|
||||
};
|
||||
|
||||
// Without the override a failing Io fails.
|
||||
var buf: [8]u8 = @splat(0);
|
||||
try testing.expectError(error.EntropyUnavailable, randomSecure(std.Io.failing, &buf));
|
||||
|
||||
// With it, the Io is never consulted.
|
||||
random_secure = &S.fill;
|
||||
defer random_secure = null;
|
||||
try randomSecure(std.Io.failing, &buf);
|
||||
try testing.expect(std.mem.allEqual(u8, &buf, 0xAB));
|
||||
|
||||
// An override failure surfaces as the Io's error.
|
||||
random_secure = &S.fail;
|
||||
try testing.expectError(error.EntropyUnavailable, randomSecure(testing.io, &buf));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user