blob: 9849d523f5084b4634bd26adbaca037f819bdccd [file] [log] [blame] [edit]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Handles the D-Bus interface for resume.
use std::sync::mpsc::channel;
use std::thread;
use std::time::Duration;
use anyhow::{Context, Result};
use dbus::blocking::Connection;
use dbus::channel::MatchingReceiver; // For start_receive
use dbus::message::MatchRule;
use dbus_crossroads::Crossroads;
use libchromeos::secure_blob::SecureBlob;
use log::{debug, error};
use protobuf::Message;
use protobuf::SingularPtrField;
use system_api::client::OrgChromiumUserDataAuthInterface; // For get_hibernate_secret
use system_api::rpc::AccountIdentifier;
use system_api::UserDataAuth::GetHibernateSecretReply;
use system_api::UserDataAuth::GetHibernateSecretRequest;
use zeroize::Zeroize;
const HIBERMAN_DBUS_NAME: &str = "org.chromium.Hibernate";
const HIBERMAN_DBUS_PATH: &str = "/org/chromium/Hibernate";
const HIBERMAN_RESUME_DBUS_INTERFACE: &str = "org.chromium.HibernateResumeInterface";
pub enum ResumeRequest {
ResumeAccountId { account_id: String },
ResumeAuthSessionId { auth_session_id: Vec<u8> },
Abort { reason: String },
}
// Returns ResumeRequest when receiving D-Bus resume events.
//
// There are 2 channels for communication between the main thread and the D-Bus server thread. The
// main thread notifies resume completion to the D-Bus thread via the completion channel
// (completion_{sender|receiver}). The D-Bus thread sends the resume request to the main thread via
// the resume request channel (resume_{sender|receiver}).
//
// The resume request channel's type is std::sync::mpsc::channel (multi producer single consumer
// channel) because the sender needs to be cloned for multiple D-Bus methods. The completion
// channel's type is crossbeam_channel::Receiver (multi producer multi consumer channel) because
// the receiver needs to be cloned for multiple D-Bus methods.
//
// Reference: dbus_crossroads example:
// https://github.com/diwic/dbus-rs/tree/master/dbus-crossroads/examples
pub fn wait_for_resume_dbus_event(
completion_receiver: crossbeam_channel::Receiver<()>,
) -> Result<ResumeRequest> {
// Clone the completion_receiver for multiple closures. Each closure needs to own its receiver.
let completion_as_receiver = completion_receiver.clone();
let completion_abort_receiver = completion_receiver.clone();
// resume_sender is used by the D-Bus server thread to send the D-Bus resume request to main
// thread.
let (resume_sender, resume_receiver) = channel();
let resume_as_sender = resume_sender.clone();
let abort_sender = resume_sender.clone();
let conn = Connection::new_system().context("Failed to start local dbus connection")?;
conn.request_name(HIBERMAN_DBUS_NAME, false, false, false)
.context("Failed to request dbus name")?;
let mut crossroads = Crossroads::new();
// Build a new HibernateResumeInterface.
let iface_token = crossroads.register(HIBERMAN_RESUME_DBUS_INTERFACE, |b| {
b.method(
"ResumeFromHibernate",
("account_id",),
(),
move |_, _, (account_id,): (String,)| {
// Send the resume request to the main thread.
if let Err(e) = resume_sender.send(ResumeRequest::ResumeAccountId { account_id }) {
error!(
"Failed to send resume account id request to the main thread: {:?}",
e
);
}
// recv() returns an error when the sender is dropped.
_ = completion_receiver.recv();
debug!("ResumeFromHibernate completing");
Ok(())
},
);
b.method(
"ResumeFromHibernateAS",
("auth_session_id",),
(),
move |_, _, (auth_session_id,): (Vec<u8>,)| {
// Send the resume request to the main thread.
if let Err(e) =
resume_as_sender.send(ResumeRequest::ResumeAuthSessionId { auth_session_id })
{
error!(
"Failed to send resume auth session id request to the main thread: {:?}",
e
);
}
// recv() returns an error when the sender is dropped.
_ = completion_as_receiver.recv();
debug!("ResumeFromHibernateAS completing");
Ok(())
},
);
b.method(
"AbortResume",
("reason",),
(),
move |_, _, (reason,): (String,)| {
// Send the resume abort request to the main thread.
if let Err(e) = abort_sender.send(ResumeRequest::Abort { reason }) {
error!(
"Failed to send resume abort request to the main thread: {:?}",
e
);
}
// recv() returns an error when the sender is dropped.
_ = completion_abort_receiver.recv();
debug!("AbortResume completing");
Ok(())
},
);
});
// Use an empty context object as we don't have shared state.
struct ResumeDbusContext {}
crossroads.insert(HIBERMAN_DBUS_PATH, &[iface_token], ResumeDbusContext {});
// The D-Bus methods are handled by the crossroads instance.
conn.start_receive(
MatchRule::new_method_call(),
Box::new(move |msg, conn| {
if let Err(e) = crossroads.handle_message(msg, conn) {
error!("Failed to handle message: {:?}", e);
false
} else {
true
}
}),
);
// Spawn a thread to process D-Bus messages.
thread::spawn(move || loop {
// When conn.process() times out, it returns Ok(false) and re-enters the loop. Longer
// timeout makes this D-Bus server thread waking up less when it's waiting for the D-Bus
// message (e.g. the user might not sign in for thirty minutes).
if let Err(e) = conn.process(Duration::from_secs(30)) {
error!("Failed to process dbus message: {:?}", e);
}
});
let resume_request = resume_receiver.recv()?;
Ok(resume_request)
}
// Define the timeout to connect to the dbus system.
const DEFAULT_DBUS_TIMEOUT: Duration = Duration::from_secs(10);
/// Ask cryptohome for the hibernate key for the given account. This call only works once, then
/// cryptohome forgets the key. The return value's type is SecureBlob so its content is zeroed when
/// no longer needed.
pub fn get_user_key(account_id: &str, auth_session_id: &[u8]) -> Result<SecureBlob> {
const CRYPTOHOME_DBUS_NAME: &str = "org.chromium.UserDataAuth";
const CRYPTOHOME_DBUS_PATH: &str = "/org/chromium/UserDataAuth";
let conn =
Connection::new_system().context("Failed to connect to dbus for hibernate secret")?;
let proxy = conn.with_proxy(
CRYPTOHOME_DBUS_NAME,
CRYPTOHOME_DBUS_PATH,
DEFAULT_DBUS_TIMEOUT,
);
let mut proto: GetHibernateSecretRequest = Message::new();
let mut account_identifier = AccountIdentifier::new();
account_identifier.set_account_id(account_id.to_string());
proto.account_id = SingularPtrField::some(account_identifier);
proto.auth_session_id = auth_session_id.to_vec();
let mut response = proxy
.get_hibernate_secret(proto.write_to_bytes().unwrap())
.context("Failed to call GetHibernateSecret dbus method")?;
let mut reply: GetHibernateSecretReply = Message::parse_from_bytes(&response)
.context("Failed to parse GetHibernateSecret dbus response")?;
response.zeroize();
// Copy the key to the output parameter so the reply structure can be zeroed.
let mut key_data: Vec<u8> = vec![0; reply.hibernate_secret.len()];
key_data.copy_from_slice(&reply.hibernate_secret);
reply.hibernate_secret.fill(0);
Ok(SecureBlob::from(key_data))
}
/// Send an abort request over dbus to cancel a pending resume. The hiberman process calling this
/// function might not be the same as the hiberman process serving the dbus requests. For example,
/// a developer may invoke the abort resume subcommand.
pub fn send_abort(reason: &str) -> Result<()> {
let conn = Connection::new_system().context("Failed to connect to dbus for send abort")?;
let proxy = conn.with_proxy(HIBERMAN_DBUS_NAME, HIBERMAN_DBUS_PATH, DEFAULT_DBUS_TIMEOUT);
proxy
.method_call(HIBERMAN_RESUME_DBUS_INTERFACE, "AbortResume", (reason,))
.context("Failed to send abort request")?;
debug!("Sent AbortResume request");
Ok(())
}