1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
|
//! Kitty clipboard protocol (OSC 5522) session password grants:
//! requests carrying a granted password skip the permission prompt, and
//! paste events mint one-time passwords.
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;
/// Session password grants, used to skip permission prompts for
/// requests carrying a known pw. Callers can choose to scope these
/// however they want, e.g. Kitty does it per window and the spec
/// doesn't demand anything.
pub const Grants = struct {
entries: std.ArrayListUnmanaged(Entry) = .empty,
const max_entries = 32;
pub const Direction = enum { read, write };
const Entry = struct {
/// Owned by the allocator passed to grant.
pw: []const u8,
read: bool = false,
write: bool = false,
one_time: bool = false,
};
pub fn deinit(self: *Grants, alloc: Allocator) void {
for (self.entries.items) |entry| alloc.free(entry.pw);
self.entries.deinit(alloc);
}
/// Record a grant for pw. An existing grant for the same password
/// gains the new direction.
pub fn grant(
self: *Grants,
alloc: Allocator,
pw: []const u8,
dir: Direction,
one_time: bool,
) Allocator.Error!void {
if (pw.len == 0 or pw.len > max_pw_len) return;
const entry: *Entry = entry: {
if (self.findIndex(pw)) |idx| {
const entry = &self.entries.items[idx];
entry.one_time = entry.one_time and one_time;
break :entry entry;
}
// Evict the oldest grant once full.
if (self.entries.items.len >= max_entries) {
const oldest = self.entries.orderedRemove(0);
alloc.free(oldest.pw);
}
const owned = try alloc.dupe(u8, pw);
errdefer alloc.free(owned);
try self.entries.append(alloc, .{
.pw = owned,
.one_time = one_time,
});
break :entry &self.entries.items[self.entries.items.len - 1];
};
switch (dir) {
.read => entry.read = true,
.write => entry.write = true,
}
}
/// Check whether pw grants the given direction. A one-time grant is
/// consumed by this check even when the direction doesn't match,
/// matching kitty's pop-on-check behavior.
pub fn use(
self: *Grants,
alloc: Allocator,
pw: []const u8,
dir: Direction,
) bool {
if (pw.len == 0) return false;
const idx = self.findIndex(pw) orelse return false;
const entry = &self.entries.items[idx];
const allowed = switch (dir) {
.read => entry.read,
.write => entry.write,
};
if (entry.one_time) {
const removed = self.entries.swapRemove(idx);
alloc.free(removed.pw);
}
return allowed;
}
fn findIndex(self: *const Grants, pw: []const u8) ?usize {
for (self.entries.items, 0..) |*entry, idx| {
if (std.mem.eql(u8, entry.pw, pw)) return idx;
}
return null;
}
};
/// The length of a one-time password generated for paste events.
pub const otp_len = 22;
/// The one-time password alphabet. This matches kitty (alphanumeric
/// without easily-confused characters), but the spec doesn't demand
/// this.
pub const otp_alphabet = "23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
/// 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. 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 sys.randomSecure(io, &raw);
const limit = (std.math.maxInt(u8) + 1) / otp_alphabet.len * otp_alphabet.len;
for (raw) |byte| {
if (byte >= limit) continue;
result[len] = otp_alphabet[byte % otp_alphabet.len];
len += 1;
if (len == result.len) break;
}
}
return result;
}
test "grants: basic grant and use" {
const testing = std.testing;
const alloc = testing.allocator;
var grants: Grants = .{};
defer grants.deinit(alloc);
try testing.expect(!grants.use(alloc, "pw1", .read));
try grants.grant(alloc, "pw1", .read, false);
try testing.expect(grants.use(alloc, "pw1", .read));
// Persistent grants survive use.
try testing.expect(grants.use(alloc, "pw1", .read));
try testing.expect(!grants.use(alloc, "pw1", .write));
}
test "grants: one-time consumed on check" {
const testing = std.testing;
const alloc = testing.allocator;
var grants: Grants = .{};
defer grants.deinit(alloc);
try grants.grant(alloc, "otp", .read, true);
try testing.expect(grants.use(alloc, "otp", .read));
try testing.expect(!grants.use(alloc, "otp", .read));
}
test "grants: one-time consumed even on direction mismatch" {
const testing = std.testing;
const alloc = testing.allocator;
var grants: Grants = .{};
defer grants.deinit(alloc);
try grants.grant(alloc, "otp", .read, true);
try testing.expect(!grants.use(alloc, "otp", .write));
try testing.expect(!grants.use(alloc, "otp", .read));
}
test "grants: directions are independent" {
const testing = std.testing;
const alloc = testing.allocator;
var grants: Grants = .{};
defer grants.deinit(alloc);
try grants.grant(alloc, "pw", .read, false);
try grants.grant(alloc, "pw", .write, false);
try testing.expect(grants.use(alloc, "pw", .read));
try testing.expect(grants.use(alloc, "pw", .write));
}
test "grants: capacity evicts the oldest" {
const testing = std.testing;
const alloc = testing.allocator;
var grants: Grants = .{};
defer grants.deinit(alloc);
var buf: [8]u8 = undefined;
for (0..Grants.max_entries + 1) |i| {
const pw = try std.fmt.bufPrint(&buf, "pw{}", .{i});
try grants.grant(alloc, pw, .read, false);
}
// The oldest grant was evicted; the newest survives.
try testing.expect(!grants.use(alloc, "pw0", .read));
const newest = try std.fmt.bufPrint(&buf, "pw{}", .{Grants.max_entries});
try testing.expect(grants.use(alloc, newest, .read));
}
test "generateOtp: length and alphabet with a real Io" {
const testing = std.testing;
const otp = try generateOtp(testing.io);
try testing.expectEqual(otp_len, otp.len);
for (otp) |c| try testing.expect(std.mem.indexOfScalar(u8, otp_alphabet, c) != null);
// Two passwords don't collide (a repeat would mean no entropy).
const other = try generateOtp(testing.io);
try testing.expect(!std.mem.eql(u8, &otp, &other));
}
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);
}
|