| // Copyright 2021 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 hibernate. |
| |
| use std::ops::{Deref, DerefMut}; |
| use std::sync::mpsc::{channel, Receiver, Sender}; |
| use std::sync::Arc; |
| use std::thread; |
| use std::time::Duration; |
| |
| use anyhow::{Context as AnyhowContext, Result}; |
| use dbus::blocking::Connection; |
| use dbus::channel::MatchingReceiver; |
| use dbus::message::MatchRule; |
| use dbus_crossroads::{Context, Crossroads}; |
| use log::{debug, error, info}; |
| use protobuf::{Message, SingularPtrField}; |
| use sync::Mutex; |
| use system_api::client::OrgChromiumUserDataAuthInterface; |
| use system_api::rpc::AccountIdentifier; |
| use system_api::UserDataAuth::{GetHibernateSecretReply, GetHibernateSecretRequest}; |
| use zeroize::{Zeroize, ZeroizeOnDrop}; |
| |
| use crate::hiberutil::HibernateError; |
| |
| /// Define the minimum acceptable seed material length. |
| const MINIMUM_SEED_SIZE: usize = 32; |
| |
| // Define the timeout to connect to the dbus system. |
| pub const DEFAULT_DBUS_TIMEOUT: Duration = Duration::from_secs(25); |
| |
| /// Define the name used on dbus. |
| const HIBERMAN_DBUS_NAME: &str = "org.chromium.Hibernate"; |
| /// Define the path used within dbus. |
| const HIBERMAN_DBUS_PATH: &str = "/org/chromium/Hibernate"; |
| /// Define the name of the resume dbus interface. |
| const HIBERMAN_RESUME_DBUS_INTERFACE: &str = "org.chromium.HibernateResumeInterface"; |
| |
| /// Define the message sent by the d-bus thread to the main thread when the |
| /// ResumeFromHibernate d-bus method is called. |
| struct ResumeRequest { |
| account_id: String, |
| auth_session_id: Vec<u8>, |
| completion_tx: Sender<u32>, |
| } |
| |
| impl ResumeRequest { |
| pub fn complete(&mut self) { |
| info!("Completing the ResumeRequest"); |
| // The content of the message is ignored, this simply allows the dbus |
| // thread to unblock and complete the method. The error is ignored |
| // because the best course of action is simply to keep going with |
| // the main thread. |
| let _ = self.completion_tx.send(0); |
| } |
| } |
| |
| impl Drop for ResumeRequest { |
| fn drop(&mut self) { |
| self.complete(); |
| } |
| } |
| |
| /// Define the message sent if the resume is aborted. |
| struct ResumeAbort { |
| reason: String, |
| } |
| |
| /// Define the union used to receive either a ResumeRequest or a ResumeAbort. |
| enum ResumeVerdict { |
| Requested(ResumeRequest), |
| Aborted(ResumeAbort), |
| } |
| |
| /// Define the context shared between dbus calls. These must all have the Send |
| /// trait. |
| struct HibernateDbusStateInternal { |
| call_count: u32, |
| resume_tx: Sender<ResumeVerdict>, |
| stop: bool, |
| } |
| |
| impl HibernateDbusStateInternal { |
| fn new(resume_tx: Sender<ResumeVerdict>) -> Self { |
| Self { |
| call_count: 0, |
| resume_tx, |
| stop: false, |
| } |
| } |
| |
| /// D-bus method called by Chrome to let the hibernate service |
| /// know a user session is about to be started. |
| fn resume_from_hibernate(&mut self, account_id: &str, auth_session_id: &[u8]) { |
| self.call_count += 1; |
| let (completion_tx, completion_rx) = channel(); |
| let request = ResumeRequest { |
| account_id: account_id.to_string(), |
| auth_session_id: auth_session_id.to_vec(), |
| completion_tx, |
| }; |
| |
| let _ = self.resume_tx.send(ResumeVerdict::Requested(request)); |
| |
| // Wait on the completion channel for the main thread to send something. |
| // It doesn't matter what it is, and on error, just keep going. |
| info!("ResumeFromHibernate: waiting on main thread"); |
| let _ = completion_rx.recv(); |
| } |
| |
| /// D-bus method called by Chrome to initiate resume from hibernation, given |
| /// an account ID. |
| fn resume_from_hibernate_acct(&mut self, account_id: &str) { |
| self.resume_from_hibernate(account_id, &[]) |
| } |
| |
| /// D-bus method called by Chrome to initiate resume from hibernation within |
| /// an auth session. |
| fn resume_from_hibernate_auth(&mut self, auth_session_id: &[u8]) { |
| self.resume_from_hibernate("", auth_session_id) |
| } |
| |
| /// D-bus method called by various components in the system to abort a |
| /// resume from hibernation. |
| fn abort_resume(&mut self, reason: String) { |
| self.call_count += 1; |
| info!("Received abort request: {}", &reason); |
| let _ = self |
| .resume_tx |
| .send(ResumeVerdict::Aborted(ResumeAbort { reason })); |
| } |
| } |
| |
| /// Define the d-bus state. Arc and Mutex are needed because crossroads takes |
| /// ownership of the state passed in, and requires the Send trait. |
| #[derive(Clone)] |
| struct HibernateDbusState(Arc<Mutex<HibernateDbusStateInternal>>); |
| |
| impl HibernateDbusState { |
| fn new(resume_tx: Sender<ResumeVerdict>) -> Self { |
| HibernateDbusState(Arc::new(Mutex::new(HibernateDbusStateInternal::new( |
| resume_tx, |
| )))) |
| } |
| } |
| |
| /// Define the connection details to Dbus. This is the unprotected version, to |
| /// be manipulated after acquiring the lock. |
| struct HiberDbusConnectionInternal { |
| conn: Connection, |
| state: HibernateDbusState, |
| } |
| |
| impl HiberDbusConnectionInternal { |
| /// Fire up a new system d-bus server. |
| fn new(resume_tx: Sender<ResumeVerdict>) -> Result<Self> { |
| info!("Setting up dbus"); |
| let state = HibernateDbusState::new(resume_tx); |
| 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 |_ctx: &mut Context, |
| state: &mut HibernateDbusState, |
| (account_id,): (String,)| { |
| // Here's what happens when the method is called. |
| let mut internal_state = state.0.lock(); |
| internal_state.resume_from_hibernate_acct(&account_id); |
| // Shut down the dbus thread since resume is committed. |
| internal_state.stop = true; |
| info!("ResumeFromHibernate completing"); |
| Ok(()) |
| }, |
| ); |
| |
| b.method( |
| "ResumeFromHibernateAS", |
| ("auth_session_id",), |
| (), |
| move |_ctx: &mut Context, |
| state: &mut HibernateDbusState, |
| (auth_session_id,): (Vec<u8>,)| { |
| // Here's what happens when the method is called. |
| let mut internal_state = state.0.lock(); |
| internal_state.resume_from_hibernate_auth(&auth_session_id); |
| // Shut down the dbus thread since resume is committed. |
| internal_state.stop = true; |
| info!("ResumeFromHibernateAS completing"); |
| Ok(()) |
| }, |
| ); |
| |
| b.method( |
| "AbortResume", |
| ("reason",), |
| (), |
| move |_ctx: &mut Context, state: &mut HibernateDbusState, (reason,): (String,)| { |
| // Here's what happens when the method is called. |
| let mut internal_state = state.0.lock(); |
| internal_state.abort_resume(reason); |
| info!("AbortResume completing"); |
| Ok(()) |
| }, |
| ); |
| }); |
| |
| crossroads.insert(HIBERMAN_DBUS_PATH, &[iface_token], state.clone()); |
| 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 |
| } |
| }), |
| ); |
| |
| info!("Completed dbus setup"); |
| Ok(HiberDbusConnectionInternal { conn, state }) |
| } |
| |
| /// Public function used by the dbus thread to process requests until the |
| /// resume method or abort gets called. At that point we drop off since |
| /// that's all we need. |
| fn receive_seed(&mut self) -> Result<()> { |
| info!("Processing dbus requests"); |
| loop { |
| self.conn |
| .process(Duration::from_millis(30000)) |
| .context("Failed to process")?; |
| // Break out if ResumefromHibernate was called. |
| let state = self.state.0.lock(); |
| if state.stop { |
| info!("Stopped serving dbus requests"); |
| break; |
| } |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| #[derive(Default, Zeroize, ZeroizeOnDrop)] |
| pub struct ResumeSecretSeed { |
| value: Vec<u8>, |
| } |
| |
| impl Deref for ResumeSecretSeed { |
| type Target = Vec<u8>; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.value |
| } |
| } |
| |
| impl DerefMut for ResumeSecretSeed { |
| fn deref_mut(&mut self) -> &mut Self::Target { |
| &mut self.value |
| } |
| } |
| |
| /// This struct serves as a ticket indicating that the dbus thread is currently |
| /// blocked in the ResumeFromHibernate method. |
| pub struct PendingResumeCall { |
| pub secret_seed: ResumeSecretSeed, |
| // When this resume request is dropped, the dbus thread allows the method |
| // call to complete. |
| _resume_request: ResumeRequest, |
| } |
| |
| /// Define the thread safe version of the dbus connection state. |
| pub struct HiberDbusConnection { |
| internal: Arc<Mutex<HiberDbusConnectionInternal>>, |
| thread: Option<thread::JoinHandle<()>>, |
| resume_rx: Receiver<ResumeVerdict>, |
| } |
| |
| impl HiberDbusConnection { |
| /// Create a new dbus connection and announce ourselves on the bus. This |
| /// function does not start serving requests yet though. |
| pub fn new() -> Result<Self> { |
| let (resume_tx, resume_rx) = channel(); |
| Ok(HiberDbusConnection { |
| internal: Arc::new(Mutex::new(HiberDbusConnectionInternal::new(resume_tx)?)), |
| thread: None, |
| resume_rx, |
| }) |
| } |
| |
| /// Fire up a thread to respond to dbus requests. |
| pub fn spawn_dbus_server(&mut self) -> Result<()> { |
| let arc_clone = Arc::clone(&self.internal); |
| self.thread = Some(thread::spawn(move || { |
| debug!("Started dbus server thread"); |
| let mut conn = arc_clone.lock(); |
| let _ = conn.receive_seed(); |
| debug!("Exiting dbus server thread"); |
| })); |
| |
| Ok(()) |
| } |
| |
| /// Block waiting for the seed material to become available from cryptohome, |
| /// then return that material. This errors out upon receiving an abort if |
| /// resume_in_progress is set, or continues waiting otherwise. |
| pub fn get_seed_material(&mut self, resume_in_progress: bool) -> Result<PendingResumeCall> { |
| loop { |
| // Block until the dbus thread receives a ResumeFromHibernate call, and |
| // receive that info. |
| info!("Waiting for ResumeFromHibernate call"); |
| let resume_verdict = self.resume_rx.recv()?; |
| match resume_verdict { |
| ResumeVerdict::Aborted(resume_abort) => { |
| error!("Got resume abort request: {}", &resume_abort.reason); |
| if !resume_in_progress { |
| debug!("Ignoring abort request since resume is not in progress"); |
| continue; |
| } |
| |
| return Err(HibernateError::ResumeAbortRequested(resume_abort.reason)) |
| .context("Resume aborted"); |
| } |
| ResumeVerdict::Requested(mut resume_request) => { |
| // If there's no resume in progress, unblock boot ASAP. |
| if !resume_in_progress { |
| info!("Unblocking ResumeFromHibernate immediately"); |
| resume_request.complete(); |
| } |
| |
| info!("Requesting secret seed"); |
| let mut pending_call = PendingResumeCall { |
| secret_seed: ResumeSecretSeed::default(), |
| _resume_request: resume_request, |
| }; |
| |
| get_secret_seed( |
| &pending_call._resume_request.account_id, |
| &pending_call._resume_request.auth_session_id, |
| &mut pending_call.secret_seed.value, |
| )?; |
| let length = pending_call.secret_seed.value.len(); |
| if length < MINIMUM_SEED_SIZE { |
| return Err(HibernateError::DbusError(format!( |
| "Seed size {} was below minium {}", |
| length, MINIMUM_SEED_SIZE |
| ))) |
| .context("Failed to receive seed"); |
| } |
| |
| info!("Got {} bytes of seed material", length); |
| return Ok(pending_call); |
| } |
| } |
| } |
| } |
| } |
| |
| /// Ask cryptohome for the hibernate seed for the given account. This call only |
| /// works once, then cryptohome forgets the secret. The vector receiving the |
| /// secret is passed in rather than being returned so its memory location can be |
| /// accounted for and explicitly zeroed out when no longer needed. |
| fn get_secret_seed(account_id: &str, auth_session_id: &[u8], seed: &mut Vec<u8>) -> Result<()> { |
| let conn = Connection::new_system().context("Failed to connect to dbus for secret seed")?; |
| let conn_path = conn.with_proxy( |
| "org.chromium.UserDataAuth", |
| "/org/chromium/UserDataAuth", |
| 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 = conn_path |
| .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 secret to the output parameter so the reply structure can be |
| // zeroed. |
| seed.resize(reply.hibernate_secret.len(), 0); |
| seed.copy_from_slice(&reply.hibernate_secret); |
| reply.hibernate_secret.fill(0); |
| Ok(()) |
| } |
| |
| /// 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 secret seed")?; |
| let conn_path = conn.with_proxy(HIBERMAN_DBUS_NAME, HIBERMAN_DBUS_PATH, DEFAULT_DBUS_TIMEOUT); |
| |
| // Now make the method call. The ListNames method call takes zero input parameters and |
| // one output parameter which is an array of strings. |
| // Therefore the input is a zero tuple "()", and the output is a single tuple "(names,)". |
| conn_path |
| .method_call(HIBERMAN_RESUME_DBUS_INTERFACE, "AbortResume", (reason,)) |
| .context("Failed to send abort request")?; |
| info!("Sent AbortResume request"); |
| Ok(()) |
| } |