Keystore 2.0: Implement APC service.
This patch implements the Android Protected Confirmation service in
Keystore 2.0. This includes a C++ wrapper around the HIDL confirmationui
interface which will stay a HIDL interface for now.
This patch also includes the new AIDL specification.
This patch lacks death listener registration b/176491050.
Bug: 159341464
Bug: 173546269
Test: None
Change-Id: Ida4af108e86b538ab64d1dea4809cfa3b36f74cd
diff --git a/keystore2/src/apc.rs b/keystore2/src/apc.rs
new file mode 100644
index 0000000..105e071
--- /dev/null
+++ b/keystore2/src/apc.rs
@@ -0,0 +1,366 @@
+// Copyright 2020, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// TODO The confirmation token is yet unused.
+#![allow(unused_variables)]
+
+//! This module implements the Android Protected Confirmation (APC) service as defined
+//! in the android.security.apc AIDL spec.
+
+use std::{
+ cmp::PartialEq,
+ collections::HashMap,
+ sync::{Arc, Mutex},
+};
+
+use crate::utils::{compat_2_response_code, ui_opts_2_compat};
+use android_security_apc::aidl::android::security::apc::{
+ IConfirmationCallback::IConfirmationCallback,
+ IProtectedConfirmation::{BnProtectedConfirmation, IProtectedConfirmation},
+ ResponseCode::ResponseCode,
+};
+use android_security_apc::binder::{
+ ExceptionCode, Interface, Result as BinderResult, SpIBinder, Status as BinderStatus,
+};
+use anyhow::{Context, Result};
+use binder::{IBinder, ThreadState};
+use keystore2_apc_compat::ApcHal;
+use keystore2_selinux as selinux;
+use std::time::{Duration, Instant};
+
+/// This is the main APC error type, it wraps binder exceptions and the
+/// APC ResponseCode.
+#[derive(Debug, thiserror::Error, PartialEq)]
+pub enum Error {
+ /// Wraps an Android Protected Confirmation (APC) response code as defined by the
+ /// android.security.apc AIDL interface specification.
+ #[error("Error::Rc({0:?})")]
+ Rc(ResponseCode),
+ /// Wraps a Binder exception code other than a service specific exception.
+ #[error("Binder exception code {0:?}, {1:?}")]
+ Binder(ExceptionCode, i32),
+}
+
+impl Error {
+ /// Short hand for `Error::Rc(ResponseCode::SYSTEM_ERROR)`
+ pub fn sys() -> Self {
+ Error::Rc(ResponseCode::SYSTEM_ERROR)
+ }
+
+ /// Short hand for `Error::Rc(ResponseCode::OPERATION_PENDING)`
+ pub fn pending() -> Self {
+ Error::Rc(ResponseCode::OPERATION_PENDING)
+ }
+
+ /// Short hand for `Error::Rc(ResponseCode::CANCELLED)`
+ pub fn cancelled() -> Self {
+ Error::Rc(ResponseCode::CANCELLED)
+ }
+
+ /// Short hand for `Error::Rc(ResponseCode::ABORTED)`
+ pub fn aborted() -> Self {
+ Error::Rc(ResponseCode::ABORTED)
+ }
+
+ /// Short hand for `Error::Rc(ResponseCode::IGNORED)`
+ pub fn ignored() -> Self {
+ Error::Rc(ResponseCode::IGNORED)
+ }
+
+ /// Short hand for `Error::Rc(ResponseCode::UNIMPLEMENTED)`
+ pub fn unimplemented() -> Self {
+ Error::Rc(ResponseCode::UNIMPLEMENTED)
+ }
+}
+
+/// This function should be used by confirmation service calls to translate error conditions
+/// into service specific exceptions.
+///
+/// All error conditions get logged by this function.
+///
+/// `Error::Rc(x)` variants get mapped onto a service specific error code of `x`.
+/// `selinux::Error::perm()` is mapped on `ResponseCode::PERMISSION_DENIED`.
+///
+/// All non `Error` error conditions get mapped onto ResponseCode::SYSTEM_ERROR`.
+///
+/// `handle_ok` will be called if `result` is `Ok(value)` where `value` will be passed
+/// as argument to `handle_ok`. `handle_ok` must generate a `BinderResult<T>`, but it
+/// typically returns Ok(value).
+pub fn map_or_log_err<T, U, F>(result: Result<U>, handle_ok: F) -> BinderResult<T>
+where
+ F: FnOnce(U) -> BinderResult<T>,
+{
+ result.map_or_else(
+ |e| {
+ log::error!("{:#?}", e);
+ let root_cause = e.root_cause();
+ let rc = match root_cause.downcast_ref::<Error>() {
+ Some(Error::Rc(rcode)) => rcode.0,
+ Some(Error::Binder(_, _)) => ResponseCode::SYSTEM_ERROR.0,
+ None => match root_cause.downcast_ref::<selinux::Error>() {
+ Some(selinux::Error::PermissionDenied) => ResponseCode::PERMISSION_DENIED.0,
+ _ => ResponseCode::SYSTEM_ERROR.0,
+ },
+ };
+ Err(BinderStatus::new_service_specific_error(rc, None))
+ },
+ handle_ok,
+ )
+}
+
+/// Rate info records how many failed attempts a client has made to display a protected
+/// confirmation prompt. Clients are penalized for attempts that get declined by the user
+/// or attempts that get aborted by the client itself.
+///
+/// After the third failed attempt the client has to cool down for 30 seconds before it
+/// it can retry. After the sixth failed attempt, the time doubles with every failed attempt
+/// until it goes into saturation at 24h.
+///
+/// A successful user prompt resets the counter.
+#[derive(Debug, Clone)]
+struct RateInfo {
+ counter: u32,
+ timestamp: Instant,
+}
+
+impl RateInfo {
+ const ONE_DAY: Duration = Duration::from_secs(60u64 * 60u64 * 24u64);
+
+ fn get_remaining_back_off(&self) -> Option<Duration> {
+ let back_off = match self.counter {
+ // The first three attempts come without penalty.
+ 0..=2 => return None,
+ // The next three attempts are are penalized with 30 seconds back off time.
+ 3..=5 => Duration::from_secs(30),
+ // After that we double the back off time the with every additional attempt
+ // until we reach 1024m (~17h).
+ 6..=16 => Duration::from_secs(60)
+ .checked_mul(1u32 << (self.counter - 6))
+ .unwrap_or(Self::ONE_DAY),
+ // After that we cap of at 24h between attempts.
+ _ => Self::ONE_DAY,
+ };
+ let elapsed = self.timestamp.elapsed();
+ // This does exactly what we want.
+ // `back_off - elapsed` is the remaining back off duration or None if elapsed is larger
+ // than back_off. Also, this operation cannot overflow as long as elapsed is less than
+ // back_off, which is all that we care about.
+ back_off.checked_sub(elapsed)
+ }
+}
+
+impl Default for RateInfo {
+ fn default() -> Self {
+ Self { counter: 0u32, timestamp: Instant::now() }
+ }
+}
+
+/// The APC session state represents the state of an APC session.
+struct ApcSessionState {
+ /// A reference to the APC HAL backend.
+ hal: Arc<ApcHal>,
+ /// The client callback object.
+ cb: SpIBinder,
+ /// The uid of the owner of this APC session.
+ uid: u32,
+ /// The time when this session was started.
+ start: Instant,
+ /// This is set when the client calls abort.
+ /// This is used by the rate limiting logic to determine
+ /// if the client needs to be penalized for this attempt.
+ client_aborted: bool,
+}
+
+#[derive(Default)]
+struct ApcState {
+ session: Option<ApcSessionState>,
+ rate_limiting: HashMap<u32, RateInfo>,
+}
+
+/// Implementation of the APC service.
+pub struct ApcManager {
+ state: Arc<Mutex<ApcState>>,
+}
+
+impl Interface for ApcManager {}
+
+impl ApcManager {
+ /// Create a new instance of the Android Protected Confirmation service.
+ pub fn new_native_binder() -> Result<impl IProtectedConfirmation> {
+ let result = BnProtectedConfirmation::new_binder(Self {
+ state: Arc::new(Mutex::new(Default::default())),
+ });
+ result.as_binder().set_requesting_sid(true);
+ Ok(result)
+ }
+
+ fn result(
+ state: Arc<Mutex<ApcState>>,
+ rc: u32,
+ data_confirmed: Option<&[u8]>,
+ confirmation_token: Option<&[u8]>,
+ ) {
+ let mut state = state.lock().unwrap();
+ let (callback, uid, start, client_aborted) = match state.session.take() {
+ None => return, // Nothing to do
+ Some(ApcSessionState { cb: callback, uid, start, client_aborted, .. }) => {
+ (callback, uid, start, client_aborted)
+ }
+ };
+
+ let rc = compat_2_response_code(rc);
+
+ // Update rate limiting information.
+ match (rc, client_aborted) {
+ // If the user confirmed the dialog.
+ (ResponseCode::OK, _) => {
+ // Reset counter.
+ state.rate_limiting.remove(&uid);
+ // TODO at this point we need to send the confirmation token to where keystore can
+ // use it.
+ }
+ // If cancelled by the user or if aborted by the client.
+ (ResponseCode::CANCELLED, _) | (ResponseCode::ABORTED, true) => {
+ // Penalize.
+ let mut rate_info = state.rate_limiting.entry(uid).or_default();
+ rate_info.counter += 1;
+ rate_info.timestamp = start;
+ }
+ // In any other case this try does not count at all.
+ _ => {}
+ }
+ drop(state);
+
+ if let Ok(listener) = callback.into_interface::<dyn IConfirmationCallback>() {
+ if let Err(e) = listener.onCompleted(rc, data_confirmed) {
+ log::error!(
+ "In ApcManagerCallback::result: Reporting completion to client failed {:?}",
+ e
+ )
+ }
+ } else {
+ log::error!("In ApcManagerCallback::result: SpIBinder is not a IConfirmationCallback.");
+ }
+ }
+
+ fn present_prompt(
+ &self,
+ listener: &dyn IConfirmationCallback,
+ prompt_text: &str,
+ extra_data: &[u8],
+ locale: &str,
+ ui_option_flags: i32,
+ ) -> Result<()> {
+ let mut state = self.state.lock().unwrap();
+ if state.session.is_some() {
+ return Err(Error::pending())
+ .context("In ApcManager::present_prompt: Session pending.");
+ }
+
+ // Perform rate limiting.
+ let uid = ThreadState::get_calling_uid();
+ match state.rate_limiting.get(&uid) {
+ None => {}
+ Some(rate_info) => {
+ if let Some(back_off) = rate_info.get_remaining_back_off() {
+ return Err(Error::sys()).context(format!(
+ "In ApcManager::present_prompt: Cooling down. Remaining back-off: {}s",
+ back_off.as_secs()
+ ));
+ }
+ }
+ }
+
+ let hal = ApcHal::try_get_service();
+ let hal = match hal {
+ None => {
+ return Err(Error::unimplemented())
+ .context("In ApcManager::present_prompt: APC not supported.")
+ }
+ Some(h) => Arc::new(h),
+ };
+
+ let ui_opts = ui_opts_2_compat(ui_option_flags);
+
+ let state_clone = self.state.clone();
+ hal.prompt_user_confirmation(
+ prompt_text,
+ extra_data,
+ locale,
+ ui_opts,
+ |rc, data_confirmed, confirmation_token| {
+ Self::result(state_clone, rc, data_confirmed, confirmation_token)
+ },
+ )
+ .map_err(|rc| Error::Rc(compat_2_response_code(rc)))
+ .context("In present_prompt: Failed to present prompt.")?;
+ state.session = Some(ApcSessionState {
+ hal,
+ cb: listener.as_binder(),
+ uid,
+ start: Instant::now(),
+ client_aborted: false,
+ });
+ Ok(())
+ }
+
+ fn cancel_prompt(&self, listener: &dyn IConfirmationCallback) -> Result<()> {
+ let mut state = self.state.lock().unwrap();
+ let hal = match &mut state.session {
+ None => {
+ return Err(Error::ignored())
+ .context("In cancel_prompt: Attempt to cancel non existing session. Ignoring.")
+ }
+ Some(session) => {
+ if session.cb != listener.as_binder() {
+ return Err(Error::ignored()).context(concat!(
+ "In cancel_prompt: Attempt to cancel session not belonging to caller. ",
+ "Ignoring."
+ ));
+ }
+ session.client_aborted = true;
+ session.hal.clone()
+ }
+ };
+ drop(state);
+ hal.abort();
+ Ok(())
+ }
+
+ fn is_supported() -> Result<bool> {
+ Ok(ApcHal::try_get_service().is_some())
+ }
+}
+
+impl IProtectedConfirmation for ApcManager {
+ fn presentPrompt(
+ &self,
+ listener: &dyn IConfirmationCallback,
+ prompt_text: &str,
+ extra_data: &[u8],
+ locale: &str,
+ ui_option_flags: i32,
+ ) -> BinderResult<()> {
+ map_or_log_err(
+ self.present_prompt(listener, prompt_text, extra_data, locale, ui_option_flags),
+ Ok,
+ )
+ }
+ fn cancelPrompt(&self, listener: &dyn IConfirmationCallback) -> BinderResult<()> {
+ map_or_log_err(self.cancel_prompt(listener), Ok)
+ }
+ fn isSupported(&self) -> BinderResult<bool> {
+ map_or_log_err(Self::is_supported(), Ok)
+ }
+}
diff --git a/keystore2/src/keystore2_main.rs b/keystore2/src/keystore2_main.rs
index 2916549..59e5972 100644
--- a/keystore2/src/keystore2_main.rs
+++ b/keystore2/src/keystore2_main.rs
@@ -15,11 +15,13 @@
//! This crate implements the Keystore 2.0 service entry point.
use binder::Interface;
+use keystore2::apc::ApcManager;
use keystore2::service::KeystoreService;
use log::{error, info};
use std::panic;
static KS2_SERVICE_NAME: &str = "android.system.keystore2";
+static APC_SERVICE_NAME: &str = "android.security.apc";
/// Keystore 2.0 takes one argument which is a path indicating its designated working directory.
fn main() {
@@ -55,6 +57,13 @@
panic!("Failed to register service {} because of {:?}.", KS2_SERVICE_NAME, e);
});
+ let apc_service = ApcManager::new_native_binder().unwrap_or_else(|e| {
+ panic!("Failed to create service {} because of {:?}.", APC_SERVICE_NAME, e);
+ });
+ binder::add_service(APC_SERVICE_NAME, apc_service.as_binder()).unwrap_or_else(|e| {
+ panic!("Failed to register service {} because of {:?}.", APC_SERVICE_NAME, e);
+ });
+
info!("Successfully registered Keystore 2.0 service.");
info!("Starting thread pool now.");
diff --git a/keystore2/src/lib.rs b/keystore2/src/lib.rs
index 5ad77d1..3fb938c 100644
--- a/keystore2/src/lib.rs
+++ b/keystore2/src/lib.rs
@@ -14,6 +14,7 @@
//! This crate implements the Android Keystore 2.0 service.
+pub mod apc;
pub mod auth_token_handler;
pub mod database;
pub mod enforcements;
diff --git a/keystore2/src/utils.rs b/keystore2/src/utils.rs
index d50f70e..eab9b4d 100644
--- a/keystore2/src/utils.rs
+++ b/keystore2/src/utils.rs
@@ -25,11 +25,20 @@
use android_hardware_security_keymint::aidl::android::hardware::security::keymint::{
KeyCharacteristics::KeyCharacteristics, SecurityLevel::SecurityLevel,
};
+use android_security_apc::aidl::android::security::apc::{
+ IProtectedConfirmation::{FLAG_UI_OPTION_INVERTED, FLAG_UI_OPTION_MAGNIFIED},
+ ResponseCode::ResponseCode as ApcResponseCode,
+};
use android_system_keystore2::aidl::android::system::keystore2::{
Authorization::Authorization, KeyDescriptor::KeyDescriptor,
};
use anyhow::{anyhow, Context};
use binder::{FromIBinder, SpIBinder, ThreadState};
+use keystore2_apc_compat::{
+ ApcCompatUiOptions, APC_COMPAT_ERROR_ABORTED, APC_COMPAT_ERROR_CANCELLED,
+ APC_COMPAT_ERROR_IGNORED, APC_COMPAT_ERROR_OK, APC_COMPAT_ERROR_OPERATION_PENDING,
+ APC_COMPAT_ERROR_SYSTEM_ERROR,
+};
use std::convert::TryFrom;
use std::sync::Mutex;
@@ -146,6 +155,31 @@
i64::try_from(current_time.tv_sec).unwrap()
}
+/// Converts a response code as returned by the Android Protected Confirmation HIDL compatibility
+/// module (keystore2_apc_compat) into a ResponseCode as defined by the APC AIDL
+/// (android.security.apc) spec.
+pub fn compat_2_response_code(rc: u32) -> ApcResponseCode {
+ match rc {
+ APC_COMPAT_ERROR_OK => ApcResponseCode::OK,
+ APC_COMPAT_ERROR_CANCELLED => ApcResponseCode::CANCELLED,
+ APC_COMPAT_ERROR_ABORTED => ApcResponseCode::ABORTED,
+ APC_COMPAT_ERROR_OPERATION_PENDING => ApcResponseCode::OPERATION_PENDING,
+ APC_COMPAT_ERROR_IGNORED => ApcResponseCode::IGNORED,
+ APC_COMPAT_ERROR_SYSTEM_ERROR => ApcResponseCode::SYSTEM_ERROR,
+ _ => ApcResponseCode::SYSTEM_ERROR,
+ }
+}
+
+/// Converts the UI Options flags as defined by the APC AIDL (android.security.apc) spec into
+/// UI Options flags as defined by the Android Protected Confirmation HIDL compatibility
+/// module (keystore2_apc_compat).
+pub fn ui_opts_2_compat(opt: i32) -> ApcCompatUiOptions {
+ ApcCompatUiOptions {
+ inverted: (opt & FLAG_UI_OPTION_INVERTED) != 0,
+ magnified: (opt & FLAG_UI_OPTION_MAGNIFIED) != 0,
+ }
+}
+
/// AID offset for uid space partitioning.
/// TODO: Replace with bindgen generated from libcutils. b/175619259
pub const AID_USER_OFFSET: u32 = 100000;