1
use std::{
2
    io::{self, IoSlice, Read},
3
    mem::MaybeUninit,
4
    os::{fd::AsFd, unix::fs::PermissionsExt},
5
    path::PathBuf,
6
    sync::LazyLock,
7
};
8

            
9
use rustix::{
10
    fs::{MemfdFlags, SealFlags},
11
    net::{SendAncillaryBuffer, SendAncillaryMessage, SendFlags},
12
};
13

            
14
const HELPER_TIMEOUT_SECS: u64 = 120;
15
const BINARY_NAME: &str = env!("CARGO_BIN_NAME");
16

            
17
pub static SOCKET_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
18
    let uid = rustix::process::getuid().as_raw();
19
    PathBuf::from(format!("/run/user/{uid}/oo7-daemon-login.sock"))
20
});
21

            
22
fn main() {
23
    tracing_subscriber::fmt::init();
24

            
25
    let mut secret = vec![];
26
    io::stdin()
27
        .lock()
28
        .read_to_end(&mut secret)
29
        .unwrap_or_else(|e| {
30
            tracing::error!("Failed to read secret from stdin: {e}");
31
            std::process::exit(1);
32
        });
33

            
34
    if secret.is_empty() {
35
        tracing::error!("No secret provided on stdin");
36
        std::process::exit(1);
37
    }
38

            
39
    tracing::info!("Starting {BINARY_NAME}");
40

            
41
    let socket_path = &*SOCKET_PATH;
42

            
43
    let _ = std::fs::remove_file(socket_path);
44

            
45
    let listener = std::os::unix::net::UnixListener::bind(socket_path).unwrap_or_else(|e| {
46
        tracing::error!("Failed to bind {}: {e}", socket_path.display());
47
        std::process::exit(1);
48
    });
49

            
50
    std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o600)).unwrap_or_else(
51
        |e| {
52
            tracing::error!("Failed to set socket permissions: {e}");
53
            std::process::exit(1);
54
        },
55
    );
56

            
57
    let uid = rustix::process::getuid();
58

            
59
    listener.set_nonblocking(false).ok();
60
    tracing::info!("Listening on {}", socket_path.display());
61

            
62
    // Poll with timeout
63
    let timeout = rustix::event::Timespec {
64
        tv_sec: HELPER_TIMEOUT_SECS as i64,
65
        tv_nsec: 0,
66
    };
67
    let poll_ret = rustix::event::poll(
68
        &mut [rustix::event::PollFd::from_borrowed_fd(
69
            listener.as_fd(),
70
            rustix::event::PollFlags::IN,
71
        )],
72
        Some(&timeout),
73
    );
74
    match poll_ret {
75
        Ok(0) => {
76
            tracing::info!("Timed out after {HELPER_TIMEOUT_SECS}s, no daemon connected");
77
            let _ = std::fs::remove_file(socket_path);
78
            std::process::exit(0);
79
        }
80
        Err(e) => {
81
            tracing::error!("Poll failed: {e}");
82
            let _ = std::fs::remove_file(socket_path);
83
            std::process::exit(1);
84
        }
85
        _ => {}
86
    }
87

            
88
    let (stream, _addr) = listener.accept().unwrap_or_else(|e| {
89
        tracing::error!("Failed to accept connection: {e}");
90
        let _ = std::fs::remove_file(socket_path);
91
        std::process::exit(1);
92
    });
93

            
94
    let peer_cred = rustix::net::sockopt::socket_peercred(&stream).unwrap_or_else(|e| {
95
        tracing::error!("Failed to get peer credentials: {e}");
96
        let _ = std::fs::remove_file(socket_path);
97
        std::process::exit(1);
98
    });
99
    if peer_cred.uid != uid {
100
        tracing::error!(
101
            "Rejected connection from UID {} (expected {})",
102
            peer_cred.uid.as_raw(),
103
            uid.as_raw()
104
        );
105
        let _ = std::fs::remove_file(socket_path);
106
        std::process::exit(1);
107
    }
108

            
109
    // Create memfd, write secret, seal it
110
    let memfd = rustix::fs::memfd_create(
111
        c"oo7-login-secret",
112
        MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING,
113
    )
114
    .unwrap_or_else(|e| {
115
        tracing::error!("Failed to create memfd: {e}");
116
        let _ = std::fs::remove_file(socket_path);
117
        std::process::exit(1);
118
    });
119

            
120
    rustix::io::write(&memfd, &secret).unwrap_or_else(|e| {
121
        tracing::error!("Failed to write to memfd: {e}");
122
        let _ = std::fs::remove_file(socket_path);
123
        std::process::exit(1);
124
    });
125

            
126
    zeroize::Zeroize::zeroize(&mut secret);
127

            
128
    rustix::fs::fcntl_add_seals(
129
        &memfd,
130
        SealFlags::WRITE | SealFlags::SHRINK | SealFlags::GROW | SealFlags::SEAL,
131
    )
132
    .unwrap_or_else(|e| {
133
        tracing::error!("Failed to seal memfd: {e}");
134
        let _ = std::fs::remove_file(socket_path);
135
        std::process::exit(1);
136
    });
137

            
138
    // Send the memfd via SCM_RIGHTS
139
    let fds = [std::os::fd::AsFd::as_fd(&memfd)];
140
    let mut space = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
141
    let mut cmsg_buf = SendAncillaryBuffer::new(&mut space);
142
    cmsg_buf.push(SendAncillaryMessage::ScmRights(&fds));
143

            
144
    let iov = [IoSlice::new(&[0u8])];
145
    rustix::net::sendmsg(&stream, &iov, &mut cmsg_buf, SendFlags::empty()).unwrap_or_else(|e| {
146
        tracing::error!("Failed to send memfd: {e}");
147
        let _ = std::fs::remove_file(socket_path);
148
        std::process::exit(1);
149
    });
150

            
151
    tracing::info!("Secret delivered to daemon");
152
    let _ = std::fs::remove_file(socket_path);
153
}