╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │155 const time_cursor = try times.getCursor(hash.hashInt(user_repo_opts.hash, repo_id)) orelse return null; │ │156 return try time_cursor.readUint(); │ │157 } │ │158 + │ │159 +// the account each ssh key last logged into with a password, which the key │ │160 +// logs into from then on. this instance's own state, so it lives outside the │ │161 +// events, in the admin repo. │ │162 +const login_key_to_user_id_key = "haxy/login-key->user-id"; │ │163 + │ │164 +pub const LoginKeyChange = enum { trust, forget }; │ │165 + │ │166 +// let the key with `fingerprint` log `user_id` in, or stop it doing so │ │167 +pub fn setLoginKey( │ │168 + io: std.Io, │ │169 + admin_repo: *rp.Repo(.xit, admin_repo_opts), │ │170 + fingerprint: *const [User.fingerprint_len]u8, │ │171 + user_id: *const [event_id_size]u8, │ │172 + change: LoginKeyChange, │ │173 +) !void { │ │174 + const hash_kind = admin_repo_opts.hash; │ │175 + const Ctx = struct { │ │176 + fingerprint: *const [User.fingerprint_len]u8, │ │177 + user_id: *const [event_id_size]u8, │ │178 + change: LoginKeyChange, │ │179 │ │180 + pub fn run(ctx: @This(), cursor: *AdminDB.Cursor(.read_write)) !void { │ │181 + const moment = try AdminDB.HashMap(.read_write).init(cursor.*); │ │182 + const keys = try AdminDB.HashMap(.read_write).init(try moment.putCursor(hash.hashInt(hash_kind, login_key_to_user_id_key))); │ │183 + const key = hash.hashInt(hash_kind, ctx.fingerprint); │ │184 + switch (ctx.change) { │ │185 + .trust => try keys.put(key, .{ .bytes = ctx.user_id }), │ │186 + // a later login to another account keeps its trust │ │187 + .forget => { │ │188 + const trusted_cursor = try keys.getCursor(key) orelse return error.CancelTransaction; │ │189 + var trusted_id: [event_id_size]u8 = undefined; │ │190 + _ = try trusted_cursor.readBytes(&trusted_id); │ │191 + if (!std.mem.eql(u8, &trusted_id, ctx.user_id)) return error.CancelTransaction; │ │192 + _ = try keys.remove(key); │ │193 + }, │ │194 + } │ │195 + } │ │196 + }; │ │197 + │ │198 + try admin_repo.core.db_file.lock(io, .exclusive); │ │199 + defer admin_repo.core.db_file.unlock(io); │ │200 + │ │201 + const history = try AdminDB.ArrayList(.read_write).init(admin_repo.core.db.rootCursor()); │ │202 + history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ .fingerprint = fingerprint, .user_id = user_id, .change = change }) catch |err| switch (err) {│ │203 + error.CancelTransaction => {}, │ │204 + else => |e| return e, │ │205 + }; │ │206 +} │ │207 + │ │208 +// the account the key with `fingerprint` logs into, from the admin repo's moment │ │209 +pub fn readLoginKeyUser(moment: AdminDB.HashMap(.read_only), fingerprint: *const [User.fingerprint_len]u8) !?[event_id_size]u8 { │ │210 + const keys_cursor = try moment.getCursor(hash.hashInt(admin_repo_opts.hash, login_key_to_user_id_key)) orelse return null; │ │211 + const keys = try AdminDB.HashMap(.read_only).init(keys_cursor); │ │212 + const user_id_cursor = try keys.getCursor(hash.hashInt(admin_repo_opts.hash, fingerprint)) orelse return null; │ │213 + var user_id: [event_id_size]u8 = undefined; │ │214 + _ = try user_id_cursor.readBytes(&user_id); │ │215 + return user_id; │ │216 +} │ │217 + │ │218 // open a user's repo, or null when it's missing │ │219 pub fn openUserRepo(io: std.Io, allocator: std.mem.Allocator, users_dir: []const u8, user_id: *const [event_id_size]u8) !?rp.Repo(.xit, user_repo_opts) { │ │220 const path = try userRepoPath(allocator, users_dir, user_id); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │239 ui_session.io = io; │ │240 ui_session.users_dir = handler.server.users_dir; │ │241 ui_session.dirty_repos = handler.server.dirty_repos; │ │242 + ui_session.ssh_key = try authorizedKeyLine(session_arena.allocator(), sess.public_key);│ │243 + try ui_session.logInWithSshKey(); │ │244 │ │245 var nav = try ui.Nav.init(allocator, &ui_session); │ │246 defer nav.deinit(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │608 ) !evt.CommitAuthor { │ │609 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, user_id)) orelse unreachable;│ │610 return .{ .name = user.event.name, .email = user.event.email }; │ │611 +} │ │612 + │ │613 +// the authorized_keys line of a pubkey blob: its type, then the blob in base64 │ │614 +fn authorizedKeyLine(allocator: std.mem.Allocator, blob: []const u8) ![]const u8 { │ │615 + var reader = std.Io.Reader.fixed(blob); │ │616 + const key_type = try ssh.takeString(&reader, 64); │ │617 + const encoder = std.base64.standard.Encoder; │ │618 + const line = try allocator.alloc(u8, key_type.len + 1 + encoder.calcSize(blob.len)); │ │619 + @memcpy(line[0..key_type.len], key_type); │ │620 + line[key_type.len] = ' '; │ │621 + _ = encoder.encode(line[key_type.len + 1 ..], blob); │ │622 + return line; │ │623 } │ │624 │ │625 fn writeError(sess: *ssh.SessionCtx, comptime msg: []const u8) !void { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │389 /// client logs as `Offering public key: ED25519 SHA256:…`. consumers │ │390 /// can use it as a stable per-key identity. │ │391 fingerprint: [fingerprint_len]u8, │ │392 + /// the SSH wire-format blob of that pubkey │ │393 + public_key: []const u8 = "", │ │394 closed: bool = false, │ │395 // what a SessionReader or SessionWriter actually hit. kept apart so a│ │396 // cleanup write can't replace the cause of a failed read. │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │785 } │ │786 │ │787 fn authAndRunChannels(conn: *Conn, handler: anytype) !void { │ │786 - const fingerprint = try runAuth(conn); │ │787 - try runChannelLayer(conn, &fingerprint, handler); │ │788 + const client_key = try runAuth(conn); │ │789 + defer conn.allocator.free(client_key.blob); │ │790 + try runChannelLayer(conn, &client_key, handler); │ │791 } │ │792 │ │793 /// tell the peer why we're hanging up (RFC 4253 §11.1), best effort. protocol│ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │1686 return out; │ │1687 } │ │1688 │ │1686 -fn runAuth(conn: *Conn) ![fingerprint_len]u8 { │ │1689 +/// the pubkey a client authenticated with │ │1690 +const ClientKey = struct { │ │1691 + fingerprint: [fingerprint_len]u8, │ │1692 + /// owned by the connection's allocator │ │1693 + blob: []const u8, │ │1694 +}; │ │1695 + │ │1696 +fn runAuth(conn: *Conn) !ClientKey { │ │1697 const allocator = conn.allocator; │ │1698 │ │1699 // step 1: service request for ssh-userauth → service accept│ ╰──────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1786 } │ │1787 │ │1788 try conn.writePacket(&.{&[_]u8{SSH_MSG_USERAUTH_SUCCESS}}); │ │1779 - return formatFingerprint(pubkey_blob); │ │1789 + return .{ .fingerprint = formatFingerprint(pubkey_blob), .blob = try allocator.dupe(u8, pubkey_blob) };│ │1790 } │ │1791 } │ │1792 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────╮ │1937 │ │1938 fn runChannelLayer( │ │1939 conn: *Conn, │ │1930 - fingerprint: *const [fingerprint_len]u8,│ │1940 + client_key: *const ClientKey, │ │1941 handler: anytype, │ │1942 ) !void { │ │1943 const allocator = conn.allocator; │ ╰──────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │1965 var sess = SessionCtx{ │ │1966 .conn = conn, │ │1967 .channel = ch, │ │1958 - .fingerprint = fingerprint.*, │ │1968 + .fingerprint = client_key.fingerprint, │ │1969 + .public_key = client_key.blob, │ │1970 }; │ │1971 defer sess.deinit(); │ │1972 handler.handleSession(&sess, request) catch |session_err| {│ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │2288 } │ │2289 }; │ │2290 var handler = Handler{}; │ │2280 - try runChannelLayer(&conn, &@as([fingerprint_len]u8, @splat(0)), &handler); │ │2291 + try runChannelLayer(&conn, &.{ .fingerprint = @splat(0), .blob = "" }, &handler);│ │2292 var output_reader = std.Io.Reader.fixed(writer.buffered()); │ │2293 var receiver = Cipher.init(&key, 0); │ │2294 const confirmation = try receiver.readPacket(allocator, &output_reader); │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯