master
1const std = @import("std");
2const Io = std.Io;
3const assert = std.debug.assert;
4const fs = std.fs;
5const mem = std.mem;
6const Allocator = std.mem.Allocator;
7const Bundle = @import("../Bundle.zig");
8
9pub const RescanMacError = Allocator.Error || fs.File.OpenError || fs.File.ReadError || fs.File.SeekError || Bundle.ParseCertError || error{EndOfStream};
10
11pub fn rescanMac(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanMacError!void {
12 cb.bytes.clearRetainingCapacity();
13 cb.map.clearRetainingCapacity();
14
15 const keychain_paths = [2][]const u8{
16 "/System/Library/Keychains/SystemRootCertificates.keychain",
17 "/Library/Keychains/System.keychain",
18 };
19
20 _ = io; // TODO migrate file system to use std.Io
21 for (keychain_paths) |keychain_path| {
22 const bytes = std.fs.cwd().readFileAlloc(keychain_path, gpa, .limited(std.math.maxInt(u32))) catch |err| switch (err) {
23 error.StreamTooLong => return error.FileTooBig,
24 else => |e| return e,
25 };
26 defer gpa.free(bytes);
27
28 var reader: Io.Reader = .fixed(bytes);
29 scanReader(cb, gpa, &reader, now.toSeconds()) catch |err| switch (err) {
30 error.ReadFailed => unreachable, // prebuffered
31 else => |e| return e,
32 };
33 }
34
35 cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
36}
37
38fn scanReader(cb: *Bundle, gpa: Allocator, reader: *Io.Reader, now_sec: i64) !void {
39 const db_header = try reader.takeStruct(ApplDbHeader, .big);
40 assert(mem.eql(u8, &db_header.signature, "kych"));
41
42 reader.seek = db_header.schema_offset;
43
44 const db_schema = try reader.takeStruct(ApplDbSchema, .big);
45
46 var table_list = try gpa.alloc(u32, db_schema.table_count);
47 defer gpa.free(table_list);
48
49 var table_idx: u32 = 0;
50 while (table_idx < table_list.len) : (table_idx += 1) {
51 table_list[table_idx] = try reader.takeInt(u32, .big);
52 }
53
54 for (table_list) |table_offset| {
55 reader.seek = db_header.schema_offset + table_offset;
56
57 const table_header = try reader.takeStruct(TableHeader, .big);
58
59 if (@as(std.c.DB_RECORDTYPE, @enumFromInt(table_header.table_id)) != .X509_CERTIFICATE) {
60 continue;
61 }
62
63 var record_list = try gpa.alloc(u32, table_header.record_count);
64 defer gpa.free(record_list);
65
66 var record_idx: u32 = 0;
67 while (record_idx < record_list.len) : (record_idx += 1) {
68 record_list[record_idx] = try reader.takeInt(u32, .big);
69 }
70
71 for (record_list) |record_offset| {
72 // An offset of zero means that the record is not present.
73 // An offset that is not 4-byte-aligned is invalid.
74 if (record_offset == 0 or record_offset % 4 != 0) continue;
75
76 reader.seek = db_header.schema_offset + table_offset + record_offset;
77
78 const cert_header = try reader.takeStruct(X509CertHeader, .big);
79
80 if (cert_header.cert_size == 0) continue;
81
82 const cert_start: u32 = @intCast(cb.bytes.items.len);
83 const dest_buf = try cb.bytes.addManyAsSlice(gpa, cert_header.cert_size);
84 try reader.readSliceAll(dest_buf);
85
86 try cb.parseCert(gpa, cert_start, now_sec);
87 }
88 }
89}
90
91const ApplDbHeader = extern struct {
92 signature: [4]u8,
93 version: u32,
94 header_size: u32,
95 schema_offset: u32,
96 auth_offset: u32,
97};
98
99const ApplDbSchema = extern struct {
100 schema_size: u32,
101 table_count: u32,
102};
103
104const TableHeader = extern struct {
105 table_size: u32,
106 table_id: u32,
107 record_count: u32,
108 records: u32,
109 indexes_offset: u32,
110 free_list_head: u32,
111 record_numbers_count: u32,
112};
113
114const X509CertHeader = extern struct {
115 record_size: u32,
116 record_number: u32,
117 unknown1: u32,
118 unknown2: u32,
119 cert_size: u32,
120 unknown3: u32,
121 cert_type: u32,
122 cert_encoding: u32,
123 print_name: u32,
124 alias: u32,
125 subject: u32,
126 issuer: u32,
127 serial_number: u32,
128 subject_key_identifier: u32,
129 public_key_hash: u32,
130};