Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 1 | // Copyright 2020, The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | // TODO: Once this is stable, remove this and document everything public. |
| 16 | #![allow(missing_docs)] |
| 17 | |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 18 | use crate::error::Error as KsError; |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 19 | use anyhow::{Context, Result}; |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 20 | use keystore_aidl_generated as aidl; |
| 21 | #[cfg(not(test))] |
| 22 | use rand::prelude::random; |
| 23 | use rusqlite::{params, Connection, NO_PARAMS}; |
| 24 | #[cfg(test)] |
| 25 | use tests::random; |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 26 | |
| 27 | pub struct KeystoreDB { |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 28 | conn: Connection, |
| 29 | } |
| 30 | |
| 31 | impl KeystoreDB { |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 32 | // TODO(b/160882985): Figure out the location for this file. |
| 33 | #[cfg(not(test))] |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 34 | pub fn new() -> Result<KeystoreDB> { |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 35 | KeystoreDB::new_with_filename("persistent.sql") |
| 36 | } |
| 37 | |
| 38 | #[cfg(test)] |
| 39 | pub fn new() -> Result<KeystoreDB> { |
| 40 | KeystoreDB::new_with_filename("") |
| 41 | } |
| 42 | |
| 43 | fn new_with_filename(persistent_file: &str) -> Result<KeystoreDB> { |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 44 | let db = KeystoreDB { |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 45 | conn: Connection::open_in_memory() |
| 46 | .context("Failed to initialize sqlite connection.")?, |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 47 | }; |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 48 | db.attach_databases(persistent_file).context("Failed to create KeystoreDB.")?; |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 49 | db.init_tables().context("Failed to create KeystoreDB.")?; |
| 50 | Ok(db) |
| 51 | } |
| 52 | |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 53 | fn attach_databases(&self, persistent_file: &str) -> Result<()> { |
| 54 | self.conn |
| 55 | .execute("ATTACH DATABASE ? as 'persistent';", params![persistent_file]) |
| 56 | .context("Failed to attach databases.")?; |
| 57 | Ok(()) |
| 58 | } |
| 59 | |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 60 | fn init_tables(&self) -> Result<()> { |
| 61 | self.conn |
| 62 | .execute( |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 63 | "CREATE TABLE IF NOT EXISTS persistent.keyentry ( |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 64 | id INTEGER UNIQUE, |
| 65 | creation_date DATETIME, |
| 66 | domain INTEGER, |
| 67 | namespace INTEGER, |
| 68 | alias TEXT);", |
| 69 | NO_PARAMS, |
| 70 | ) |
| 71 | .context("Failed to initialize \"keyentry\" table.")?; |
| 72 | Ok(()) |
| 73 | } |
| 74 | |
| 75 | pub fn create_key_entry(&self, domain: aidl::Domain, namespace: i64) -> Result<i64> { |
| 76 | match domain { |
| 77 | aidl::Domain::App | aidl::Domain::SELinux => {} |
| 78 | _ => { |
| 79 | return Err(KsError::sys()) |
| 80 | .context(format!("Domain {:?} must be either App or SELinux.", domain)); |
| 81 | } |
| 82 | } |
| 83 | // Loop until we get a unique id. |
| 84 | loop { |
| 85 | let newid: i64 = random(); |
| 86 | let ret = self.conn.execute( |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 87 | "INSERT into persistent.keyentry (id, creation_date, domain, namespace, alias) |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 88 | VALUES(?, datetime('now'), ?, ?, NULL);", |
| 89 | params![newid, domain as i64, namespace], |
| 90 | ); |
| 91 | match ret { |
| 92 | // If the id already existed, try again. |
| 93 | Err(rusqlite::Error::SqliteFailure( |
| 94 | libsqlite3_sys::Error { |
| 95 | code: libsqlite3_sys::ErrorCode::ConstraintViolation, |
| 96 | extended_code: libsqlite3_sys::SQLITE_CONSTRAINT_UNIQUE, |
| 97 | }, |
| 98 | _, |
| 99 | )) => (), |
| 100 | _ => return Ok(newid), |
| 101 | } |
| 102 | } |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 103 | } |
| 104 | } |
| 105 | |
| 106 | #[cfg(test)] |
| 107 | mod tests { |
| 108 | |
| 109 | use super::*; |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 110 | use std::cell::RefCell; |
| 111 | |
| 112 | // Ensure that we're using the "injected" random function, not the real one. |
| 113 | #[test] |
| 114 | fn test_mocked_random() { |
| 115 | let rand1 = random(); |
| 116 | let rand2 = random(); |
| 117 | let rand3 = random(); |
| 118 | if rand1 == rand2 { |
| 119 | assert_eq!(rand2 + 1, rand3); |
| 120 | } else { |
| 121 | assert_eq!(rand1 + 1, rand2); |
| 122 | assert_eq!(rand2, rand3); |
| 123 | } |
| 124 | } |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 125 | |
| 126 | // Ensure we can initialize the database. |
| 127 | #[test] |
| 128 | fn test_new() -> Result<()> { |
| 129 | KeystoreDB::new()?; |
| 130 | Ok(()) |
| 131 | } |
| 132 | |
| 133 | // Test that we have the correct tables. |
| 134 | #[test] |
| 135 | fn test_tables() -> Result<()> { |
| 136 | let db = KeystoreDB::new()?; |
| 137 | let tables = db |
| 138 | .conn |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 139 | .prepare("SELECT name from persistent.sqlite_master WHERE type='table' ORDER BY name;")? |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 140 | .query_map(params![], |row| row.get(0))? |
| 141 | .collect::<rusqlite::Result<Vec<String>>>()?; |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 142 | assert_eq!(tables.len(), 1); |
| 143 | assert_eq!(tables[0], "keyentry"); |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 144 | Ok(()) |
| 145 | } |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 146 | |
| 147 | #[test] |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 148 | fn test_no_persistence_for_tests() -> Result<()> { |
| 149 | let db = KeystoreDB::new()?; |
| 150 | |
| 151 | db.create_key_entry(aidl::Domain::App, 100)?; |
| 152 | let entries = get_keyentry(&db)?; |
| 153 | assert_eq!(entries.len(), 1); |
| 154 | let db = KeystoreDB::new()?; |
| 155 | |
| 156 | let entries = get_keyentry(&db)?; |
| 157 | assert_eq!(entries.len(), 0); |
| 158 | Ok(()) |
| 159 | } |
| 160 | |
| 161 | #[test] |
| 162 | fn test_persistence_for_files() -> Result<()> { |
| 163 | let persistent = TempFile { filename: "/data/local/tmp/persistent.sql" }; |
| 164 | let db = KeystoreDB::new_with_filename(persistent.filename)?; |
| 165 | |
| 166 | db.create_key_entry(aidl::Domain::App, 100)?; |
| 167 | let entries = get_keyentry(&db)?; |
| 168 | assert_eq!(entries.len(), 1); |
| 169 | let db = KeystoreDB::new_with_filename(persistent.filename)?; |
| 170 | |
| 171 | let entries_new = get_keyentry(&db)?; |
| 172 | assert_eq!(entries, entries_new); |
| 173 | Ok(()) |
| 174 | } |
| 175 | |
| 176 | #[test] |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 177 | fn test_create_key_entry() -> Result<()> { |
| 178 | use aidl::Domain; |
| 179 | |
| 180 | fn extractor(ke: &KeyEntryRow) -> (Domain, i64, Option<&str>) { |
| 181 | (ke.domain.unwrap(), ke.namespace.unwrap(), ke.alias.as_deref()) |
| 182 | } |
| 183 | |
| 184 | let db = KeystoreDB::new()?; |
| 185 | |
| 186 | db.create_key_entry(Domain::App, 100)?; |
| 187 | db.create_key_entry(Domain::SELinux, 101)?; |
| 188 | |
| 189 | let entries = get_keyentry(&db)?; |
| 190 | assert_eq!(entries.len(), 2); |
| 191 | assert_eq!(extractor(&entries[0]), (Domain::App, 100, None)); |
| 192 | assert_eq!(extractor(&entries[1]), (Domain::SELinux, 101, None)); |
| 193 | |
| 194 | // Test that we must pass in a valid Domain. |
| 195 | check_result_is_error_containing_string( |
| 196 | db.create_key_entry(Domain::Grant, 102), |
| 197 | "Domain Grant must be either App or SELinux.", |
| 198 | ); |
| 199 | check_result_is_error_containing_string( |
| 200 | db.create_key_entry(Domain::Blob, 103), |
| 201 | "Domain Blob must be either App or SELinux.", |
| 202 | ); |
| 203 | check_result_is_error_containing_string( |
| 204 | db.create_key_entry(Domain::KeyId, 104), |
| 205 | "Domain KeyId must be either App or SELinux.", |
| 206 | ); |
| 207 | |
| 208 | Ok(()) |
| 209 | } |
| 210 | |
| 211 | // Helpers |
| 212 | |
| 213 | // Checks that the given result is an error containing the given string. |
| 214 | fn check_result_is_error_containing_string<T>(result: Result<T>, target: &str) { |
| 215 | let error_str = format!( |
| 216 | "{:#?}", |
| 217 | result.err().unwrap_or_else(|| panic!("Expected the error: {}", target)) |
| 218 | ); |
| 219 | assert!( |
| 220 | error_str.contains(target), |
| 221 | "The string \"{}\" should contain \"{}\"", |
| 222 | error_str, |
| 223 | target |
| 224 | ); |
| 225 | } |
| 226 | |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 227 | #[derive(Debug, PartialEq)] |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 228 | #[allow(dead_code)] |
| 229 | struct KeyEntryRow { |
| 230 | id: u32, |
| 231 | creation_date: String, |
| 232 | domain: Option<aidl::Domain>, |
| 233 | namespace: Option<i64>, |
| 234 | alias: Option<String>, |
| 235 | } |
| 236 | |
| 237 | fn get_keyentry(db: &KeystoreDB) -> Result<Vec<KeyEntryRow>> { |
| 238 | db.conn |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 239 | .prepare("SELECT * FROM persistent.keyentry;")? |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 240 | .query_map(NO_PARAMS, |row| { |
| 241 | let domain: Option<i32> = row.get(2)?; |
| 242 | Ok(KeyEntryRow { |
| 243 | id: row.get(0)?, |
| 244 | creation_date: row.get(1)?, |
| 245 | domain: domain.map(domain_from_integer), |
| 246 | namespace: row.get(3)?, |
| 247 | alias: row.get(4)?, |
| 248 | }) |
| 249 | })? |
| 250 | .map(|r| r.context("Could not read keyentry row.")) |
| 251 | .collect::<Result<Vec<_>>>() |
| 252 | } |
| 253 | |
| 254 | // TODO: Replace this with num_derive. |
| 255 | fn domain_from_integer(value: i32) -> aidl::Domain { |
| 256 | use aidl::Domain; |
| 257 | match value { |
| 258 | x if Domain::App as i32 == x => Domain::App, |
| 259 | x if Domain::Grant as i32 == x => Domain::Grant, |
| 260 | x if Domain::SELinux as i32 == x => Domain::SELinux, |
| 261 | x if Domain::Blob as i32 == x => Domain::Blob, |
| 262 | x if Domain::KeyId as i32 == x => Domain::KeyId, |
| 263 | _ => panic!("Unexpected domain: {}", value), |
| 264 | } |
| 265 | } |
| 266 | |
Joel Galenson | 2aab443 | 2020-07-22 15:27:57 -0700 | [diff] [blame^] | 267 | // A class that deletes a file when it is dropped. |
| 268 | // TODO: If we ever add a crate that does this, we can use it instead. |
| 269 | struct TempFile { |
| 270 | filename: &'static str, |
| 271 | } |
| 272 | |
| 273 | impl Drop for TempFile { |
| 274 | fn drop(&mut self) { |
| 275 | std::fs::remove_file(self.filename).expect("Cannot delete temporary file"); |
| 276 | } |
| 277 | } |
| 278 | |
Joel Galenson | 0891bc1 | 2020-07-20 10:37:03 -0700 | [diff] [blame] | 279 | // Use a custom random number generator that repeats each number once. |
| 280 | // This allows us to test repeated elements. |
| 281 | |
| 282 | thread_local! { |
| 283 | static RANDOM_COUNTER: RefCell<i64> = RefCell::new(0); |
| 284 | } |
| 285 | |
| 286 | pub fn random() -> i64 { |
| 287 | RANDOM_COUNTER.with(|counter| { |
| 288 | let result = *counter.borrow() / 2; |
| 289 | *counter.borrow_mut() += 1; |
| 290 | result |
| 291 | }) |
| 292 | } |
Joel Galenson | 26f4d01 | 2020-07-17 14:57:21 -0700 | [diff] [blame] | 293 | } |