Merge changes from topic "ks2_vpnprofilestore"
* changes:
Keystore 2.0: VPN profile store legacy support
Keystore 2.0: Implement vpn profile store db.
Keystore 2.0: IVpnProfileStore interface.
diff --git a/keystore2/Android.bp b/keystore2/Android.bp
index aaa5659..77ab83c 100644
--- a/keystore2/Android.bp
+++ b/keystore2/Android.bp
@@ -107,6 +107,7 @@
"libbinder_rs",
"libkeystore2",
"liblog_rust",
+ "libvpnprofilestore-rust",
],
init_rc: ["keystore2.rc"],
}
diff --git a/keystore2/aidl/Android.bp b/keystore2/aidl/Android.bp
index c92417b..f30ad83 100644
--- a/keystore2/aidl/Android.bp
+++ b/keystore2/aidl/Android.bp
@@ -134,3 +134,18 @@
}
},
}
+
+aidl_interface {
+ name: "android.security.vpnprofilestore",
+ srcs: [ "android/security/vpnprofilestore/*.aidl" ],
+ unstable: true,
+ backend: {
+ java: {
+ sdk_version: "module_current",
+ },
+ rust: {
+ enabled: true,
+ },
+ },
+}
+
diff --git a/keystore2/aidl/android/security/vpnprofilestore/IVpnProfileStore.aidl b/keystore2/aidl/android/security/vpnprofilestore/IVpnProfileStore.aidl
new file mode 100644
index 0000000..054a4d7
--- /dev/null
+++ b/keystore2/aidl/android/security/vpnprofilestore/IVpnProfileStore.aidl
@@ -0,0 +1,58 @@
+/*
+ * 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);
+}
\ No newline at end of file
diff --git a/keystore2/src/keystore2_main.rs b/keystore2/src/keystore2_main.rs
index 9dc59a2..0d14f84 100644
--- a/keystore2/src/keystore2_main.rs
+++ b/keystore2/src/keystore2_main.rs
@@ -22,12 +22,14 @@
use keystore2::user_manager::UserManager;
use log::{error, info};
use std::{panic, path::Path, sync::mpsc::channel};
+use vpnprofilestore::VpnProfileStore;
static KS2_SERVICE_NAME: &str = "android.system.keystore2";
static APC_SERVICE_NAME: &str = "android.security.apc";
static AUTHORIZATION_SERVICE_NAME: &str = "android.security.authorization";
static REMOTE_PROVISIONING_SERVICE_NAME: &str = "android.security.remoteprovisioning";
static USER_MANAGER_SERVICE_NAME: &str = "android.security.usermanager";
+static VPNPROFILESTORE_SERVICE_NAME: &str = "android.security.vpnprofilestore";
/// Keystore 2.0 takes one argument which is a path indicating its designated working directory.
fn main() {
@@ -114,6 +116,19 @@
);
});
}
+
+ let vpnprofilestore = VpnProfileStore::new_native_binder(
+ &keystore2::globals::DB_PATH.lock().expect("Could not get DB_PATH."),
+ );
+ binder::add_service(VPNPROFILESTORE_SERVICE_NAME, vpnprofilestore.as_binder()).unwrap_or_else(
+ |e| {
+ panic!(
+ "Failed to register service {} because of {:?}.",
+ VPNPROFILESTORE_SERVICE_NAME, e
+ );
+ },
+ );
+
info!("Successfully registered Keystore 2.0 service.");
info!("Joining thread pool now.");
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
new file mode 100644
index 0000000..2fb9aab
--- /dev/null
+++ b/keystore2/vpnprofilestore/Android.bp
@@ -0,0 +1,48 @@
+// Copyright 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.
+
+rust_library {
+ name: "libvpnprofilestore-rust",
+ crate_name: "vpnprofilestore",
+ srcs: [
+ "lib.rs",
+ ],
+ rustlibs: [
+ "android.security.vpnprofilestore-rust",
+ "libanyhow",
+ "libbinder_rs",
+ "libkeystore2",
+ "liblog_rust",
+ "librusqlite",
+ "libthiserror",
+ ],
+}
+
+rust_test {
+ name: "vpnprofilestore_test",
+ crate_name: "vpnprofilestore",
+ srcs: ["lib.rs"],
+ test_suites: ["general-tests"],
+ auto_gen_config: true,
+ rustlibs: [
+ "android.security.vpnprofilestore-rust",
+ "libanyhow",
+ "libbinder_rs",
+ "libkeystore2",
+ "libkeystore2_test_utils",
+ "liblog_rust",
+ "librusqlite",
+ "libthiserror",
+ ],
+}
diff --git a/keystore2/vpnprofilestore/lib.rs b/keystore2/vpnprofilestore/lib.rs
new file mode 100644
index 0000000..f92eacd
--- /dev/null
+++ b/keystore2/vpnprofilestore/lib.rs
@@ -0,0 +1,443 @@
+// 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.
+
+//! Implements the android.security.vpnprofilestore interface.
+
+use android_security_vpnprofilestore::aidl::android::security::vpnprofilestore::{
+ IVpnProfileStore::BnVpnProfileStore, IVpnProfileStore::IVpnProfileStore,
+ IVpnProfileStore::ERROR_PROFILE_NOT_FOUND, IVpnProfileStore::ERROR_SYSTEM_ERROR,
+};
+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::{
+ collections::HashSet,
+ path::{Path, PathBuf},
+};
+
+struct DB {
+ conn: Connection,
+}
+
+impl DB {
+ fn new(db_file: &Path) -> Result<Self> {
+ let mut db = Self {
+ conn: Connection::open(db_file).context("Failed to initialize SQLite connection.")?,
+ };
+ db.init_tables().context("Trying to initialize vpnstore db.")?;
+ Ok(db)
+ }
+
+ fn with_transaction<T, F>(&mut self, behavior: TransactionBehavior, f: F) -> Result<T>
+ where
+ F: Fn(&Transaction) -> Result<T>,
+ {
+ loop {
+ match self
+ .conn
+ .transaction_with_behavior(behavior)
+ .context("In with_transaction.")
+ .and_then(|tx| f(&tx).map(|result| (result, tx)))
+ .and_then(|(result, tx)| {
+ tx.commit().context("In with_transaction: Failed to commit transaction.")?;
+ Ok(result)
+ }) {
+ Ok(result) => break Ok(result),
+ Err(e) => {
+ if Self::is_locked_error(&e) {
+ std::thread::sleep(std::time::Duration::from_micros(500));
+ continue;
+ } else {
+ return Err(e).context("In with_transaction.");
+ }
+ }
+ }
+ }
+ }
+
+ fn is_locked_error(e: &anyhow::Error) -> bool {
+ matches!(e.root_cause().downcast_ref::<rusqlite::ffi::Error>(),
+ Some(rusqlite::ffi::Error {
+ code: rusqlite::ErrorCode::DatabaseBusy,
+ ..
+ })
+ | Some(rusqlite::ffi::Error {
+ code: rusqlite::ErrorCode::DatabaseLocked,
+ ..
+ }))
+ }
+
+ fn init_tables(&mut self) -> Result<()> {
+ self.with_transaction(TransactionBehavior::Immediate, |tx| {
+ tx.execute(
+ "CREATE TABLE IF NOT EXISTS profiles (
+ owner INTEGER,
+ alias BLOB,
+ profile BLOB,
+ UNIQUE(owner, alias));",
+ NO_PARAMS,
+ )
+ .context("Failed to initialize \"profiles\" table.")?;
+ Ok(())
+ })
+ }
+
+ fn list(&mut self, caller_uid: u32) -> Result<Vec<String>> {
+ self.with_transaction(TransactionBehavior::Deferred, |tx| {
+ let mut stmt = tx
+ .prepare("SELECT alias FROM profiles WHERE owner = ? ORDER BY alias ASC;")
+ .context("In list: Failed to prepare statement.")?;
+
+ let aliases = stmt
+ .query_map(params![caller_uid], |row| row.get(0))?
+ .collect::<rusqlite::Result<Vec<String>>>()
+ .context("In list: query_map failed.");
+ aliases
+ })
+ }
+
+ fn put(&mut self, caller_uid: u32, alias: &str, profile: &[u8]) -> Result<()> {
+ self.with_transaction(TransactionBehavior::Immediate, |tx| {
+ tx.execute(
+ "INSERT OR REPLACE INTO profiles (owner, alias, profile) values (?, ?, ?)",
+ params![caller_uid, alias, profile,],
+ )
+ .context("In put: Failed to insert or replace.")?;
+ Ok(())
+ })
+ }
+
+ fn get(&mut self, caller_uid: u32, alias: &str) -> Result<Option<Vec<u8>>> {
+ self.with_transaction(TransactionBehavior::Deferred, |tx| {
+ tx.query_row(
+ "SELECT profile FROM profiles WHERE owner = ? AND alias = ?;",
+ params![caller_uid, alias],
+ |row| row.get(0),
+ )
+ .optional()
+ .context("In get: failed loading profile.")
+ })
+ }
+
+ fn remove(&mut self, caller_uid: u32, alias: &str) -> Result<bool> {
+ let removed = self.with_transaction(TransactionBehavior::Immediate, |tx| {
+ tx.execute(
+ "DELETE FROM profiles WHERE owner = ? AND alias = ?;",
+ params![caller_uid, alias],
+ )
+ .context("In remove: Failed to delete row.")
+ })?;
+ Ok(removed == 1)
+ }
+}
+
+/// This is the main VpnProfileStore error type, it wraps binder exceptions and the
+/// VnpStore errors.
+#[derive(Debug, thiserror::Error, PartialEq)]
+pub enum Error {
+ /// Wraps a VpnProfileStore error code.
+ #[error("Error::Error({0:?})")]
+ Error(i32),
+ /// 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::Error(ERROR_SYSTEM_ERROR)`
+ pub fn sys() -> Self {
+ Error::Error(ERROR_SYSTEM_ERROR)
+ }
+
+ /// Short hand for `Error::Error(ERROR_PROFILE_NOT_FOUND)`
+ pub fn not_found() -> Self {
+ Error::Error(ERROR_PROFILE_NOT_FOUND)
+ }
+}
+
+/// This function should be used by vpnprofilestore service calls to translate error conditions
+/// into service specific exceptions.
+///
+/// All error conditions get logged by this function.
+///
+/// `Error::Error(x)` variants get mapped onto a service specific error code of `x`.
+///
+/// All non `Error` error conditions get mapped onto `ERROR_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).
+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::Error(e)) => *e,
+ Some(Error::Binder(_, _)) | None => ERROR_SYSTEM_ERROR,
+ };
+ Err(BinderStatus::new_service_specific_error(rc, None))
+ },
+ handle_ok,
+ )
+}
+
+/// 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(path: &Path) -> Strong<dyn IVpnProfileStore> {
+ let mut db_path = path.to_path_buf();
+ db_path.push("vpnprofilestore.sqlite");
+
+ let result = Self { db_path, async_task: Default::default() };
+ result.init_shelf(path);
+ BnVpnProfileStore::new_binder(result)
+ }
+
+ fn open_db(&self) -> Result<DB> {
+ DB::new(&self.db_path).context("In open_db: Failed to open db.")
+ }
+
+ fn get(&self, alias: &str) -> Result<Vec<u8>> {
+ let mut db = self.open_db().context("In get.")?;
+ let calling_uid = ThreadState::get_calling_uid();
+
+ 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 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 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.")?;
+ if removed {
+ Ok(())
+ } else {
+ Err(Error::not_found()).context("In remove: No such profile.")
+ }
+ }
+
+ fn list(&self, prefix: &str) -> Result<Vec<String>> {
+ let mut db = self.open_db().context("In list.")?;
+ let calling_uid = ThreadState::get_calling_uid();
+ 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)
+ }
+ }
+}
+
+impl binder::Interface for VpnProfileStore {}
+
+impl IVpnProfileStore for VpnProfileStore {
+ fn get(&self, alias: &str) -> BinderResult<Vec<u8>> {
+ map_or_log_err(self.get(alias), Ok)
+ }
+ fn put(&self, alias: &str, profile: &[u8]) -> BinderResult<()> {
+ map_or_log_err(self.put(alias, profile), Ok)
+ }
+ fn remove(&self, alias: &str) -> BinderResult<()> {
+ map_or_log_err(self.remove(alias), Ok)
+ }
+ fn list(&self, prefix: &str) -> BinderResult<Vec<String>> {
+ map_or_log_err(self.list(prefix), Ok)
+ }
+}
+
+#[cfg(test)]
+mod db_test {
+ use super::*;
+ use keystore2_test_utils::TempDir;
+
+ static TEST_BLOB1: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
+ static TEST_BLOB2: &[u8] = &[2, 2, 3, 4, 5, 6, 7, 8, 9, 0];
+ static TEST_BLOB3: &[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]
+ fn test_profile_db() {
+ let test_dir = TempDir::new("profiledb_test_").expect("Failed to create temp dir.");
+ let mut db =
+ DB::new(&test_dir.build().push("vpnprofile.sqlite")).expect("Failed to open database.");
+
+ // Insert three profiles for owner 2.
+ 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, "test3", TEST_BLOB3).expect("Failed to insert test3.");
+
+ // Check list returns all inserted aliases.
+ assert_eq!(
+ vec!["test1".to_string(), "test2".to_string(), "test3".to_string(),],
+ db.list(2).expect("Failed to list profiles.")
+ );
+
+ // There should be no profiles for owner 1.
+ assert_eq!(Vec::<String>::new(), db.list(1).expect("Failed to list profiles."));
+
+ // Check the content of the three entries.
+ assert_eq!(
+ Some(TEST_BLOB1),
+ db.get(2, "test1").expect("Failed to get profile.").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.
+ assert!(db.remove(2, "test2").expect("Failed to remove profile."));
+ assert!(db.get(2, "test2").expect("Failed to get profile.").is_none());
+
+ // test2 should now no longer be in the list.
+ assert_eq!(
+ vec!["test1".to_string(), "test3".to_string(),],
+ db.list(2).expect("Failed to list profiles.")
+ );
+
+ // Put on existing alias replaces it.
+ // Verify test1 is TEST_BLOB1.
+ assert_eq!(
+ 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.");
+ // Verify test1 is TEST_BLOB4.
+ assert_eq!(
+ Some(TEST_BLOB4),
+ db.get(2, "test1").expect("Failed to get profile.").as_deref()
+ );
+ }
+}