lib/sys/src/x11/auth.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("../root.zig");
3
4 const env = sys.env;
5 const fs = sys.fs;
6
7 pub const cookie_name = "MIT-MAGIC-COOKIE-1";
8 pub const cookie_length = 16;
9 pub const maximum_authority_file_byte_count = 64 * 1024;
10
11 const family_local: u16 = 256;
12 const family_wild: u16 = 65535;
13
14 pub const Cookie = struct {
15 data: [cookie_length]u8,
16 };
17
18 const Record = struct {
19 family: u16,
20 address: []const u8,
21 number: []const u8,
22 name: []const u8,
23 data: []const u8,
24 };
25
26 const Reader = struct {
27 bytes: []const u8,
28 index: usize = 0,
29
30 fn u16be(self: *Reader) ?u16 {
31 if (self.index + 2 > self.bytes.len) return null;
32 const value = std.mem.readInt(u16, self.bytes[self.index..][0..2], .big);
33 self.index += 2;
34 return value;
35 }
36
37 fn counted(self: *Reader) ?[]const u8 {
38 const len = self.u16be() orelse return null;
39 if (self.index + len > self.bytes.len) return null;
40 const slice = self.bytes[self.index .. self.index + len];
41 self.index += len;
42 return slice;
43 }
44
45 fn record(self: *Reader) ?Record {
46 const family = self.u16be() orelse return null;
47 const address = self.counted() orelse return null;
48 const number = self.counted() orelse return null;
49 const name = self.counted() orelse return null;
50 const data = self.counted() orelse return null;
51 return .{
52 .family = family,
53 .address = address,
54 .number = number,
55 .name = name,
56 .data = data,
57 };
58 }
59 };
60
61 pub fn findCookie(bytes: []const u8, display: u32) ?Cookie {
62 var number_buffer: [10]u8 = undefined;
63 const number = std.fmt.bufPrint(&number_buffer, "{d}", .{display}) catch return null;
64
65 var reader = Reader{ .bytes = bytes };
66 while (reader.record()) |record| {
67 if (record.family != family_local and record.family != family_wild) continue;
68 if (record.number.len != 0 and !std.mem.eql(u8, record.number, number)) continue;
69 if (!std.mem.eql(u8, record.name, cookie_name)) continue;
70 if (record.data.len != cookie_length) continue;
71 var cookie: Cookie = undefined;
72 @memcpy(&cookie.data, record.data);
73 return cookie;
74 }
75 return null;
76 }
77
78 pub fn loadCookie(storage: []u8, display: u32) ?Cookie {
79 std.debug.assert(storage.len >= maximum_authority_file_byte_count);
80 var path_buffer: [512]u8 = undefined;
81 const path = authorityPath(&path_buffer) orelse return null;
82 var file = fs.cwd().openFile(fs.debugIo(), path, .{}) catch return null;
83 defer file.close(fs.debugIo());
84 const bytes = readAuthorityFile(file, storage[0..maximum_authority_file_byte_count]) orelse
85 return null;
86 return findCookie(bytes, display);
87 }
88
89 fn readAuthorityFile(file: std.Io.File, storage: []u8) ?[]const u8 {
90 var filled: usize = 0;
91 while (filled < storage.len) {
92 const count = fs.readHandle(file, storage[filled..]) catch return null;
93 if (count == 0) return storage[0..filled];
94 filled += count;
95 }
96 var probe: [1]u8 = undefined;
97 if ((fs.readHandle(file, &probe) catch return null) != 0) return null;
98 return storage;
99 }
100
101 fn authorityPath(buffer: []u8) ?[]const u8 {
102 if (env.get("XAUTHORITY")) |explicit| {
103 if (explicit.len == 0 or explicit.len > buffer.len) return null;
104 @memcpy(buffer[0..explicit.len], explicit);
105 return buffer[0..explicit.len];
106 }
107 const home = env.get("HOME") orelse return null;
108 return std.fmt.bufPrint(buffer, "{s}/.Xauthority", .{home}) catch null;
109 }
110
111 fn appendU16Be(list: *std.ArrayList(u8), allocator: std.mem.Allocator, value: u16) !void {
112 var scratch: [2]u8 = undefined;
113 std.mem.writeInt(u16, &scratch, value, .big);
114 try list.appendSlice(allocator, &scratch);
115 }
116
117 fn appendCounted(list: *std.ArrayList(u8), allocator: std.mem.Allocator, bytes: []const u8) !void {
118 try appendU16Be(list, allocator, @intCast(bytes.len));
119 try list.appendSlice(allocator, bytes);
120 }
121
122 fn appendRecord(
123 list: *std.ArrayList(u8),
124 allocator: std.mem.Allocator,
125 family: u16,
126 address: []const u8,
127 number: []const u8,
128 name: []const u8,
129 data: []const u8,
130 ) !void {
131 try appendU16Be(list, allocator, family);
132 try appendCounted(list, allocator, address);
133 try appendCounted(list, allocator, number);
134 try appendCounted(list, allocator, name);
135 try appendCounted(list, allocator, data);
136 }
137
138 test "findCookie matches the display record" {
139 const allocator = std.testing.allocator;
140 var bytes: std.ArrayList(u8) = .empty;
141 defer bytes.deinit(allocator);
142
143 const other = @as([cookie_length]u8, @splat(0xAA));
144 const wanted = @as([cookie_length]u8, @splat(0x5A));
145 try appendRecord(&bytes, allocator, family_local, "host", "0", cookie_name, &other);
146 try appendRecord(&bytes, allocator, family_local, "host", "7", cookie_name, &wanted);
147
148 const cookie = findCookie(bytes.items, 7) orelse return error.TestExpectedCookie;
149 try std.testing.expectEqualSlices(u8, &wanted, &cookie.data);
150 }
151
152 test "findCookie skips foreign families and short data" {
153 const allocator = std.testing.allocator;
154 var bytes: std.ArrayList(u8) = .empty;
155 defer bytes.deinit(allocator);
156
157 const cookie = @as([cookie_length]u8, @splat(0x11));
158 try appendRecord(&bytes, allocator, 0, "10.0.0.1", "0", cookie_name, &cookie);
159 try appendRecord(&bytes, allocator, family_local, "host", "0", cookie_name, cookie[0..4]);
160 try std.testing.expect(findCookie(bytes.items, 0) == null);
161 }
162
163 test "findCookie accepts wildcard records and truncated files" {
164 const allocator = std.testing.allocator;
165 var bytes: std.ArrayList(u8) = .empty;
166 defer bytes.deinit(allocator);
167
168 const cookie = @as([cookie_length]u8, @splat(0x77));
169 try appendRecord(&bytes, allocator, family_wild, "", "", cookie_name, &cookie);
170 try std.testing.expect(findCookie(bytes.items, 3) != null);
171
172 try std.testing.expect(findCookie(bytes.items[0 .. bytes.items.len - 1], 99) == null);
173 }
174
175 test "authority file read accepts exact storage and rejects max plus one" {
176 const io = std.Options.debug_io;
177 var tmp = std.testing.tmpDir(.{});
178 defer tmp.cleanup();
179
180 var exact = try tmp.dir.createFile(io, "exact", .{});
181 try fs.writeHandleAll(exact, "four");
182 exact.close(io);
183 var exact_reader = try tmp.dir.openFile(io, "exact", .{});
184 defer exact_reader.close(io);
185 var storage: [4]u8 = undefined;
186 try std.testing.expectEqualStrings(
187 "four",
188 readAuthorityFile(exact_reader, &storage).?,
189 );
190
191 var oversized = try tmp.dir.createFile(io, "oversized", .{});
192 try fs.writeHandleAll(oversized, "fives");
193 oversized.close(io);
194 var oversized_reader = try tmp.dir.openFile(io, "oversized", .{});
195 defer oversized_reader.close(io);
196 try std.testing.expect(readAuthorityFile(oversized_reader, &storage) == null);
197 }