Keystore 2.0: Extend the functionality of the Vpn profile store.

It turns out there are more clients that use Keystore in a creative
way. This patch renames the VpnProfileStore to LegacyKeystore and
extends the functionality such that it allows access to all blobs with
alias prefixes that were not known to Keystore. It also brings back the
option to specify a uid argument. Specifically, for AID_SYSTEM to
manipulate the WIFI namespace.

Test: TBD
Bug: 191373871
Merged-In: Iaf81e7ccaee3c09a465dcec0fd5899b781c31db5
Change-Id: Iaf81e7ccaee3c09a465dcec0fd5899b781c31db5
This commit is contained in:
Janis Danisevskis 2021-06-14 14:18:20 -07:00
parent 30257fa908
commit 3eb829da4d
8 changed files with 304 additions and 238 deletions

View file

@ -108,7 +108,7 @@ rust_binary {
"libbinder_rs", "libbinder_rs",
"libkeystore2", "libkeystore2",
"liblog_rust", "liblog_rust",
"libvpnprofilestore-rust", "liblegacykeystore-rust",
], ],
init_rc: ["keystore2.rc"], init_rc: ["keystore2.rc"],

View file

@ -151,8 +151,8 @@ aidl_interface {
} }
aidl_interface { aidl_interface {
name: "android.security.vpnprofilestore", name: "android.security.legacykeystore",
srcs: [ "android/security/vpnprofilestore/*.aidl" ], srcs: [ "android/security/legacykeystore/*.aidl" ],
unstable: true, unstable: true,
backend: { backend: {
java: { java: {
@ -162,6 +162,10 @@ aidl_interface {
rust: { rust: {
enabled: true, enabled: true,
}, },
ndk: {
enabled: true,
apps_enabled: false,
}
}, },
} }

View file

@ -0,0 +1,98 @@
/*
* Copyright (C) 2021 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.
*/
package android.security.legacykeystore;
/**
* Internal interface for accessing and storing legacy keystore blobs.
* Before Android S, Keystore offered a key-value store that was intended for storing
* data associated with certain types of keys. E.g., public certificates for asymmetric keys.
* This key value store no longer exists as part of the Keystore 2.0 protocol.
* However, there are some clients that used Keystore in an unintended way.
* This interface exists to give these clients a grace period to migrate their keys
* out of legacy keystore. In Android S, this legacy keystore may be used as keystore was
* used in earlier versions, and provides access to entries that were put into keystore
* before Android S.
*
* DEPRECATION NOTICE: In Android T, the `put` function is slated to be removed.
* This will allow clients to use the `get`, `list`, and `remove` API to migrate blobs out
* of legacy keystore.
* @hide
*/
interface ILegacyKeystore {
/**
* Special value indicating the callers uid.
*/
const int UID_SELF = -1;
/**
* Service specific error code indicating that an unexpected system error occurred.
*/
const int ERROR_SYSTEM_ERROR = 4;
/**
* Service specific error code indicating that the caller does not have the
* right to access the requested uid.
*/
const int ERROR_PERMISSION_DENIED = 6;
/**
* Service specific error code indicating that the entry was not found.
*/
const int ERROR_ENTRY_NOT_FOUND = 7;
/**
* Returns the blob stored under the given name.
*
* @param alias name of the blob entry.
* @param uid designates the legacy namespace. Specify UID_SELF for the caller's namespace.
* @return The unstructured blob that was passed as blob parameter into put()
*/
byte[] get(in String alias, int uid);
/**
* Stores one entry as unstructured blob under the given alias.
* Overwrites existing entries with the same alias.
*
* @param alias name of the new entry.
* @param uid designates the legacy namespace. Specify UID_SELF for the caller's namespace.
* @param blob the payload of the new entry.
*
* IMPORTANT DEPRECATION NOTICE: This function is slated to be removed in Android T.
* Do not add new callers. The remaining functionality will remain for the purpose
* of migrating legacy configuration out.
*/
void put(in String alias, int uid, in byte[] blob);
/**
* Deletes the entry under the given alias.
*
* @param alias name of the entry to be removed.
* @param uid designates the legacy namespace of the entry. Specify UID_SELF for the caller's
* namespace.
*/
void remove(in String alias, int uid);
/**
* Returns a list of aliases of entries stored. The list is filtered by prefix.
* The resulting strings are the full aliases including the prefix.
*
* @param prefix used to filter results.
* @param uid legacy namespace to list. Specify UID_SELF for caller's namespace.
*/
String[] list(in String prefix, int uid);
}

View file

@ -1,57 +0,0 @@
/*
* Copyright (C) 2021 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.
*/
package android.security.vpnprofilestore;
/**
* Internal interface for accessing and storing VPN profiles.
* @hide
*/
interface IVpnProfileStore {
/**
* Service specific error code indicating that the profile was not found.
*/
const int ERROR_PROFILE_NOT_FOUND = 1;
/**
* Service specific error code indicating that an unexpected system error occurred.
*/
const int ERROR_SYSTEM_ERROR = 2;
/**
* Returns the profile stored under the given alias.
*
* @param alias name of the profile.
* @return The unstructured blob that was passed as profile parameter into put()
*/
byte[] get(in String alias);
/**
* Stores one profile as unstructured blob under the given alias.
*/
void put(in String alias, in byte[] profile);
/**
* Deletes the profile under the given alias.
*/
void remove(in String alias);
/**
* Returns a list of aliases of profiles stored. The list is filtered by prefix.
* The resulting strings are the full aliases including the prefix.
*/
String[] list(in String prefix);
}

View file

@ -22,13 +22,13 @@ package {
} }
rust_library { rust_library {
name: "libvpnprofilestore-rust", name: "liblegacykeystore-rust",
crate_name: "vpnprofilestore", crate_name: "legacykeystore",
srcs: [ srcs: [
"lib.rs", "lib.rs",
], ],
rustlibs: [ rustlibs: [
"android.security.vpnprofilestore-rust", "android.security.legacykeystore-rust",
"libanyhow", "libanyhow",
"libbinder_rs", "libbinder_rs",
"libkeystore2", "libkeystore2",
@ -39,13 +39,13 @@ rust_library {
} }
rust_test { rust_test {
name: "vpnprofilestore_test", name: "legacykeystore_test",
crate_name: "vpnprofilestore", crate_name: "legacykeystore",
srcs: ["lib.rs"], srcs: ["lib.rs"],
test_suites: ["general-tests"], test_suites: ["general-tests"],
auto_gen_config: true, auto_gen_config: true,
rustlibs: [ rustlibs: [
"android.security.vpnprofilestore-rust", "android.security.legacykeystore-rust",
"libanyhow", "libanyhow",
"libbinder_rs", "libbinder_rs",
"libkeystore2", "libkeystore2",

View file

@ -12,13 +12,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! Implements the android.security.vpnprofilestore interface. //! Implements the android.security.legacykeystore interface.
use android_security_vpnprofilestore::aidl::android::security::vpnprofilestore::{ use android_security_legacykeystore::aidl::android::security::legacykeystore::{
IVpnProfileStore::BnVpnProfileStore, IVpnProfileStore::IVpnProfileStore, ILegacyKeystore::BnLegacyKeystore, ILegacyKeystore::ILegacyKeystore,
IVpnProfileStore::ERROR_PROFILE_NOT_FOUND, IVpnProfileStore::ERROR_SYSTEM_ERROR, ILegacyKeystore::ERROR_ENTRY_NOT_FOUND, ILegacyKeystore::ERROR_PERMISSION_DENIED,
ILegacyKeystore::ERROR_SYSTEM_ERROR, ILegacyKeystore::UID_SELF,
}; };
use android_security_vpnprofilestore::binder::{ use android_security_legacykeystore::binder::{
BinderFeatures, ExceptionCode, Result as BinderResult, Status as BinderStatus, Strong, BinderFeatures, ExceptionCode, Result as BinderResult, Status as BinderStatus, Strong,
ThreadState, ThreadState,
}; };
@ -42,7 +43,7 @@ impl DB {
conn: Connection::open(db_file).context("Failed to initialize SQLite connection.")?, conn: Connection::open(db_file).context("Failed to initialize SQLite connection.")?,
}; };
db.init_tables().context("Trying to initialize vpnstore db.")?; db.init_tables().context("Trying to initialize legacy keystore db.")?;
Ok(db) Ok(db)
} }
@ -110,11 +111,11 @@ impl DB {
}) })
} }
fn put(&mut self, caller_uid: u32, alias: &str, profile: &[u8]) -> Result<()> { fn put(&mut self, caller_uid: u32, alias: &str, entry: &[u8]) -> Result<()> {
self.with_transaction(TransactionBehavior::Immediate, |tx| { self.with_transaction(TransactionBehavior::Immediate, |tx| {
tx.execute( tx.execute(
"INSERT OR REPLACE INTO profiles (owner, alias, profile) values (?, ?, ?)", "INSERT OR REPLACE INTO profiles (owner, alias, profile) values (?, ?, ?)",
params![caller_uid, alias, profile,], params![caller_uid, alias, entry,],
) )
.context("In put: Failed to insert or replace.")?; .context("In put: Failed to insert or replace.")?;
Ok(()) Ok(())
@ -129,7 +130,7 @@ impl DB {
|row| row.get(0), |row| row.get(0),
) )
.optional() .optional()
.context("In get: failed loading profile.") .context("In get: failed loading entry.")
}) })
} }
@ -145,11 +146,11 @@ impl DB {
} }
} }
/// This is the main VpnProfileStore error type, it wraps binder exceptions and the /// This is the main LegacyKeystore error type, it wraps binder exceptions and the
/// VnpStore errors. /// LegacyKeystore errors.
#[derive(Debug, thiserror::Error, PartialEq)] #[derive(Debug, thiserror::Error, PartialEq)]
pub enum Error { pub enum Error {
/// Wraps a VpnProfileStore error code. /// Wraps a LegacyKeystore error code.
#[error("Error::Error({0:?})")] #[error("Error::Error({0:?})")]
Error(i32), Error(i32),
/// Wraps a Binder exception code other than a service specific exception. /// Wraps a Binder exception code other than a service specific exception.
@ -163,16 +164,21 @@ impl Error {
Error::Error(ERROR_SYSTEM_ERROR) Error::Error(ERROR_SYSTEM_ERROR)
} }
/// Short hand for `Error::Error(ERROR_PROFILE_NOT_FOUND)` /// Short hand for `Error::Error(ERROR_ENTRY_NOT_FOUND)`
pub fn not_found() -> Self { pub fn not_found() -> Self {
Error::Error(ERROR_PROFILE_NOT_FOUND) Error::Error(ERROR_ENTRY_NOT_FOUND)
}
/// Short hand for `Error::Error(ERROR_PERMISSION_DENIED)`
pub fn perm() -> Self {
Error::Error(ERROR_PERMISSION_DENIED)
} }
} }
/// This function should be used by vpnprofilestore service calls to translate error conditions /// This function should be used by legacykeystore service calls to translate error conditions
/// into service specific exceptions. /// into service specific exceptions.
/// ///
/// All error conditions get logged by this function, except for ERROR_PROFILE_NOT_FOUND error. /// All error conditions get logged by this function, except for ERROR_ENTRY_NOT_FOUND error.
/// ///
/// `Error::Error(x)` variants get mapped onto a service specific error code of `x`. /// `Error::Error(x)` variants get mapped onto a service specific error code of `x`.
/// ///
@ -189,8 +195,8 @@ where
|e| { |e| {
let root_cause = e.root_cause(); let root_cause = e.root_cause();
let (rc, log_error) = match root_cause.downcast_ref::<Error>() { let (rc, log_error) = match root_cause.downcast_ref::<Error>() {
// Make the profile not found errors silent. // Make the entry not found errors silent.
Some(Error::Error(ERROR_PROFILE_NOT_FOUND)) => (ERROR_PROFILE_NOT_FOUND, false), Some(Error::Error(ERROR_ENTRY_NOT_FOUND)) => (ERROR_ENTRY_NOT_FOUND, false),
Some(Error::Error(e)) => (*e, true), Some(Error::Error(e)) => (*e, true),
Some(Error::Binder(_, _)) | None => (ERROR_SYSTEM_ERROR, true), Some(Error::Binder(_, _)) | None => (ERROR_SYSTEM_ERROR, true),
}; };
@ -203,8 +209,8 @@ where
) )
} }
/// Implements IVpnProfileStore AIDL interface. /// Implements ILegacyKeystore AIDL interface.
pub struct VpnProfileStore { pub struct LegacyKeystore {
db_path: PathBuf, db_path: PathBuf,
async_task: AsyncTask, async_task: AsyncTask,
} }
@ -215,72 +221,93 @@ struct AsyncState {
db_path: PathBuf, db_path: PathBuf,
} }
impl VpnProfileStore { impl LegacyKeystore {
/// Creates a new VpnProfileStore instance. /// Note: The filename was chosen before the purpose of this module was extended.
pub fn new_native_binder(path: &Path) -> Strong<dyn IVpnProfileStore> { /// It is kept for backward compatibility with early adopters.
const LEGACY_KEYSTORE_FILE_NAME: &'static str = "vpnprofilestore.sqlite";
/// Creates a new LegacyKeystore instance.
pub fn new_native_binder(path: &Path) -> Strong<dyn ILegacyKeystore> {
let mut db_path = path.to_path_buf(); let mut db_path = path.to_path_buf();
db_path.push("vpnprofilestore.sqlite"); db_path.push(Self::LEGACY_KEYSTORE_FILE_NAME);
let result = Self { db_path, async_task: Default::default() }; let result = Self { db_path, async_task: Default::default() };
result.init_shelf(path); result.init_shelf(path);
BnVpnProfileStore::new_binder(result, BinderFeatures::default()) BnLegacyKeystore::new_binder(result, BinderFeatures::default())
} }
fn open_db(&self) -> Result<DB> { fn open_db(&self) -> Result<DB> {
DB::new(&self.db_path).context("In open_db: Failed to open db.") DB::new(&self.db_path).context("In open_db: Failed to open db.")
} }
fn get(&self, alias: &str) -> Result<Vec<u8>> { fn get_effective_uid(uid: i32) -> Result<u32> {
let mut db = self.open_db().context("In get.")?; const AID_SYSTEM: u32 = 1000;
const AID_WIFI: u32 = 1010;
let calling_uid = ThreadState::get_calling_uid(); let calling_uid = ThreadState::get_calling_uid();
let uid = uid as u32;
if let Some(profile) = if uid == UID_SELF as u32 || uid == calling_uid {
db.get(calling_uid, alias).context("In get: Trying to load profile from DB.")? Ok(calling_uid)
{ } else if calling_uid == AID_SYSTEM && uid == AID_WIFI {
return Ok(profile); // The only exception for legacy reasons is allowing SYSTEM to access
// the WIFI namespace.
// IMPORTANT: If you attempt to add more exceptions, it means you are adding
// more callers to this deprecated feature. DON'T!
Ok(AID_WIFI)
} else {
Err(Error::perm()).with_context(|| {
format!("In get_effective_uid: caller: {}, requested uid: {}.", calling_uid, uid)
})
} }
if self.get_legacy(calling_uid, alias).context("In get: Trying to migrate legacy blob.")? { }
fn get(&self, alias: &str, uid: i32) -> Result<Vec<u8>> {
let mut db = self.open_db().context("In get.")?;
let uid = Self::get_effective_uid(uid).context("In get.")?;
if let Some(entry) = db.get(uid, alias).context("In get: Trying to load entry from DB.")? {
return Ok(entry);
}
if self.get_legacy(uid, alias).context("In get: Trying to migrate legacy blob.")? {
// If we were able to migrate a legacy blob try again. // If we were able to migrate a legacy blob try again.
if let Some(profile) = if let Some(entry) =
db.get(calling_uid, alias).context("In get: Trying to load profile from DB.")? db.get(uid, alias).context("In get: Trying to load entry from DB.")?
{ {
return Ok(profile); return Ok(entry);
} }
} }
Err(Error::not_found()).context("In get: No such profile.") Err(Error::not_found()).context("In get: No such entry.")
} }
fn put(&self, alias: &str, profile: &[u8]) -> Result<()> { fn put(&self, alias: &str, uid: i32, entry: &[u8]) -> Result<()> {
let calling_uid = ThreadState::get_calling_uid(); let uid = Self::get_effective_uid(uid).context("In put.")?;
// In order to make sure that we don't have stale legacy profiles, make sure they are // In order to make sure that we don't have stale legacy entries, make sure they are
// migrated before replacing them. // migrated before replacing them.
let _ = self.get_legacy(calling_uid, alias); let _ = self.get_legacy(uid, alias);
let mut db = self.open_db().context("In put.")?; let mut db = self.open_db().context("In put.")?;
db.put(calling_uid, alias, profile).context("In put: Trying to insert profile into DB.") db.put(uid, alias, entry).context("In put: Trying to insert entry into DB.")
} }
fn remove(&self, alias: &str) -> Result<()> { fn remove(&self, alias: &str, uid: i32) -> Result<()> {
let calling_uid = ThreadState::get_calling_uid(); let uid = Self::get_effective_uid(uid).context("In remove.")?;
let mut db = self.open_db().context("In remove.")?; let mut db = self.open_db().context("In remove.")?;
// In order to make sure that we don't have stale legacy profiles, make sure they are // In order to make sure that we don't have stale legacy entries, make sure they are
// migrated before removing them. // migrated before removing them.
let _ = self.get_legacy(calling_uid, alias); let _ = self.get_legacy(uid, alias);
let removed = db let removed =
.remove(calling_uid, alias) db.remove(uid, alias).context("In remove: Trying to remove entry from DB.")?;
.context("In remove: Trying to remove profile from DB.")?;
if removed { if removed {
Ok(()) Ok(())
} else { } else {
Err(Error::not_found()).context("In remove: No such profile.") Err(Error::not_found()).context("In remove: No such entry.")
} }
} }
fn list(&self, prefix: &str) -> Result<Vec<String>> { fn list(&self, prefix: &str, uid: i32) -> Result<Vec<String>> {
let mut db = self.open_db().context("In list.")?; let mut db = self.open_db().context("In list.")?;
let calling_uid = ThreadState::get_calling_uid(); let uid = Self::get_effective_uid(uid).context("In list.")?;
let mut result = self.list_legacy(calling_uid).context("In list.")?; let mut result = self.list_legacy(uid).context("In list.")?;
result result.append(&mut db.list(uid).context("In list: Trying to get list of entries.")?);
.append(&mut db.list(calling_uid).context("In list: Trying to get list of profiles.")?);
result = result.into_iter().filter(|s| s.starts_with(prefix)).collect(); result = result.into_iter().filter(|s| s.starts_with(prefix)).collect();
result.sort_unstable(); result.sort_unstable();
result.dedup(); result.dedup();
@ -291,7 +318,7 @@ impl VpnProfileStore {
let mut db_path = path.to_path_buf(); let mut db_path = path.to_path_buf();
self.async_task.queue_hi(move |shelf| { self.async_task.queue_hi(move |shelf| {
let legacy_loader = LegacyBlobLoader::new(&db_path); let legacy_loader = LegacyBlobLoader::new(&db_path);
db_path.push("vpnprofilestore.sqlite"); db_path.push(Self::LEGACY_KEYSTORE_FILE_NAME);
shelf.put(AsyncState { legacy_loader, db_path, recently_imported: Default::default() }); shelf.put(AsyncState { legacy_loader, db_path, recently_imported: Default::default() });
}) })
@ -313,8 +340,8 @@ impl VpnProfileStore {
self.do_serialized(move |state| { self.do_serialized(move |state| {
state state
.legacy_loader .legacy_loader
.list_vpn_profiles(uid) .list_legacy_keystore_entries(uid)
.context("Trying to list legacy vnp profiles.") .context("Trying to list legacy keystore entries.")
}) })
.context("In list_legacy.") .context("In list_legacy.")
} }
@ -327,8 +354,8 @@ impl VpnProfileStore {
} }
let mut db = DB::new(&state.db_path).context("In open_db: Failed to open db.")?; let mut db = DB::new(&state.db_path).context("In open_db: Failed to open db.")?;
let migrated = let migrated =
Self::migrate_one_legacy_profile(uid, &alias, &state.legacy_loader, &mut db) Self::migrate_one_legacy_entry(uid, &alias, &state.legacy_loader, &mut db)
.context("Trying to migrate legacy vpn profile.")?; .context("Trying to migrate legacy keystore entries.")?;
if migrated { if migrated {
state.recently_imported.insert((uid, alias)); state.recently_imported.insert((uid, alias));
} }
@ -337,21 +364,21 @@ impl VpnProfileStore {
.context("In get_legacy.") .context("In get_legacy.")
} }
fn migrate_one_legacy_profile( fn migrate_one_legacy_entry(
uid: u32, uid: u32,
alias: &str, alias: &str,
legacy_loader: &LegacyBlobLoader, legacy_loader: &LegacyBlobLoader,
db: &mut DB, db: &mut DB,
) -> Result<bool> { ) -> Result<bool> {
let blob = legacy_loader let blob = legacy_loader
.read_vpn_profile(uid, alias) .read_legacy_keystore_entry(uid, alias)
.context("In migrate_one_legacy_profile: Trying to read legacy vpn profile.")?; .context("In migrate_one_legacy_entry: Trying to read legacy keystore entry.")?;
if let Some(profile) = blob { if let Some(entry) = blob {
db.put(uid, alias, &profile) db.put(uid, alias, &entry)
.context("In migrate_one_legacy_profile: Trying to insert profile into DB.")?; .context("In migrate_one_legacy_entry: Trying to insert entry into DB.")?;
legacy_loader legacy_loader
.remove_vpn_profile(uid, alias) .remove_legacy_keystore_entry(uid, alias)
.context("In migrate_one_legacy_profile: Trying to delete legacy profile.")?; .context("In migrate_one_legacy_entry: Trying to delete legacy keystore entry.")?;
Ok(true) Ok(true)
} else { } else {
Ok(false) Ok(false)
@ -359,24 +386,24 @@ impl VpnProfileStore {
} }
} }
impl binder::Interface for VpnProfileStore {} impl binder::Interface for LegacyKeystore {}
impl IVpnProfileStore for VpnProfileStore { impl ILegacyKeystore for LegacyKeystore {
fn get(&self, alias: &str) -> BinderResult<Vec<u8>> { fn get(&self, alias: &str, uid: i32) -> BinderResult<Vec<u8>> {
let _wp = wd::watch_millis("IVpnProfileStore::get", 500); let _wp = wd::watch_millis("ILegacyKeystore::get", 500);
map_or_log_err(self.get(alias), Ok) map_or_log_err(self.get(alias, uid), Ok)
} }
fn put(&self, alias: &str, profile: &[u8]) -> BinderResult<()> { fn put(&self, alias: &str, uid: i32, entry: &[u8]) -> BinderResult<()> {
let _wp = wd::watch_millis("IVpnProfileStore::put", 500); let _wp = wd::watch_millis("ILegacyKeystore::put", 500);
map_or_log_err(self.put(alias, profile), Ok) map_or_log_err(self.put(alias, uid, entry), Ok)
} }
fn remove(&self, alias: &str) -> BinderResult<()> { fn remove(&self, alias: &str, uid: i32) -> BinderResult<()> {
let _wp = wd::watch_millis("IVpnProfileStore::remove", 500); let _wp = wd::watch_millis("ILegacyKeystore::remove", 500);
map_or_log_err(self.remove(alias), Ok) map_or_log_err(self.remove(alias, uid), Ok)
} }
fn list(&self, prefix: &str) -> BinderResult<Vec<String>> { fn list(&self, prefix: &str, uid: i32) -> BinderResult<Vec<String>> {
let _wp = wd::watch_millis("IVpnProfileStore::list", 500); let _wp = wd::watch_millis("ILegacyKeystore::list", 500);
map_or_log_err(self.list(prefix), Ok) map_or_log_err(self.list(prefix, uid), Ok)
} }
} }
@ -396,12 +423,12 @@ mod db_test {
static TEST_BLOB4: &[u8] = &[3, 2, 3, 4, 5, 6, 7, 8, 9, 0]; static TEST_BLOB4: &[u8] = &[3, 2, 3, 4, 5, 6, 7, 8, 9, 0];
#[test] #[test]
fn test_profile_db() { fn test_entry_db() {
let test_dir = TempDir::new("profiledb_test_").expect("Failed to create temp dir."); let test_dir = TempDir::new("entrydb_test_").expect("Failed to create temp dir.");
let mut db = let mut db = DB::new(&test_dir.build().push(LegacyKeystore::LEGACY_KEYSTORE_FILE_NAME))
DB::new(&test_dir.build().push("vpnprofile.sqlite")).expect("Failed to open database."); .expect("Failed to open database.");
// Insert three profiles for owner 2. // Insert three entries for owner 2.
db.put(2, "test1", TEST_BLOB1).expect("Failed to insert test1."); db.put(2, "test1", TEST_BLOB1).expect("Failed to insert test1.");
db.put(2, "test2", TEST_BLOB2).expect("Failed to insert test2."); db.put(2, "test2", TEST_BLOB2).expect("Failed to insert test2.");
db.put(2, "test3", TEST_BLOB3).expect("Failed to insert test3."); db.put(2, "test3", TEST_BLOB3).expect("Failed to insert test3.");
@ -409,73 +436,59 @@ mod db_test {
// Check list returns all inserted aliases. // Check list returns all inserted aliases.
assert_eq!( assert_eq!(
vec!["test1".to_string(), "test2".to_string(), "test3".to_string(),], vec!["test1".to_string(), "test2".to_string(), "test3".to_string(),],
db.list(2).expect("Failed to list profiles.") db.list(2).expect("Failed to list entries.")
); );
// There should be no profiles for owner 1. // There should be no entries for owner 1.
assert_eq!(Vec::<String>::new(), db.list(1).expect("Failed to list profiles.")); assert_eq!(Vec::<String>::new(), db.list(1).expect("Failed to list entries."));
// Check the content of the three entries. // Check the content of the three entries.
assert_eq!( assert_eq!(Some(TEST_BLOB1), db.get(2, "test1").expect("Failed to get entry.").as_deref());
Some(TEST_BLOB1), assert_eq!(Some(TEST_BLOB2), db.get(2, "test2").expect("Failed to get entry.").as_deref());
db.get(2, "test1").expect("Failed to get profile.").as_deref() assert_eq!(Some(TEST_BLOB3), db.get(2, "test3").expect("Failed to get entry.").as_deref());
);
assert_eq!(
Some(TEST_BLOB2),
db.get(2, "test2").expect("Failed to get profile.").as_deref()
);
assert_eq!(
Some(TEST_BLOB3),
db.get(2, "test3").expect("Failed to get profile.").as_deref()
);
// Remove test2 and check and check that it is no longer retrievable. // Remove test2 and check and check that it is no longer retrievable.
assert!(db.remove(2, "test2").expect("Failed to remove profile.")); assert!(db.remove(2, "test2").expect("Failed to remove entry."));
assert!(db.get(2, "test2").expect("Failed to get profile.").is_none()); assert!(db.get(2, "test2").expect("Failed to get entry.").is_none());
// test2 should now no longer be in the list. // test2 should now no longer be in the list.
assert_eq!( assert_eq!(
vec!["test1".to_string(), "test3".to_string(),], vec!["test1".to_string(), "test3".to_string(),],
db.list(2).expect("Failed to list profiles.") db.list(2).expect("Failed to list entries.")
); );
// Put on existing alias replaces it. // Put on existing alias replaces it.
// Verify test1 is TEST_BLOB1. // Verify test1 is TEST_BLOB1.
assert_eq!( assert_eq!(Some(TEST_BLOB1), db.get(2, "test1").expect("Failed to get entry.").as_deref());
Some(TEST_BLOB1),
db.get(2, "test1").expect("Failed to get profile.").as_deref()
);
db.put(2, "test1", TEST_BLOB4).expect("Failed to replace test1."); db.put(2, "test1", TEST_BLOB4).expect("Failed to replace test1.");
// Verify test1 is TEST_BLOB4. // Verify test1 is TEST_BLOB4.
assert_eq!( assert_eq!(Some(TEST_BLOB4), db.get(2, "test1").expect("Failed to get entry.").as_deref());
Some(TEST_BLOB4),
db.get(2, "test1").expect("Failed to get profile.").as_deref()
);
} }
#[test] #[test]
fn concurrent_vpn_profile_test() -> Result<()> { fn concurrent_legacy_keystore_entry_test() -> Result<()> {
let temp_dir = Arc::new( let temp_dir = Arc::new(
TempDir::new("concurrent_vpn_profile_test_").expect("Failed to create temp dir."), TempDir::new("concurrent_legacy_keystore_entry_test_")
.expect("Failed to create temp dir."),
); );
let db_path = temp_dir.build().push("vpnprofile.sqlite").to_owned(); let db_path = temp_dir.build().push(LegacyKeystore::LEGACY_KEYSTORE_FILE_NAME).to_owned();
let test_begin = Instant::now(); let test_begin = Instant::now();
let mut db = DB::new(&db_path).expect("Failed to open database."); let mut db = DB::new(&db_path).expect("Failed to open database.");
const PROFILE_COUNT: u32 = 5000u32; const ENTRY_COUNT: u32 = 5000u32;
const PROFILE_DB_COUNT: u32 = 5000u32; const ENTRY_DB_COUNT: u32 = 5000u32;
let mut actual_profile_count = PROFILE_COUNT; let mut actual_entry_count = ENTRY_COUNT;
// First insert PROFILE_COUNT profiles. // First insert ENTRY_COUNT entries.
for count in 0..PROFILE_COUNT { for count in 0..ENTRY_COUNT {
if Instant::now().duration_since(test_begin) >= Duration::from_secs(15) { if Instant::now().duration_since(test_begin) >= Duration::from_secs(15) {
actual_profile_count = count; actual_entry_count = count;
break; break;
} }
let alias = format!("test_alias_{}", count); let alias = format!("test_alias_{}", count);
db.put(1, &alias, TEST_BLOB1).expect("Failed to add profile (1)."); db.put(1, &alias, TEST_BLOB1).expect("Failed to add entry (1).");
} }
// Insert more keys from a different thread and into a different namespace. // Insert more keys from a different thread and into a different namespace.
@ -483,16 +496,16 @@ mod db_test {
let handle1 = thread::spawn(move || { let handle1 = thread::spawn(move || {
let mut db = DB::new(&db_path1).expect("Failed to open database."); let mut db = DB::new(&db_path1).expect("Failed to open database.");
for count in 0..actual_profile_count { for count in 0..actual_entry_count {
if Instant::now().duration_since(test_begin) >= Duration::from_secs(40) { if Instant::now().duration_since(test_begin) >= Duration::from_secs(40) {
return; return;
} }
let alias = format!("test_alias_{}", count); let alias = format!("test_alias_{}", count);
db.put(2, &alias, TEST_BLOB2).expect("Failed to add profile (2)."); db.put(2, &alias, TEST_BLOB2).expect("Failed to add entry (2).");
} }
// Then delete them again. // Then delete them again.
for count in 0..actual_profile_count { for count in 0..actual_entry_count {
if Instant::now().duration_since(test_begin) >= Duration::from_secs(40) { if Instant::now().duration_since(test_begin) >= Duration::from_secs(40) {
return; return;
} }
@ -501,12 +514,12 @@ mod db_test {
} }
}); });
// And start deleting the first set of profiles. // And start deleting the first set of entries.
let db_path2 = db_path.clone(); let db_path2 = db_path.clone();
let handle2 = thread::spawn(move || { let handle2 = thread::spawn(move || {
let mut db = DB::new(&db_path2).expect("Failed to open database."); let mut db = DB::new(&db_path2).expect("Failed to open database.");
for count in 0..actual_profile_count { for count in 0..actual_entry_count {
if Instant::now().duration_since(test_begin) >= Duration::from_secs(40) { if Instant::now().duration_since(test_begin) >= Duration::from_secs(40) {
return; return;
} }
@ -516,16 +529,16 @@ mod db_test {
}); });
// While a lot of inserting and deleting is going on we have to open database connections // While a lot of inserting and deleting is going on we have to open database connections
// successfully and then insert and delete a specific profile. // successfully and then insert and delete a specific entry.
let db_path3 = db_path.clone(); let db_path3 = db_path.clone();
let handle3 = thread::spawn(move || { let handle3 = thread::spawn(move || {
for _count in 0..PROFILE_DB_COUNT { for _count in 0..ENTRY_DB_COUNT {
if Instant::now().duration_since(test_begin) >= Duration::from_secs(40) { if Instant::now().duration_since(test_begin) >= Duration::from_secs(40) {
return; return;
} }
let mut db = DB::new(&db_path3).expect("Failed to open database."); let mut db = DB::new(&db_path3).expect("Failed to open database.");
db.put(3, &TEST_ALIAS, TEST_BLOB3).expect("Failed to add profile (3)."); db.put(3, &TEST_ALIAS, TEST_BLOB3).expect("Failed to add entry (3).");
db.remove(3, &TEST_ALIAS).expect("Remove failed (3)."); db.remove(3, &TEST_ALIAS).expect("Remove failed (3).");
} }
@ -534,14 +547,14 @@ mod db_test {
// While thread 3 is inserting and deleting TEST_ALIAS, we try to get the alias. // While thread 3 is inserting and deleting TEST_ALIAS, we try to get the alias.
// This may yield an entry or none, but it must not fail. // This may yield an entry or none, but it must not fail.
let handle4 = thread::spawn(move || { let handle4 = thread::spawn(move || {
for _count in 0..PROFILE_DB_COUNT { for _count in 0..ENTRY_DB_COUNT {
if Instant::now().duration_since(test_begin) >= Duration::from_secs(40) { if Instant::now().duration_since(test_begin) >= Duration::from_secs(40) {
return; return;
} }
let mut db = DB::new(&db_path).expect("Failed to open database."); let mut db = DB::new(&db_path).expect("Failed to open database.");
// This may return Some or None but it must not fail. // This may return Some or None but it must not fail.
db.get(3, &TEST_ALIAS).expect("Failed to get profile (4)."); db.get(3, &TEST_ALIAS).expect("Failed to get entry (4).");
} }
}); });

View file

@ -22,16 +22,16 @@ use keystore2::remote_provisioning::RemoteProvisioningService;
use keystore2::service::KeystoreService; use keystore2::service::KeystoreService;
use keystore2::{apc::ApcManager, shared_secret_negotiation}; use keystore2::{apc::ApcManager, shared_secret_negotiation};
use keystore2::{authorization::AuthorizationManager, id_rotation::IdRotationState}; use keystore2::{authorization::AuthorizationManager, id_rotation::IdRotationState};
use legacykeystore::LegacyKeystore;
use log::{error, info}; use log::{error, info};
use std::{panic, path::Path, sync::mpsc::channel}; use std::{panic, path::Path, sync::mpsc::channel};
use vpnprofilestore::VpnProfileStore;
static KS2_SERVICE_NAME: &str = "android.system.keystore2.IKeystoreService/default"; static KS2_SERVICE_NAME: &str = "android.system.keystore2.IKeystoreService/default";
static APC_SERVICE_NAME: &str = "android.security.apc"; static APC_SERVICE_NAME: &str = "android.security.apc";
static AUTHORIZATION_SERVICE_NAME: &str = "android.security.authorization"; static AUTHORIZATION_SERVICE_NAME: &str = "android.security.authorization";
static REMOTE_PROVISIONING_SERVICE_NAME: &str = "android.security.remoteprovisioning"; static REMOTE_PROVISIONING_SERVICE_NAME: &str = "android.security.remoteprovisioning";
static USER_MANAGER_SERVICE_NAME: &str = "android.security.maintenance"; static USER_MANAGER_SERVICE_NAME: &str = "android.security.maintenance";
static VPNPROFILESTORE_SERVICE_NAME: &str = "android.security.vpnprofilestore"; static LEGACY_KEYSTORE_SERVICE_NAME: &str = "android.security.legacykeystore";
/// Keystore 2.0 takes one argument which is a path indicating its designated working directory. /// Keystore 2.0 takes one argument which is a path indicating its designated working directory.
fn main() { fn main() {
@ -120,14 +120,14 @@ fn main() {
}); });
} }
let vpnprofilestore = VpnProfileStore::new_native_binder( let legacykeystore = LegacyKeystore::new_native_binder(
&keystore2::globals::DB_PATH.read().expect("Could not get DB_PATH."), &keystore2::globals::DB_PATH.read().expect("Could not get DB_PATH."),
); );
binder::add_service(VPNPROFILESTORE_SERVICE_NAME, vpnprofilestore.as_binder()).unwrap_or_else( binder::add_service(LEGACY_KEYSTORE_SERVICE_NAME, legacykeystore.as_binder()).unwrap_or_else(
|e| { |e| {
panic!( panic!(
"Failed to register service {} because of {:?}.", "Failed to register service {} because of {:?}.",
VPNPROFILESTORE_SERVICE_NAME, e LEGACY_KEYSTORE_SERVICE_NAME, e
); );
}, },
); );

View file

@ -599,6 +599,15 @@ impl LegacyBlobLoader {
// * USRCERT was used for public certificates of USRPKEY entries. But KeyChain also // * USRCERT was used for public certificates of USRPKEY entries. But KeyChain also
// used this for user installed certificates without private key material. // used this for user installed certificates without private key material.
const KNOWN_KEYSTORE_PREFIXES: &'static [&'static str] =
&["USRPKEY_", "USRSKEY_", "USRCERT_", "CACERT_"];
fn is_keystore_alias(encoded_alias: &str) -> bool {
// We can check the encoded alias because the prefixes we are interested
// in are all in the printable range that don't get mangled.
Self::KNOWN_KEYSTORE_PREFIXES.iter().any(|prefix| encoded_alias.starts_with(prefix))
}
fn read_km_blob_file(&self, uid: u32, alias: &str) -> Result<Option<(Blob, String)>> { fn read_km_blob_file(&self, uid: u32, alias: &str) -> Result<Option<(Blob, String)>> {
let mut iter = ["USRPKEY", "USRSKEY"].iter(); let mut iter = ["USRPKEY", "USRSKEY"].iter();
@ -630,28 +639,28 @@ impl LegacyBlobLoader {
Ok(Some(Self::new_from_stream(&mut file).context("In read_generic_blob.")?)) Ok(Some(Self::new_from_stream(&mut file).context("In read_generic_blob.")?))
} }
/// Read a legacy vpn profile blob. /// Read a legacy keystore entry blob.
pub fn read_vpn_profile(&self, uid: u32, alias: &str) -> Result<Option<Vec<u8>>> { pub fn read_legacy_keystore_entry(&self, uid: u32, alias: &str) -> Result<Option<Vec<u8>>> {
let path = match self.make_vpn_profile_filename(uid, alias) { let path = match self.make_legacy_keystore_entry_filename(uid, alias) {
Some(path) => path, Some(path) => path,
None => return Ok(None), None => return Ok(None),
}; };
let blob = let blob = Self::read_generic_blob(&path)
Self::read_generic_blob(&path).context("In read_vpn_profile: Failed to read blob.")?; .context("In read_legacy_keystore_entry: Failed to read blob.")?;
Ok(blob.and_then(|blob| match blob.value { Ok(blob.and_then(|blob| match blob.value {
BlobValue::Generic(blob) => Some(blob), BlobValue::Generic(blob) => Some(blob),
_ => { _ => {
log::info!("Unexpected vpn profile blob type. Ignoring"); log::info!("Unexpected legacy keystore entry blob type. Ignoring");
None None
} }
})) }))
} }
/// Remove a vpn profile by the name alias with owner uid. /// Remove a legacy keystore entry by the name alias with owner uid.
pub fn remove_vpn_profile(&self, uid: u32, alias: &str) -> Result<()> { pub fn remove_legacy_keystore_entry(&self, uid: u32, alias: &str) -> Result<()> {
let path = match self.make_vpn_profile_filename(uid, alias) { let path = match self.make_legacy_keystore_entry_filename(uid, alias) {
Some(path) => path, Some(path) => path,
None => return Ok(()), None => return Ok(()),
}; };
@ -659,25 +668,17 @@ impl LegacyBlobLoader {
if let Err(e) = Self::with_retry_interrupted(|| fs::remove_file(path.as_path())) { if let Err(e) = Self::with_retry_interrupted(|| fs::remove_file(path.as_path())) {
match e.kind() { match e.kind() {
ErrorKind::NotFound => return Ok(()), ErrorKind::NotFound => return Ok(()),
_ => return Err(e).context("In remove_vpn_profile."), _ => return Err(e).context("In remove_legacy_keystore_entry."),
} }
} }
let user_id = uid_to_android_user(uid); let user_id = uid_to_android_user(uid);
self.remove_user_dir_if_empty(user_id) self.remove_user_dir_if_empty(user_id)
.context("In remove_vpn_profile: Trying to remove empty user dir.") .context("In remove_legacy_keystore_entry: Trying to remove empty user dir.")
} }
fn is_vpn_profile(encoded_alias: &str) -> bool { /// List all entries belonging to the given uid.
// We can check the encoded alias because the prefixes we are interested pub fn list_legacy_keystore_entries(&self, uid: u32) -> Result<Vec<String>> {
// in are all in the printable range that don't get mangled.
encoded_alias.starts_with("VPN_")
|| encoded_alias.starts_with("PLATFORM_VPN_")
|| encoded_alias == "LOCKDOWN_VPN"
}
/// List all profiles belonging to the given uid.
pub fn list_vpn_profiles(&self, uid: u32) -> Result<Vec<String>> {
let mut path = self.path.clone(); let mut path = self.path.clone();
let user_id = uid_to_android_user(uid); let user_id = uid_to_android_user(uid);
path.push(format!("user_{}", user_id)); path.push(format!("user_{}", user_id));
@ -688,7 +689,10 @@ impl LegacyBlobLoader {
ErrorKind::NotFound => return Ok(Default::default()), ErrorKind::NotFound => return Ok(Default::default()),
_ => { _ => {
return Err(e).context(format!( return Err(e).context(format!(
"In list_vpn_profiles: Failed to open legacy blob database. {:?}", concat!(
"In list_legacy_keystore_entries: ,",
"Failed to open legacy blob database: {:?}"
),
path path
)) ))
} }
@ -696,14 +700,15 @@ impl LegacyBlobLoader {
}; };
let mut result: Vec<String> = Vec::new(); let mut result: Vec<String> = Vec::new();
for entry in dir { for entry in dir {
let file_name = let file_name = entry
entry.context("In list_vpn_profiles: Trying to access dir entry")?.file_name(); .context("In list_legacy_keystore_entries: Trying to access dir entry")?
.file_name();
if let Some(f) = file_name.to_str() { if let Some(f) = file_name.to_str() {
let encoded_alias = &f[uid_str.len() + 1..]; let encoded_alias = &f[uid_str.len() + 1..];
if f.starts_with(&uid_str) && Self::is_vpn_profile(encoded_alias) { if f.starts_with(&uid_str) && !Self::is_keystore_alias(encoded_alias) {
result.push( result.push(
Self::decode_alias(encoded_alias) Self::decode_alias(encoded_alias)
.context("In list_vpn_profiles: Trying to decode alias.")?, .context("In list_legacy_keystore_entries: Trying to decode alias.")?,
) )
} }
} }
@ -711,12 +716,15 @@ impl LegacyBlobLoader {
Ok(result) Ok(result)
} }
/// This function constructs the vpn_profile file name which has the form: /// This function constructs the legacy blob file name which has the form:
/// user_<android user id>/<uid>_<alias>. /// user_<android user id>/<uid>_<alias>. Legacy blob file names must not use
fn make_vpn_profile_filename(&self, uid: u32, alias: &str) -> Option<PathBuf> { /// known keystore prefixes.
// legacy vpn entries must start with VPN_ or PLATFORM_VPN_ or are literally called fn make_legacy_keystore_entry_filename(&self, uid: u32, alias: &str) -> Option<PathBuf> {
// LOCKDOWN_VPN. // Legacy entries must not use known keystore prefixes.
if !Self::is_vpn_profile(alias) { if Self::is_keystore_alias(alias) {
log::warn!(
"Known keystore prefixes cannot be used with legacy keystore -> ignoring request."
);
return None; return None;
} }
@ -1376,11 +1384,11 @@ mod test {
} }
#[test] #[test]
fn list_vpn_profiles_on_non_existing_user() -> Result<()> { fn list_legacy_keystore_entries_on_non_existing_user() -> Result<()> {
let temp_dir = TempDir::new("list_vpn_profiles_on_non_existing_user")?; let temp_dir = TempDir::new("list_legacy_keystore_entries_on_non_existing_user")?;
let legacy_blob_loader = LegacyBlobLoader::new(temp_dir.path()); let legacy_blob_loader = LegacyBlobLoader::new(temp_dir.path());
assert!(legacy_blob_loader.list_vpn_profiles(20)?.is_empty()); assert!(legacy_blob_loader.list_legacy_keystore_entries(20)?.is_empty());
Ok(()) Ok(())
} }