blob: d64a26e5edfef3685fc91d158dce3d1e66ff0864 [file] [log] [blame]
// Copyright 2020, The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// TODO: Once this is stable, remove this and document everything public.
#![allow(missing_docs)]
use crate::error::Error as KsError;
use anyhow::{Context, Result};
use keystore_aidl_generated as aidl;
#[cfg(not(test))]
use rand::prelude::random;
use rusqlite::{params, Connection, TransactionBehavior, NO_PARAMS};
#[cfg(test)]
use tests::random;
pub struct KeystoreDB {
conn: Connection,
}
impl KeystoreDB {
// TODO(b/160882985): Figure out the location for this file.
#[cfg(not(test))]
pub fn new() -> Result<KeystoreDB> {
KeystoreDB::new_with_filename("persistent.sql")
}
#[cfg(test)]
pub fn new() -> Result<KeystoreDB> {
KeystoreDB::new_with_filename("")
}
fn new_with_filename(persistent_file: &str) -> Result<KeystoreDB> {
let db = KeystoreDB {
conn: Connection::open_in_memory()
.context("Failed to initialize sqlite connection.")?,
};
db.attach_databases(persistent_file).context("Failed to create KeystoreDB.")?;
db.init_tables().context("Failed to create KeystoreDB.")?;
Ok(db)
}
fn attach_databases(&self, persistent_file: &str) -> Result<()> {
self.conn
.execute("ATTACH DATABASE ? as 'persistent';", params![persistent_file])
.context("Failed to attach databases.")?;
Ok(())
}
fn init_tables(&self) -> Result<()> {
self.conn
.execute(
"CREATE TABLE IF NOT EXISTS persistent.keyentry (
id INTEGER UNIQUE,
creation_date DATETIME,
domain INTEGER,
namespace INTEGER,
alias TEXT);",
NO_PARAMS,
)
.context("Failed to initialize \"keyentry\" table.")?;
self.conn
.execute(
"CREATE TABLE IF NOT EXISTS persistent.keyparameter (
keyentryid INTEGER,
tag INTEGER,
data ANY,
security_level INTEGER);",
NO_PARAMS,
)
.context("Failed to initialize \"keyparameter\" table.")?;
Ok(())
}
pub fn create_key_entry(&self, domain: aidl::Domain, namespace: i64) -> Result<i64> {
match domain {
aidl::Domain::App | aidl::Domain::SELinux => {}
_ => {
return Err(KsError::sys())
.context(format!("Domain {:?} must be either App or SELinux.", domain));
}
}
// Loop until we get a unique id.
loop {
let newid: i64 = random();
let ret = self.conn.execute(
"INSERT into persistent.keyentry (id, creation_date, domain, namespace, alias)
VALUES(?, datetime('now'), ?, ?, NULL);",
params![newid, domain as i64, namespace],
);
match ret {
// If the id already existed, try again.
Err(rusqlite::Error::SqliteFailure(
libsqlite3_sys::Error {
code: libsqlite3_sys::ErrorCode::ConstraintViolation,
extended_code: libsqlite3_sys::SQLITE_CONSTRAINT_UNIQUE,
},
_,
)) => (),
_ => return Ok(newid),
}
}
}
pub fn rebind_alias(
&mut self,
newid: u32,
alias: &str,
domain: aidl::Domain,
namespace: i64,
) -> Result<()> {
match domain {
aidl::Domain::App | aidl::Domain::SELinux => {}
_ => {
return Err(KsError::sys())
.context(format!("Domain {:?} must be either App or SELinux.", domain));
}
}
let tx = self
.conn
.transaction_with_behavior(TransactionBehavior::Immediate)
.context("Failed to initialize transaction.")?;
tx.execute(
"UPDATE persistent.keyentry
SET alias = NULL, domain = NULL, namespace = NULL
WHERE alias = ? AND domain = ? AND namespace = ?;",
params![alias, domain as i64, namespace],
)
.context("Failed to rebind existing entry.")?;
let result = tx
.execute(
"UPDATE persistent.keyentry
SET alias = ?
WHERE id = ? AND domain = ? AND namespace = ?;",
params![alias, newid, domain as i64, namespace],
)
.context("Failed to set alias.")?;
if result != 1 {
// Note that this explicit rollback is not required, as
// the transaction should rollback if we do not commit it.
// We leave it here for readability.
tx.rollback().context("Failed to rollback a failed transaction.")?;
return Err(KsError::sys()).context(format!(
"Expected to update a single entry but instead updated {}.",
result
));
}
tx.commit().context("Failed to commit transaction.")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
// Ensure that we're using the "injected" random function, not the real one.
#[test]
fn test_mocked_random() {
let rand1 = random();
let rand2 = random();
let rand3 = random();
if rand1 == rand2 {
assert_eq!(rand2 + 1, rand3);
} else {
assert_eq!(rand1 + 1, rand2);
assert_eq!(rand2, rand3);
}
}
// Ensure we can initialize the database.
#[test]
fn test_new() -> Result<()> {
KeystoreDB::new()?;
Ok(())
}
// Test that we have the correct tables.
#[test]
fn test_tables() -> Result<()> {
let db = KeystoreDB::new()?;
let tables = db
.conn
.prepare("SELECT name from persistent.sqlite_master WHERE type='table' ORDER BY name;")?
.query_map(params![], |row| row.get(0))?
.collect::<rusqlite::Result<Vec<String>>>()?;
assert_eq!(tables.len(), 2);
assert_eq!(tables[0], "keyentry");
assert_eq!(tables[1], "keyparameter");
Ok(())
}
#[test]
fn test_no_persistence_for_tests() -> Result<()> {
let db = KeystoreDB::new()?;
db.create_key_entry(aidl::Domain::App, 100)?;
let entries = get_keyentry(&db)?;
assert_eq!(entries.len(), 1);
let db = KeystoreDB::new()?;
let entries = get_keyentry(&db)?;
assert_eq!(entries.len(), 0);
Ok(())
}
#[test]
fn test_persistence_for_files() -> Result<()> {
let persistent = TempFile { filename: "/data/local/tmp/persistent.sql" };
let db = KeystoreDB::new_with_filename(persistent.filename)?;
db.create_key_entry(aidl::Domain::App, 100)?;
let entries = get_keyentry(&db)?;
assert_eq!(entries.len(), 1);
let db = KeystoreDB::new_with_filename(persistent.filename)?;
let entries_new = get_keyentry(&db)?;
assert_eq!(entries, entries_new);
Ok(())
}
#[test]
fn test_create_key_entry() -> Result<()> {
use aidl::Domain;
fn extractor(ke: &KeyEntryRow) -> (Domain, i64, Option<&str>) {
(ke.domain.unwrap(), ke.namespace.unwrap(), ke.alias.as_deref())
}
let db = KeystoreDB::new()?;
db.create_key_entry(Domain::App, 100)?;
db.create_key_entry(Domain::SELinux, 101)?;
let entries = get_keyentry(&db)?;
assert_eq!(entries.len(), 2);
assert_eq!(extractor(&entries[0]), (Domain::App, 100, None));
assert_eq!(extractor(&entries[1]), (Domain::SELinux, 101, None));
// Test that we must pass in a valid Domain.
check_result_is_error_containing_string(
db.create_key_entry(Domain::Grant, 102),
"Domain Grant must be either App or SELinux.",
);
check_result_is_error_containing_string(
db.create_key_entry(Domain::Blob, 103),
"Domain Blob must be either App or SELinux.",
);
check_result_is_error_containing_string(
db.create_key_entry(Domain::KeyId, 104),
"Domain KeyId must be either App or SELinux.",
);
Ok(())
}
#[test]
fn test_rebind_alias() -> Result<()> {
use aidl::Domain;
fn extractor(ke: &KeyEntryRow) -> (Option<Domain>, Option<i64>, Option<&str>) {
(ke.domain, ke.namespace, ke.alias.as_deref())
}
let mut db = KeystoreDB::new()?;
db.create_key_entry(Domain::App, 42)?;
db.create_key_entry(Domain::App, 42)?;
let entries = get_keyentry(&db)?;
assert_eq!(entries.len(), 2);
assert_eq!(extractor(&entries[0]), (Some(Domain::App), Some(42), None));
assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), None));
// Test that the first call to rebind_alias sets the alias.
db.rebind_alias(entries[0].id, "foo", Domain::App, 42)?;
let entries = get_keyentry(&db)?;
assert_eq!(entries.len(), 2);
assert_eq!(extractor(&entries[0]), (Some(Domain::App), Some(42), Some("foo")));
assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), None));
// Test that the second call to rebind_alias also empties the old one.
db.rebind_alias(entries[1].id, "foo", Domain::App, 42)?;
let entries = get_keyentry(&db)?;
assert_eq!(entries.len(), 2);
assert_eq!(extractor(&entries[0]), (None, None, None));
assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), Some("foo")));
// Test that we must pass in a valid Domain.
check_result_is_error_containing_string(
db.rebind_alias(0, "foo", Domain::Grant, 42),
"Domain Grant must be either App or SELinux.",
);
check_result_is_error_containing_string(
db.rebind_alias(0, "foo", Domain::Blob, 42),
"Domain Blob must be either App or SELinux.",
);
check_result_is_error_containing_string(
db.rebind_alias(0, "foo", Domain::KeyId, 42),
"Domain KeyId must be either App or SELinux.",
);
// Test that we correctly handle setting an alias for something that does not exist.
check_result_is_error_containing_string(
db.rebind_alias(0, "foo", Domain::SELinux, 42),
"Expected to update a single entry but instead updated 0",
);
// Test that we correctly abort the transaction in this case.
let entries = get_keyentry(&db)?;
assert_eq!(entries.len(), 2);
assert_eq!(extractor(&entries[0]), (None, None, None));
assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), Some("foo")));
Ok(())
}
// Helpers
// Checks that the given result is an error containing the given string.
fn check_result_is_error_containing_string<T>(result: Result<T>, target: &str) {
let error_str = format!(
"{:#?}",
result.err().unwrap_or_else(|| panic!("Expected the error: {}", target))
);
assert!(
error_str.contains(target),
"The string \"{}\" should contain \"{}\"",
error_str,
target
);
}
#[derive(Debug, PartialEq)]
#[allow(dead_code)]
struct KeyEntryRow {
id: u32,
creation_date: String,
domain: Option<aidl::Domain>,
namespace: Option<i64>,
alias: Option<String>,
}
fn get_keyentry(db: &KeystoreDB) -> Result<Vec<KeyEntryRow>> {
db.conn
.prepare("SELECT * FROM persistent.keyentry;")?
.query_map(NO_PARAMS, |row| {
let domain: Option<i32> = row.get(2)?;
Ok(KeyEntryRow {
id: row.get(0)?,
creation_date: row.get(1)?,
domain: domain.map(domain_from_integer),
namespace: row.get(3)?,
alias: row.get(4)?,
})
})?
.map(|r| r.context("Could not read keyentry row."))
.collect::<Result<Vec<_>>>()
}
// TODO: Replace this with num_derive.
fn domain_from_integer(value: i32) -> aidl::Domain {
use aidl::Domain;
match value {
x if Domain::App as i32 == x => Domain::App,
x if Domain::Grant as i32 == x => Domain::Grant,
x if Domain::SELinux as i32 == x => Domain::SELinux,
x if Domain::Blob as i32 == x => Domain::Blob,
x if Domain::KeyId as i32 == x => Domain::KeyId,
_ => panic!("Unexpected domain: {}", value),
}
}
// A class that deletes a file when it is dropped.
// TODO: If we ever add a crate that does this, we can use it instead.
struct TempFile {
filename: &'static str,
}
impl Drop for TempFile {
fn drop(&mut self) {
std::fs::remove_file(self.filename).expect("Cannot delete temporary file");
}
}
// Use a custom random number generator that repeats each number once.
// This allows us to test repeated elements.
thread_local! {
static RANDOM_COUNTER: RefCell<i64> = RefCell::new(0);
}
pub fn random() -> i64 {
RANDOM_COUNTER.with(|counter| {
let result = *counter.borrow() / 2;
*counter.borrow_mut() += 1;
result
})
}
}