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