Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 1 | // Copyright 2020, The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 15 | //! This module implements the Android Protected Confirmation (APC) service as defined |
| 16 | //! in the android.security.apc AIDL spec. |
| 17 | |
| 18 | use std::{ |
| 19 | cmp::PartialEq, |
| 20 | collections::HashMap, |
Janis Danisevskis | b1673db | 2021-02-08 18:11:57 -0800 | [diff] [blame] | 21 | sync::{mpsc::Sender, Arc, Mutex}, |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 22 | }; |
| 23 | |
Janis Danisevskis | ea03cff | 2021-12-16 08:10:17 -0800 | [diff] [blame] | 24 | use crate::error::anyhow_error_to_cstring; |
Shaquille Johnson | 9da2e1c | 2022-09-19 12:39:01 +0000 | [diff] [blame] | 25 | use crate::ks_err; |
Hasini Gunasinghe | 5a893e8 | 2021-05-05 14:32:32 +0000 | [diff] [blame] | 26 | use crate::utils::{compat_2_response_code, ui_opts_2_compat, watchdog as wd}; |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 27 | use android_security_apc::aidl::android::security::apc::{ |
| 28 | IConfirmationCallback::IConfirmationCallback, |
| 29 | IProtectedConfirmation::{BnProtectedConfirmation, IProtectedConfirmation}, |
| 30 | ResponseCode::ResponseCode, |
| 31 | }; |
| 32 | use android_security_apc::binder::{ |
Andrew Walbran | de45c8b | 2021-04-13 14:42:38 +0000 | [diff] [blame] | 33 | BinderFeatures, ExceptionCode, Interface, Result as BinderResult, SpIBinder, |
| 34 | Status as BinderStatus, Strong, ThreadState, |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 35 | }; |
| 36 | use anyhow::{Context, Result}; |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 37 | use keystore2_apc_compat::ApcHal; |
| 38 | use keystore2_selinux as selinux; |
| 39 | use std::time::{Duration, Instant}; |
| 40 | |
| 41 | /// This is the main APC error type, it wraps binder exceptions and the |
| 42 | /// APC ResponseCode. |
Chris Wailes | 263de9f | 2022-08-11 15:00:51 -0700 | [diff] [blame] | 43 | #[derive(Debug, thiserror::Error, PartialEq, Eq)] |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 44 | pub enum Error { |
| 45 | /// Wraps an Android Protected Confirmation (APC) response code as defined by the |
| 46 | /// android.security.apc AIDL interface specification. |
| 47 | #[error("Error::Rc({0:?})")] |
| 48 | Rc(ResponseCode), |
| 49 | /// Wraps a Binder exception code other than a service specific exception. |
| 50 | #[error("Binder exception code {0:?}, {1:?}")] |
| 51 | Binder(ExceptionCode, i32), |
| 52 | } |
| 53 | |
| 54 | impl Error { |
| 55 | /// Short hand for `Error::Rc(ResponseCode::SYSTEM_ERROR)` |
| 56 | pub fn sys() -> Self { |
| 57 | Error::Rc(ResponseCode::SYSTEM_ERROR) |
| 58 | } |
| 59 | |
| 60 | /// Short hand for `Error::Rc(ResponseCode::OPERATION_PENDING)` |
| 61 | pub fn pending() -> Self { |
| 62 | Error::Rc(ResponseCode::OPERATION_PENDING) |
| 63 | } |
| 64 | |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 65 | /// Short hand for `Error::Rc(ResponseCode::IGNORED)` |
| 66 | pub fn ignored() -> Self { |
| 67 | Error::Rc(ResponseCode::IGNORED) |
| 68 | } |
| 69 | |
| 70 | /// Short hand for `Error::Rc(ResponseCode::UNIMPLEMENTED)` |
| 71 | pub fn unimplemented() -> Self { |
| 72 | Error::Rc(ResponseCode::UNIMPLEMENTED) |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | /// This function should be used by confirmation service calls to translate error conditions |
| 77 | /// into service specific exceptions. |
| 78 | /// |
| 79 | /// All error conditions get logged by this function. |
| 80 | /// |
| 81 | /// `Error::Rc(x)` variants get mapped onto a service specific error code of `x`. |
| 82 | /// `selinux::Error::perm()` is mapped on `ResponseCode::PERMISSION_DENIED`. |
| 83 | /// |
| 84 | /// All non `Error` error conditions get mapped onto ResponseCode::SYSTEM_ERROR`. |
David Drysdale | 5238d77 | 2024-06-07 15:12:10 +0100 | [diff] [blame] | 85 | pub fn map_or_log_err<T>(result: Result<T>) -> BinderResult<T> { |
| 86 | result.map_err(|e| { |
| 87 | log::error!("{:#?}", e); |
| 88 | let root_cause = e.root_cause(); |
| 89 | let rc = match root_cause.downcast_ref::<Error>() { |
| 90 | Some(Error::Rc(rcode)) => rcode.0, |
| 91 | Some(Error::Binder(_, _)) => ResponseCode::SYSTEM_ERROR.0, |
| 92 | None => match root_cause.downcast_ref::<selinux::Error>() { |
| 93 | Some(selinux::Error::PermissionDenied) => ResponseCode::PERMISSION_DENIED.0, |
| 94 | _ => ResponseCode::SYSTEM_ERROR.0, |
| 95 | }, |
| 96 | }; |
| 97 | BinderStatus::new_service_specific_error(rc, anyhow_error_to_cstring(&e).as_deref()) |
| 98 | }) |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 99 | } |
| 100 | |
| 101 | /// Rate info records how many failed attempts a client has made to display a protected |
| 102 | /// confirmation prompt. Clients are penalized for attempts that get declined by the user |
| 103 | /// or attempts that get aborted by the client itself. |
| 104 | /// |
| 105 | /// After the third failed attempt the client has to cool down for 30 seconds before it |
| 106 | /// it can retry. After the sixth failed attempt, the time doubles with every failed attempt |
| 107 | /// until it goes into saturation at 24h. |
| 108 | /// |
| 109 | /// A successful user prompt resets the counter. |
| 110 | #[derive(Debug, Clone)] |
| 111 | struct RateInfo { |
| 112 | counter: u32, |
| 113 | timestamp: Instant, |
| 114 | } |
| 115 | |
| 116 | impl RateInfo { |
| 117 | const ONE_DAY: Duration = Duration::from_secs(60u64 * 60u64 * 24u64); |
| 118 | |
| 119 | fn get_remaining_back_off(&self) -> Option<Duration> { |
| 120 | let back_off = match self.counter { |
| 121 | // The first three attempts come without penalty. |
| 122 | 0..=2 => return None, |
| 123 | // The next three attempts are are penalized with 30 seconds back off time. |
| 124 | 3..=5 => Duration::from_secs(30), |
| 125 | // After that we double the back off time the with every additional attempt |
| 126 | // until we reach 1024m (~17h). |
| 127 | 6..=16 => Duration::from_secs(60) |
| 128 | .checked_mul(1u32 << (self.counter - 6)) |
| 129 | .unwrap_or(Self::ONE_DAY), |
| 130 | // After that we cap of at 24h between attempts. |
| 131 | _ => Self::ONE_DAY, |
| 132 | }; |
| 133 | let elapsed = self.timestamp.elapsed(); |
| 134 | // This does exactly what we want. |
| 135 | // `back_off - elapsed` is the remaining back off duration or None if elapsed is larger |
| 136 | // than back_off. Also, this operation cannot overflow as long as elapsed is less than |
| 137 | // back_off, which is all that we care about. |
| 138 | back_off.checked_sub(elapsed) |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | impl Default for RateInfo { |
| 143 | fn default() -> Self { |
| 144 | Self { counter: 0u32, timestamp: Instant::now() } |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | /// The APC session state represents the state of an APC session. |
| 149 | struct ApcSessionState { |
| 150 | /// A reference to the APC HAL backend. |
| 151 | hal: Arc<ApcHal>, |
| 152 | /// The client callback object. |
| 153 | cb: SpIBinder, |
| 154 | /// The uid of the owner of this APC session. |
| 155 | uid: u32, |
| 156 | /// The time when this session was started. |
| 157 | start: Instant, |
| 158 | /// This is set when the client calls abort. |
| 159 | /// This is used by the rate limiting logic to determine |
| 160 | /// if the client needs to be penalized for this attempt. |
| 161 | client_aborted: bool, |
| 162 | } |
| 163 | |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 164 | struct ApcState { |
| 165 | session: Option<ApcSessionState>, |
| 166 | rate_limiting: HashMap<u32, RateInfo>, |
Janis Danisevskis | b1673db | 2021-02-08 18:11:57 -0800 | [diff] [blame] | 167 | confirmation_token_sender: Sender<Vec<u8>>, |
| 168 | } |
| 169 | |
| 170 | impl ApcState { |
| 171 | fn new(confirmation_token_sender: Sender<Vec<u8>>) -> Self { |
| 172 | Self { session: None, rate_limiting: Default::default(), confirmation_token_sender } |
| 173 | } |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 174 | } |
| 175 | |
| 176 | /// Implementation of the APC service. |
| 177 | pub struct ApcManager { |
| 178 | state: Arc<Mutex<ApcState>>, |
| 179 | } |
| 180 | |
| 181 | impl Interface for ApcManager {} |
| 182 | |
| 183 | impl ApcManager { |
| 184 | /// Create a new instance of the Android Protected Confirmation service. |
Janis Danisevskis | b1673db | 2021-02-08 18:11:57 -0800 | [diff] [blame] | 185 | pub fn new_native_binder( |
| 186 | confirmation_token_sender: Sender<Vec<u8>>, |
Stephen Crane | 221bbb5 | 2020-12-16 15:52:10 -0800 | [diff] [blame] | 187 | ) -> Result<Strong<dyn IProtectedConfirmation>> { |
Andrew Walbran | de45c8b | 2021-04-13 14:42:38 +0000 | [diff] [blame] | 188 | Ok(BnProtectedConfirmation::new_binder( |
| 189 | Self { state: Arc::new(Mutex::new(ApcState::new(confirmation_token_sender))) }, |
| 190 | BinderFeatures { set_requesting_sid: true, ..BinderFeatures::default() }, |
| 191 | )) |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 192 | } |
| 193 | |
| 194 | fn result( |
| 195 | state: Arc<Mutex<ApcState>>, |
| 196 | rc: u32, |
| 197 | data_confirmed: Option<&[u8]>, |
| 198 | confirmation_token: Option<&[u8]>, |
| 199 | ) { |
| 200 | let mut state = state.lock().unwrap(); |
| 201 | let (callback, uid, start, client_aborted) = match state.session.take() { |
| 202 | None => return, // Nothing to do |
| 203 | Some(ApcSessionState { cb: callback, uid, start, client_aborted, .. }) => { |
| 204 | (callback, uid, start, client_aborted) |
| 205 | } |
| 206 | }; |
| 207 | |
| 208 | let rc = compat_2_response_code(rc); |
| 209 | |
| 210 | // Update rate limiting information. |
Janis Danisevskis | b1673db | 2021-02-08 18:11:57 -0800 | [diff] [blame] | 211 | match (rc, client_aborted, confirmation_token) { |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 212 | // If the user confirmed the dialog. |
Janis Danisevskis | b1673db | 2021-02-08 18:11:57 -0800 | [diff] [blame] | 213 | (ResponseCode::OK, _, Some(confirmation_token)) => { |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 214 | // Reset counter. |
| 215 | state.rate_limiting.remove(&uid); |
Janis Danisevskis | b1673db | 2021-02-08 18:11:57 -0800 | [diff] [blame] | 216 | // Send confirmation token to the enforcement module. |
| 217 | if let Err(e) = state.confirmation_token_sender.send(confirmation_token.to_vec()) { |
| 218 | log::error!("Got confirmation token, but receiver would not have it. {:?}", e); |
| 219 | } |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 220 | } |
| 221 | // If cancelled by the user or if aborted by the client. |
Janis Danisevskis | b1673db | 2021-02-08 18:11:57 -0800 | [diff] [blame] | 222 | (ResponseCode::CANCELLED, _, _) | (ResponseCode::ABORTED, true, _) => { |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 223 | // Penalize. |
Chris Wailes | 53a22af | 2023-07-12 17:02:47 -0700 | [diff] [blame] | 224 | let rate_info = state.rate_limiting.entry(uid).or_default(); |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 225 | rate_info.counter += 1; |
| 226 | rate_info.timestamp = start; |
| 227 | } |
Janis Danisevskis | b1673db | 2021-02-08 18:11:57 -0800 | [diff] [blame] | 228 | (ResponseCode::OK, _, None) => { |
| 229 | log::error!( |
| 230 | "Confirmation prompt was successful but no confirmation token was returned." |
| 231 | ); |
| 232 | } |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 233 | // In any other case this try does not count at all. |
| 234 | _ => {} |
| 235 | } |
| 236 | drop(state); |
| 237 | |
| 238 | if let Ok(listener) = callback.into_interface::<dyn IConfirmationCallback>() { |
| 239 | if let Err(e) = listener.onCompleted(rc, data_confirmed) { |
Shaquille Johnson | 9da2e1c | 2022-09-19 12:39:01 +0000 | [diff] [blame] | 240 | log::error!("Reporting completion to client failed {:?}", e) |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 241 | } |
| 242 | } else { |
Shaquille Johnson | 9da2e1c | 2022-09-19 12:39:01 +0000 | [diff] [blame] | 243 | log::error!("SpIBinder is not a IConfirmationCallback."); |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 244 | } |
| 245 | } |
| 246 | |
| 247 | fn present_prompt( |
| 248 | &self, |
Andrei Homescu | 0a8291b | 2021-03-24 02:36:43 +0000 | [diff] [blame] | 249 | listener: &binder::Strong<dyn IConfirmationCallback>, |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 250 | prompt_text: &str, |
| 251 | extra_data: &[u8], |
| 252 | locale: &str, |
| 253 | ui_option_flags: i32, |
| 254 | ) -> Result<()> { |
| 255 | let mut state = self.state.lock().unwrap(); |
| 256 | if state.session.is_some() { |
Shaquille Johnson | 9da2e1c | 2022-09-19 12:39:01 +0000 | [diff] [blame] | 257 | return Err(Error::pending()).context(ks_err!("APC Session pending.")); |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 258 | } |
| 259 | |
| 260 | // Perform rate limiting. |
| 261 | let uid = ThreadState::get_calling_uid(); |
| 262 | match state.rate_limiting.get(&uid) { |
| 263 | None => {} |
| 264 | Some(rate_info) => { |
| 265 | if let Some(back_off) = rate_info.get_remaining_back_off() { |
Shaquille Johnson | 9da2e1c | 2022-09-19 12:39:01 +0000 | [diff] [blame] | 266 | return Err(Error::sys()).context(ks_err!( |
| 267 | "APC Cooling down. Remaining back-off: {}s", |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 268 | back_off.as_secs() |
| 269 | )); |
| 270 | } |
| 271 | } |
| 272 | } |
| 273 | |
| 274 | let hal = ApcHal::try_get_service(); |
| 275 | let hal = match hal { |
| 276 | None => { |
Shaquille Johnson | 9da2e1c | 2022-09-19 12:39:01 +0000 | [diff] [blame] | 277 | return Err(Error::unimplemented()).context(ks_err!("APC not supported.")); |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 278 | } |
| 279 | Some(h) => Arc::new(h), |
| 280 | }; |
| 281 | |
| 282 | let ui_opts = ui_opts_2_compat(ui_option_flags); |
| 283 | |
| 284 | let state_clone = self.state.clone(); |
| 285 | hal.prompt_user_confirmation( |
| 286 | prompt_text, |
| 287 | extra_data, |
| 288 | locale, |
| 289 | ui_opts, |
Janis Danisevskis | b1673db | 2021-02-08 18:11:57 -0800 | [diff] [blame] | 290 | move |rc, data_confirmed, confirmation_token| { |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 291 | Self::result(state_clone, rc, data_confirmed, confirmation_token) |
| 292 | }, |
| 293 | ) |
| 294 | .map_err(|rc| Error::Rc(compat_2_response_code(rc))) |
Shaquille Johnson | 9da2e1c | 2022-09-19 12:39:01 +0000 | [diff] [blame] | 295 | .context(ks_err!("APC Failed to present prompt."))?; |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 296 | state.session = Some(ApcSessionState { |
| 297 | hal, |
| 298 | cb: listener.as_binder(), |
| 299 | uid, |
| 300 | start: Instant::now(), |
| 301 | client_aborted: false, |
| 302 | }); |
| 303 | Ok(()) |
| 304 | } |
| 305 | |
Andrei Homescu | 0a8291b | 2021-03-24 02:36:43 +0000 | [diff] [blame] | 306 | fn cancel_prompt(&self, listener: &binder::Strong<dyn IConfirmationCallback>) -> Result<()> { |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 307 | let mut state = self.state.lock().unwrap(); |
| 308 | let hal = match &mut state.session { |
| 309 | None => { |
| 310 | return Err(Error::ignored()) |
Shaquille Johnson | 9da2e1c | 2022-09-19 12:39:01 +0000 | [diff] [blame] | 311 | .context(ks_err!("Attempt to cancel non existing session. Ignoring.")); |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 312 | } |
| 313 | Some(session) => { |
| 314 | if session.cb != listener.as_binder() { |
Shaquille Johnson | 9da2e1c | 2022-09-19 12:39:01 +0000 | [diff] [blame] | 315 | return Err(Error::ignored()).context(ks_err!( |
| 316 | "Attempt to cancel session not belonging to caller. Ignoring." |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 317 | )); |
| 318 | } |
| 319 | session.client_aborted = true; |
| 320 | session.hal.clone() |
| 321 | } |
| 322 | }; |
| 323 | drop(state); |
| 324 | hal.abort(); |
| 325 | Ok(()) |
| 326 | } |
| 327 | |
| 328 | fn is_supported() -> Result<bool> { |
| 329 | Ok(ApcHal::try_get_service().is_some()) |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | impl IProtectedConfirmation for ApcManager { |
| 334 | fn presentPrompt( |
| 335 | &self, |
Andrei Homescu | 0a8291b | 2021-03-24 02:36:43 +0000 | [diff] [blame] | 336 | listener: &binder::Strong<dyn IConfirmationCallback>, |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 337 | prompt_text: &str, |
| 338 | extra_data: &[u8], |
| 339 | locale: &str, |
| 340 | ui_option_flags: i32, |
| 341 | ) -> BinderResult<()> { |
Hasini Gunasinghe | 5a893e8 | 2021-05-05 14:32:32 +0000 | [diff] [blame] | 342 | // presentPrompt can take more time than other operations. |
| 343 | let _wp = wd::watch_millis("IProtectedConfirmation::presentPrompt", 3000); |
David Drysdale | 5238d77 | 2024-06-07 15:12:10 +0100 | [diff] [blame] | 344 | map_or_log_err(self.present_prompt( |
| 345 | listener, |
| 346 | prompt_text, |
| 347 | extra_data, |
| 348 | locale, |
| 349 | ui_option_flags, |
| 350 | )) |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 351 | } |
Andrei Homescu | 0a8291b | 2021-03-24 02:36:43 +0000 | [diff] [blame] | 352 | fn cancelPrompt( |
| 353 | &self, |
| 354 | listener: &binder::Strong<dyn IConfirmationCallback>, |
| 355 | ) -> BinderResult<()> { |
David Drysdale | 541846b | 2024-05-23 13:16:07 +0100 | [diff] [blame] | 356 | let _wp = wd::watch("IProtectedConfirmation::cancelPrompt"); |
David Drysdale | 5238d77 | 2024-06-07 15:12:10 +0100 | [diff] [blame] | 357 | map_or_log_err(self.cancel_prompt(listener)) |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 358 | } |
| 359 | fn isSupported(&self) -> BinderResult<bool> { |
David Drysdale | 541846b | 2024-05-23 13:16:07 +0100 | [diff] [blame] | 360 | let _wp = wd::watch("IProtectedConfirmation::isSupported"); |
David Drysdale | 5238d77 | 2024-06-07 15:12:10 +0100 | [diff] [blame] | 361 | map_or_log_err(Self::is_supported()) |
Janis Danisevskis | 7a1cf38 | 2020-11-20 11:22:14 -0800 | [diff] [blame] | 362 | } |
| 363 | } |