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)
+        }
     }
 }