| // Copyright 2021 The Chromium OS Authors. All rights reserved. |
| // 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 message sent by the d-bus thread to the main thread when the |
| /// ResumeFromHibernate d-bus method is called. |
| struct ResumeRequest { |
| account_id: String, |
| completion_tx: Sender<u32>, |
| } |
| |
| impl ResumeRequest { |
| pub fn complete(&mut self) { |
| // 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 context shared between dbus calls. These must all have the Send |
| /// trait. |
| struct HibernateDbusStateInternal { |
| call_count: u32, |
| resume_tx: Sender<ResumeRequest>, |
| stop: bool, |
| } |
| |
| impl HibernateDbusStateInternal { |
| fn new(resume_tx: Sender<ResumeRequest>) -> Self { |
| Self { |
| call_count: 0, |
| resume_tx, |
| stop: false, |
| } |
| } |
| |
| /// D-bus method called by login_manager to let the hibernate service |
| /// know a user session is about to be started. |
| fn resume_from_hibernate(&mut self, account_id: &str) { |
| self.call_count += 1; |
| let (completion_tx, completion_rx) = channel(); |
| let _ = self.resume_tx.send(ResumeRequest { |
| account_id: account_id.to_string(), |
| completion_tx, |
| }); |
| |
| // 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(); |
| } |
| } |
| |
| /// 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<ResumeRequest>) -> 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<ResumeRequest>) -> 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("org.chromium.Hibernate", false, false, false) |
| .context("Failed to request dbus name")?; |
| |
| let mut crossroads = Crossroads::new(); |
| // Build a new HibernateResumeInterface. |
| let iface_token = crossroads.register("org.chromium.HibernateResumeInterface", |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(&account_id); |
| // This is currently the only thing the dbus thread is alive |
| // for, so shut it down after this method call. |
| internal_state.stop = true; |
| info!("ResumeFromHibernate completing"); |
| Ok(()) |
| }, |
| ); |
| }); |
| crossroads.insert("/org/chromium/Hibernate", &[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 gets called. At that point we drop off since that's all we |
| /// need. |
| fn receive_seed(&mut self) -> Result<()> { |
| info!("Looping to receive ResumeFromHibernate dbus call"); |
| 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 { |
| break; |
| } |
| |
| debug!("Still waiting for ResumeFromHibernate dbus call"); |
| } |
| |
| 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<ResumeRequest>, |
| } |
| |
| 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. |
| pub fn get_seed_material(&mut self, resume_in_progress: bool) -> Result<PendingResumeCall> { |
| // Block until the dbus thread receives a ResumeFromHibernate call, and |
| // receive that info. |
| info!("Waiting for ResumeFromHibernate call"); |
| let mut resume_request = self.resume_rx.recv()?; |
| |
| // 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, |
| &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); |
| 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, 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); |
| 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(()) |
| } |