Keystore 2.0: VPN profile store legacy support
Test: Manually tested by storing profiles through the UI then upgrading
to keystore 2 and verifying that they are still visible.
Also checked that they were correctly imported into the sqlite
database.
Change-Id: I0cbfb026db032290163469ee8824b5f59edc06fb
diff --git a/keystore2/src/legacy_blob.rs b/keystore2/src/legacy_blob.rs
index b51f644..3fc77b7 100644
--- a/keystore2/src/legacy_blob.rs
+++ b/keystore2/src/legacy_blob.rs
@@ -634,8 +634,98 @@
Ok(Some(Self::new_from_stream(&mut file).context("In read_generic_blob.")?))
}
- /// This function constructs the blob file name which has the form:
+ /// Read a legacy vpn profile blob.
+ pub fn read_vpn_profile(&self, uid: u32, alias: &str) -> Result<Option<Vec<u8>>> {
+ let path = match self.make_vpn_profile_filename(uid, alias) {
+ Some(path) => path,
+ None => return Ok(None),
+ };
+
+ let blob =
+ Self::read_generic_blob(&path).context("In read_vpn_profile: Failed to read blob.")?;
+
+ Ok(blob.and_then(|blob| match blob.value {
+ BlobValue::Generic(blob) => Some(blob),
+ _ => {
+ log::info!("Unexpected vpn profile blob type. Ignoring");
+ None
+ }
+ }))
+ }
+
+ /// Remove a vpn profile by the name alias with owner uid.
+ pub fn remove_vpn_profile(&self, uid: u32, alias: &str) -> Result<()> {
+ let path = match self.make_vpn_profile_filename(uid, alias) {
+ Some(path) => path,
+ None => return Ok(()),
+ };
+
+ if let Err(e) = Self::with_retry_interrupted(|| fs::remove_file(path.as_path())) {
+ match e.kind() {
+ ErrorKind::NotFound => return Ok(()),
+ _ => return Err(e).context("In remove_vpn_profile."),
+ }
+ }
+
+ let user_id = uid_to_android_user(uid);
+ self.remove_user_dir_if_empty(user_id)
+ .context("In remove_vpn_profile: Trying to remove empty user dir.")
+ }
+
+ fn is_vpn_profile(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.
+ 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 user_id = uid_to_android_user(uid);
+ path.push(format!("user_{}", user_id));
+ let uid_str = uid.to_string();
+ let dir =
+ Self::with_retry_interrupted(|| fs::read_dir(path.as_path())).with_context(|| {
+ format!("In list_vpn_profiles: Failed to open legacy blob database. {:?}", path)
+ })?;
+ let mut result: Vec<String> = Vec::new();
+ for entry in dir {
+ let file_name =
+ entry.context("In list_vpn_profiles: Trying to access dir entry")?.file_name();
+ if let Some(f) = file_name.to_str() {
+ let encoded_alias = &f[uid_str.len() + 1..];
+ if f.starts_with(&uid_str) && Self::is_vpn_profile(encoded_alias) {
+ result.push(
+ Self::decode_alias(encoded_alias)
+ .context("In list_vpn_profiles: Trying to decode alias.")?,
+ )
+ }
+ }
+ }
+ Ok(result)
+ }
+
+ /// This function constructs the vpn_profile file name which has the form:
/// user_<android user id>/<uid>_<alias>.
+ fn make_vpn_profile_filename(&self, uid: u32, alias: &str) -> Option<PathBuf> {
+ // legacy vpn entries must start with VPN_ or PLATFORM_VPN_ or are literally called
+ // LOCKDOWN_VPN.
+ if !Self::is_vpn_profile(alias) {
+ return None;
+ }
+
+ let mut path = self.path.clone();
+ let user_id = uid_to_android_user(uid);
+ let encoded_alias = Self::encode_alias(alias);
+ path.push(format!("user_{}", user_id));
+ path.push(format!("{}_{}", uid, encoded_alias));
+ Some(path)
+ }
+
+ /// This function constructs the blob file name which has the form:
+ /// user_<android user id>/<uid>_<prefix>_<alias>.
fn make_blob_filename(&self, uid: u32, alias: &str, prefix: &str) -> PathBuf {
let user_id = uid_to_android_user(uid);
let encoded_alias = Self::encode_alias(&format!("{}_{}", prefix, alias));
@@ -838,18 +928,24 @@
if something_was_deleted {
let user_id = uid_to_android_user(uid);
- if self
- .is_empty_user(user_id)
- .context("In remove_keystore_entry: Trying to check for empty user dir.")?
- {
- let user_path = self.make_user_path_name(user_id);
- Self::with_retry_interrupted(|| fs::remove_dir(user_path.as_path())).ok();
- }
+ self.remove_user_dir_if_empty(user_id)
+ .context("In remove_keystore_entry: Trying to remove empty user dir.")?;
}
Ok(something_was_deleted)
}
+ fn remove_user_dir_if_empty(&self, user_id: u32) -> Result<()> {
+ if self
+ .is_empty_user(user_id)
+ .context("In remove_user_dir_if_empty: Trying to check for empty user dir.")?
+ {
+ let user_path = self.make_user_path_name(user_id);
+ Self::with_retry_interrupted(|| fs::remove_dir(user_path.as_path())).ok();
+ }
+ Ok(())
+ }
+
/// Load a legacy key blob entry by uid and alias.
pub fn load_by_uid_alias(
&self,
diff --git a/keystore2/src/lib.rs b/keystore2/src/lib.rs
index 358fce8..8fef6cf 100644
--- a/keystore2/src/lib.rs
+++ b/keystore2/src/lib.rs
@@ -16,6 +16,7 @@
#![recursion_limit = "256"]
pub mod apc;
+pub mod async_task;
pub mod authorization;
pub mod database;
pub mod enforcements;
@@ -33,7 +34,6 @@
pub mod user_manager;
pub mod utils;
-mod async_task;
mod db_utils;
mod gc;
mod super_key;
diff --git a/keystore2/vpnprofilestore/Android.bp b/keystore2/vpnprofilestore/Android.bp
index 6a0c71c..2fb9aab 100644
--- a/keystore2/vpnprofilestore/Android.bp
+++ b/keystore2/vpnprofilestore/Android.bp
@@ -22,6 +22,7 @@
"android.security.vpnprofilestore-rust",
"libanyhow",
"libbinder_rs",
+ "libkeystore2",
"liblog_rust",
"librusqlite",
"libthiserror",
@@ -38,6 +39,7 @@
"android.security.vpnprofilestore-rust",
"libanyhow",
"libbinder_rs",
+ "libkeystore2",
"libkeystore2_test_utils",
"liblog_rust",
"librusqlite",
diff --git a/keystore2/vpnprofilestore/lib.rs b/keystore2/vpnprofilestore/lib.rs
index b631286..f92eacd 100644
--- a/keystore2/vpnprofilestore/lib.rs
+++ b/keystore2/vpnprofilestore/lib.rs
@@ -21,10 +21,14 @@
use android_security_vpnprofilestore::binder::{Result as BinderResult, Status as BinderStatus};
use anyhow::{Context, Result};
use binder::{ExceptionCode, Strong, ThreadState};
+use keystore2::{async_task::AsyncTask, legacy_blob::LegacyBlobLoader};
use rusqlite::{
params, Connection, OptionalExtension, Transaction, TransactionBehavior, NO_PARAMS,
};
-use std::path::{Path, PathBuf};
+use std::{
+ collections::HashSet,
+ path::{Path, PathBuf},
+};
struct DB {
conn: Connection,
@@ -196,20 +200,27 @@
)
}
-// TODO make sure that ALIASES have a prefix of VPN_ PLATFORM_VPN_ or
-// is equal to LOCKDOWN_VPN.
-
/// Implements IVpnProfileStore AIDL interface.
pub struct VpnProfileStore {
db_path: PathBuf,
+ async_task: AsyncTask,
+}
+
+struct AsyncState {
+ recently_imported: HashSet<(u32, String)>,
+ legacy_loader: LegacyBlobLoader,
+ db_path: PathBuf,
}
impl VpnProfileStore {
/// Creates a new VpnProfileStore instance.
- pub fn new_native_binder(db_path: &Path) -> Strong<dyn IVpnProfileStore> {
+ pub fn new_native_binder(path: &Path) -> Strong<dyn IVpnProfileStore> {
let mut db_path = path.to_path_buf();
db_path.push("vpnprofilestore.sqlite");
- BnVpnProfileStore::new_binder(Self { db_path })
+
+ let result = Self { db_path, async_task: Default::default() };
+ result.init_shelf(path);
+ BnVpnProfileStore::new_binder(result)
}
fn open_db(&self) -> Result<DB> {
@@ -219,21 +230,38 @@
fn get(&self, alias: &str) -> Result<Vec<u8>> {
let mut db = self.open_db().context("In get.")?;
let calling_uid = ThreadState::get_calling_uid();
- db.get(calling_uid, alias)
- .context("In get: Trying to load profile from DB.")?
- .ok_or_else(Error::not_found)
- .context("In get: No such profile.")
+
+ if let Some(profile) =
+ db.get(calling_uid, alias).context("In get: Trying to load profile from DB.")?
+ {
+ return Ok(profile);
+ }
+ if self.get_legacy(calling_uid, alias).context("In get: Trying to migrate legacy blob.")? {
+ // If we were able to migrate a legacy blob try again.
+ if let Some(profile) =
+ db.get(calling_uid, alias).context("In get: Trying to load profile from DB.")?
+ {
+ return Ok(profile);
+ }
+ }
+ Err(Error::not_found()).context("In get: No such profile.")
}
fn put(&self, alias: &str, profile: &[u8]) -> Result<()> {
- let mut db = self.open_db().context("In put.")?;
let calling_uid = ThreadState::get_calling_uid();
+ // In order to make sure that we don't have stale legacy profiles, make sure they are
+ // migrated before replacing them.
+ let _ = self.get_legacy(calling_uid, alias);
+ let mut db = self.open_db().context("In put.")?;
db.put(calling_uid, alias, profile).context("In put: Trying to insert profile into DB.")
}
fn remove(&self, alias: &str) -> Result<()> {
- let mut db = self.open_db().context("In remove.")?;
let calling_uid = ThreadState::get_calling_uid();
+ 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
+ // migrated before removing them.
+ let _ = self.get_legacy(calling_uid, alias);
let removed = db
.remove(calling_uid, alias)
.context("In remove: Trying to remove profile from DB.")?;
@@ -247,12 +275,84 @@
fn list(&self, prefix: &str) -> Result<Vec<String>> {
let mut db = self.open_db().context("In list.")?;
let calling_uid = ThreadState::get_calling_uid();
- Ok(db
- .list(calling_uid)
- .context("In list: Trying to get list of profiles.")?
- .into_iter()
- .filter(|s| s.starts_with(prefix))
- .collect())
+ let mut result = self.list_legacy(calling_uid).context("In list.")?;
+ result
+ .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.sort_unstable();
+ result.dedup();
+ Ok(result)
+ }
+
+ fn init_shelf(&self, path: &Path) {
+ let mut db_path = path.to_path_buf();
+ self.async_task.queue_hi(move |shelf| {
+ let legacy_loader = LegacyBlobLoader::new(&db_path);
+ db_path.push("vpnprofilestore.sqlite");
+
+ shelf.put(AsyncState { legacy_loader, db_path, recently_imported: Default::default() });
+ })
+ }
+
+ fn do_serialized<F, T: Send + 'static>(&self, f: F) -> Result<T>
+ where
+ F: FnOnce(&mut AsyncState) -> Result<T> + Send + 'static,
+ {
+ let (sender, receiver) = std::sync::mpsc::channel::<Result<T>>();
+ self.async_task.queue_hi(move |shelf| {
+ let state = shelf.get_downcast_mut::<AsyncState>().expect("Failed to get shelf.");
+ sender.send(f(state)).expect("Failed to send result.");
+ });
+ receiver.recv().context("In do_serialized: Failed to receive result.")?
+ }
+
+ fn list_legacy(&self, uid: u32) -> Result<Vec<String>> {
+ self.do_serialized(move |state| {
+ state
+ .legacy_loader
+ .list_vpn_profiles(uid)
+ .context("Trying to list legacy vnp profiles.")
+ })
+ .context("In list_legacy.")
+ }
+
+ fn get_legacy(&self, uid: u32, alias: &str) -> Result<bool> {
+ let alias = alias.to_string();
+ self.do_serialized(move |state| {
+ if state.recently_imported.contains(&(uid, alias.clone())) {
+ return Ok(true);
+ }
+ let mut db = DB::new(&state.db_path).context("In open_db: Failed to open db.")?;
+ let migrated =
+ Self::migrate_one_legacy_profile(uid, &alias, &state.legacy_loader, &mut db)
+ .context("Trying to migrate legacy vpn profile.")?;
+ if migrated {
+ state.recently_imported.insert((uid, alias));
+ }
+ Ok(migrated)
+ })
+ .context("In get_legacy.")
+ }
+
+ fn migrate_one_legacy_profile(
+ uid: u32,
+ alias: &str,
+ legacy_loader: &LegacyBlobLoader,
+ db: &mut DB,
+ ) -> Result<bool> {
+ let blob = legacy_loader
+ .read_vpn_profile(uid, alias)
+ .context("In migrate_one_legacy_profile: Trying to read legacy vpn profile.")?;
+ if let Some(profile) = blob {
+ db.put(uid, alias, &profile)
+ .context("In migrate_one_legacy_profile: Trying to insert profile into DB.")?;
+ legacy_loader
+ .remove_vpn_profile(uid, alias)
+ .context("In migrate_one_legacy_profile: Trying to delete legacy profile.")?;
+ Ok(true)
+ } else {
+ Ok(false)
+ }
}
}