blob: 394b7be6236099af64827b3cf80b08aa69fca108 [file] [log] [blame]
Joel Galenson26f4d012020-07-17 14:57:21 -07001// 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 Galenson0891bc12020-07-20 10:37:03 -070018use crate::error::Error as KsError;
Joel Galenson26f4d012020-07-17 14:57:21 -070019use anyhow::{Context, Result};
Joel Galenson0891bc12020-07-20 10:37:03 -070020use keystore_aidl_generated as aidl;
21#[cfg(not(test))]
22use rand::prelude::random;
Joel Galenson33c04ad2020-08-03 11:04:38 -070023use rusqlite::{params, Connection, TransactionBehavior, NO_PARAMS};
Joel Galenson0891bc12020-07-20 10:37:03 -070024#[cfg(test)]
25use tests::random;
Joel Galenson26f4d012020-07-17 14:57:21 -070026
27pub struct KeystoreDB {
Joel Galenson26f4d012020-07-17 14:57:21 -070028 conn: Connection,
29}
30
31impl KeystoreDB {
Joel Galenson2aab4432020-07-22 15:27:57 -070032 // TODO(b/160882985): Figure out the location for this file.
33 #[cfg(not(test))]
Joel Galenson26f4d012020-07-17 14:57:21 -070034 pub fn new() -> Result<KeystoreDB> {
Joel Galenson2aab4432020-07-22 15:27:57 -070035 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 Galenson0891bc12020-07-20 10:37:03 -070044 let db = KeystoreDB {
Joel Galenson26f4d012020-07-17 14:57:21 -070045 conn: Connection::open_in_memory()
46 .context("Failed to initialize sqlite connection.")?,
Joel Galenson0891bc12020-07-20 10:37:03 -070047 };
Joel Galenson2aab4432020-07-22 15:27:57 -070048 db.attach_databases(persistent_file).context("Failed to create KeystoreDB.")?;
Joel Galenson0891bc12020-07-20 10:37:03 -070049 db.init_tables().context("Failed to create KeystoreDB.")?;
50 Ok(db)
51 }
52
Joel Galenson2aab4432020-07-22 15:27:57 -070053 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 Galenson0891bc12020-07-20 10:37:03 -070060 fn init_tables(&self) -> Result<()> {
61 self.conn
62 .execute(
Joel Galenson2aab4432020-07-22 15:27:57 -070063 "CREATE TABLE IF NOT EXISTS persistent.keyentry (
Joel Galenson0891bc12020-07-20 10:37:03 -070064 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 Galenson2aab4432020-07-22 15:27:57 -070087 "INSERT into persistent.keyentry (id, creation_date, domain, namespace, alias)
Joel Galenson0891bc12020-07-20 10:37:03 -070088 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 Galenson26f4d012020-07-17 14:57:21 -0700103 }
Joel Galenson33c04ad2020-08-03 11:04:38 -0700104
105 pub fn rebind_alias(
106 &mut self,
107 newid: u32,
108 alias: &str,
109 domain: aidl::Domain,
110 namespace: i64,
111 ) -> Result<()> {
112 match domain {
113 aidl::Domain::App | aidl::Domain::SELinux => {}
114 _ => {
115 return Err(KsError::sys())
116 .context(format!("Domain {:?} must be either App or SELinux.", domain));
117 }
118 }
119 let tx = self
120 .conn
121 .transaction_with_behavior(TransactionBehavior::Immediate)
122 .context("Failed to initialize transaction.")?;
123 tx.execute(
124 "UPDATE persistent.keyentry
125 SET alias = NULL, domain = NULL, namespace = NULL
126 WHERE alias = ? AND domain = ? AND namespace = ?;",
127 params![alias, domain as i64, namespace],
128 )
129 .context("Failed to rebind existing entry.")?;
130 let result = tx
131 .execute(
132 "UPDATE persistent.keyentry
133 SET alias = ?
134 WHERE id = ? AND domain = ? AND namespace = ?;",
135 params![alias, newid, domain as i64, namespace],
136 )
137 .context("Failed to set alias.")?;
138 if result != 1 {
139 // Note that this explicit rollback is not required, as
140 // the transaction should rollback if we do not commit it.
141 // We leave it here for readability.
142 tx.rollback().context("Failed to rollback a failed transaction.")?;
143 return Err(KsError::sys()).context(format!(
144 "Expected to update a single entry but instead updated {}.",
145 result
146 ));
147 }
148 tx.commit().context("Failed to commit transaction.")
149 }
Joel Galenson26f4d012020-07-17 14:57:21 -0700150}
151
152#[cfg(test)]
153mod tests {
154
155 use super::*;
Joel Galenson0891bc12020-07-20 10:37:03 -0700156 use std::cell::RefCell;
157
158 // Ensure that we're using the "injected" random function, not the real one.
159 #[test]
160 fn test_mocked_random() {
161 let rand1 = random();
162 let rand2 = random();
163 let rand3 = random();
164 if rand1 == rand2 {
165 assert_eq!(rand2 + 1, rand3);
166 } else {
167 assert_eq!(rand1 + 1, rand2);
168 assert_eq!(rand2, rand3);
169 }
170 }
Joel Galenson26f4d012020-07-17 14:57:21 -0700171
172 // Ensure we can initialize the database.
173 #[test]
174 fn test_new() -> Result<()> {
175 KeystoreDB::new()?;
176 Ok(())
177 }
178
179 // Test that we have the correct tables.
180 #[test]
181 fn test_tables() -> Result<()> {
182 let db = KeystoreDB::new()?;
183 let tables = db
184 .conn
Joel Galenson2aab4432020-07-22 15:27:57 -0700185 .prepare("SELECT name from persistent.sqlite_master WHERE type='table' ORDER BY name;")?
Joel Galenson26f4d012020-07-17 14:57:21 -0700186 .query_map(params![], |row| row.get(0))?
187 .collect::<rusqlite::Result<Vec<String>>>()?;
Joel Galenson0891bc12020-07-20 10:37:03 -0700188 assert_eq!(tables.len(), 1);
189 assert_eq!(tables[0], "keyentry");
Joel Galenson26f4d012020-07-17 14:57:21 -0700190 Ok(())
191 }
Joel Galenson0891bc12020-07-20 10:37:03 -0700192
193 #[test]
Joel Galenson2aab4432020-07-22 15:27:57 -0700194 fn test_no_persistence_for_tests() -> Result<()> {
195 let db = KeystoreDB::new()?;
196
197 db.create_key_entry(aidl::Domain::App, 100)?;
198 let entries = get_keyentry(&db)?;
199 assert_eq!(entries.len(), 1);
200 let db = KeystoreDB::new()?;
201
202 let entries = get_keyentry(&db)?;
203 assert_eq!(entries.len(), 0);
204 Ok(())
205 }
206
207 #[test]
208 fn test_persistence_for_files() -> Result<()> {
209 let persistent = TempFile { filename: "/data/local/tmp/persistent.sql" };
210 let db = KeystoreDB::new_with_filename(persistent.filename)?;
211
212 db.create_key_entry(aidl::Domain::App, 100)?;
213 let entries = get_keyentry(&db)?;
214 assert_eq!(entries.len(), 1);
215 let db = KeystoreDB::new_with_filename(persistent.filename)?;
216
217 let entries_new = get_keyentry(&db)?;
218 assert_eq!(entries, entries_new);
219 Ok(())
220 }
221
222 #[test]
Joel Galenson0891bc12020-07-20 10:37:03 -0700223 fn test_create_key_entry() -> Result<()> {
224 use aidl::Domain;
225
226 fn extractor(ke: &KeyEntryRow) -> (Domain, i64, Option<&str>) {
227 (ke.domain.unwrap(), ke.namespace.unwrap(), ke.alias.as_deref())
228 }
229
230 let db = KeystoreDB::new()?;
231
232 db.create_key_entry(Domain::App, 100)?;
233 db.create_key_entry(Domain::SELinux, 101)?;
234
235 let entries = get_keyentry(&db)?;
236 assert_eq!(entries.len(), 2);
237 assert_eq!(extractor(&entries[0]), (Domain::App, 100, None));
238 assert_eq!(extractor(&entries[1]), (Domain::SELinux, 101, None));
239
240 // Test that we must pass in a valid Domain.
241 check_result_is_error_containing_string(
242 db.create_key_entry(Domain::Grant, 102),
243 "Domain Grant must be either App or SELinux.",
244 );
245 check_result_is_error_containing_string(
246 db.create_key_entry(Domain::Blob, 103),
247 "Domain Blob must be either App or SELinux.",
248 );
249 check_result_is_error_containing_string(
250 db.create_key_entry(Domain::KeyId, 104),
251 "Domain KeyId must be either App or SELinux.",
252 );
253
254 Ok(())
255 }
256
Joel Galenson33c04ad2020-08-03 11:04:38 -0700257 #[test]
258 fn test_rebind_alias() -> Result<()> {
259 use aidl::Domain;
260
261 fn extractor(ke: &KeyEntryRow) -> (Option<Domain>, Option<i64>, Option<&str>) {
262 (ke.domain, ke.namespace, ke.alias.as_deref())
263 }
264
265 let mut db = KeystoreDB::new()?;
266 db.create_key_entry(Domain::App, 42)?;
267 db.create_key_entry(Domain::App, 42)?;
268 let entries = get_keyentry(&db)?;
269 assert_eq!(entries.len(), 2);
270 assert_eq!(extractor(&entries[0]), (Some(Domain::App), Some(42), None));
271 assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), None));
272
273 // Test that the first call to rebind_alias sets the alias.
274 db.rebind_alias(entries[0].id, "foo", Domain::App, 42)?;
275 let entries = get_keyentry(&db)?;
276 assert_eq!(entries.len(), 2);
277 assert_eq!(extractor(&entries[0]), (Some(Domain::App), Some(42), Some("foo")));
278 assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), None));
279
280 // Test that the second call to rebind_alias also empties the old one.
281 db.rebind_alias(entries[1].id, "foo", Domain::App, 42)?;
282 let entries = get_keyentry(&db)?;
283 assert_eq!(entries.len(), 2);
284 assert_eq!(extractor(&entries[0]), (None, None, None));
285 assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), Some("foo")));
286
287 // Test that we must pass in a valid Domain.
288 check_result_is_error_containing_string(
289 db.rebind_alias(0, "foo", Domain::Grant, 42),
290 "Domain Grant must be either App or SELinux.",
291 );
292 check_result_is_error_containing_string(
293 db.rebind_alias(0, "foo", Domain::Blob, 42),
294 "Domain Blob must be either App or SELinux.",
295 );
296 check_result_is_error_containing_string(
297 db.rebind_alias(0, "foo", Domain::KeyId, 42),
298 "Domain KeyId must be either App or SELinux.",
299 );
300
301 // Test that we correctly handle setting an alias for something that does not exist.
302 check_result_is_error_containing_string(
303 db.rebind_alias(0, "foo", Domain::SELinux, 42),
304 "Expected to update a single entry but instead updated 0",
305 );
306 // Test that we correctly abort the transaction in this case.
307 let entries = get_keyentry(&db)?;
308 assert_eq!(entries.len(), 2);
309 assert_eq!(extractor(&entries[0]), (None, None, None));
310 assert_eq!(extractor(&entries[1]), (Some(Domain::App), Some(42), Some("foo")));
311
312 Ok(())
313 }
314
Joel Galenson0891bc12020-07-20 10:37:03 -0700315 // Helpers
316
317 // Checks that the given result is an error containing the given string.
318 fn check_result_is_error_containing_string<T>(result: Result<T>, target: &str) {
319 let error_str = format!(
320 "{:#?}",
321 result.err().unwrap_or_else(|| panic!("Expected the error: {}", target))
322 );
323 assert!(
324 error_str.contains(target),
325 "The string \"{}\" should contain \"{}\"",
326 error_str,
327 target
328 );
329 }
330
Joel Galenson2aab4432020-07-22 15:27:57 -0700331 #[derive(Debug, PartialEq)]
Joel Galenson0891bc12020-07-20 10:37:03 -0700332 #[allow(dead_code)]
333 struct KeyEntryRow {
334 id: u32,
335 creation_date: String,
336 domain: Option<aidl::Domain>,
337 namespace: Option<i64>,
338 alias: Option<String>,
339 }
340
341 fn get_keyentry(db: &KeystoreDB) -> Result<Vec<KeyEntryRow>> {
342 db.conn
Joel Galenson2aab4432020-07-22 15:27:57 -0700343 .prepare("SELECT * FROM persistent.keyentry;")?
Joel Galenson0891bc12020-07-20 10:37:03 -0700344 .query_map(NO_PARAMS, |row| {
345 let domain: Option<i32> = row.get(2)?;
346 Ok(KeyEntryRow {
347 id: row.get(0)?,
348 creation_date: row.get(1)?,
349 domain: domain.map(domain_from_integer),
350 namespace: row.get(3)?,
351 alias: row.get(4)?,
352 })
353 })?
354 .map(|r| r.context("Could not read keyentry row."))
355 .collect::<Result<Vec<_>>>()
356 }
357
358 // TODO: Replace this with num_derive.
359 fn domain_from_integer(value: i32) -> aidl::Domain {
360 use aidl::Domain;
361 match value {
362 x if Domain::App as i32 == x => Domain::App,
363 x if Domain::Grant as i32 == x => Domain::Grant,
364 x if Domain::SELinux as i32 == x => Domain::SELinux,
365 x if Domain::Blob as i32 == x => Domain::Blob,
366 x if Domain::KeyId as i32 == x => Domain::KeyId,
367 _ => panic!("Unexpected domain: {}", value),
368 }
369 }
370
Joel Galenson2aab4432020-07-22 15:27:57 -0700371 // A class that deletes a file when it is dropped.
372 // TODO: If we ever add a crate that does this, we can use it instead.
373 struct TempFile {
374 filename: &'static str,
375 }
376
377 impl Drop for TempFile {
378 fn drop(&mut self) {
379 std::fs::remove_file(self.filename).expect("Cannot delete temporary file");
380 }
381 }
382
Joel Galenson0891bc12020-07-20 10:37:03 -0700383 // Use a custom random number generator that repeats each number once.
384 // This allows us to test repeated elements.
385
386 thread_local! {
387 static RANDOM_COUNTER: RefCell<i64> = RefCell::new(0);
388 }
389
390 pub fn random() -> i64 {
391 RANDOM_COUNTER.with(|counter| {
392 let result = *counter.borrow() / 2;
393 *counter.borrow_mut() += 1;
394 result
395 })
396 }
Joel Galenson26f4d012020-07-17 14:57:21 -0700397}