diff --git a/keystore2/src/database/versioning.rs b/keystore2/src/database/versioning.rs new file mode 100644 index 00000000..e3a95c8e --- /dev/null +++ b/keystore2/src/database/versioning.rs @@ -0,0 +1,379 @@ +// 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. + +use anyhow::{anyhow, Context, Result}; +use rusqlite::{params, OptionalExtension, Transaction, NO_PARAMS}; + +pub fn create_or_get_version(tx: &Transaction, current_version: u32) -> Result { + tx.execute( + "CREATE TABLE IF NOT EXISTS persistent.version ( + id INTEGER PRIMARY KEY, + version INTEGER);", + NO_PARAMS, + ) + .context("In create_or_get_version: Failed to create version table.")?; + + let version = tx + .query_row("SELECT version FROM persistent.version WHERE id = 0;", NO_PARAMS, |row| { + row.get(0) + }) + .optional() + .context("In create_or_get_version: Failed to read version.")?; + + let version = if let Some(version) = version { + version + } else { + // If no version table existed it could mean one of two things: + // 1) This database is completely new. In this case the version has to be set + // to the current version and the current version which also needs to be + // returned. + // 2) The database predates db versioning. In this case the version needs to be + // set to 0, and 0 needs to be returned. + let version = if tx + .query_row( + "SELECT name FROM persistent.sqlite_master + WHERE type = 'table' AND name = 'keyentry';", + NO_PARAMS, + |_| Ok(()), + ) + .optional() + .context("In create_or_get_version: Failed to check for keyentry table.")? + .is_none() + { + current_version + } else { + 0 + }; + + tx.execute("INSERT INTO persistent.version (id, version) VALUES(0, ?);", params![version]) + .context("In create_or_get_version: Failed to insert initial version.")?; + version + }; + Ok(version) +} + +pub fn update_version(tx: &Transaction, new_version: u32) -> Result<()> { + let updated = tx + .execute("UPDATE persistent.version SET version = ? WHERE id = 0;", params![new_version]) + .context("In update_version: Failed to update row.")?; + if updated == 1 { + Ok(()) + } else { + Err(anyhow!("In update_version: No rows were updated.")) + } +} + +pub fn upgrade_database(tx: &Transaction, current_version: u32, upgraders: &[F]) -> Result<()> +where + F: Fn(&Transaction) -> Result + 'static, +{ + if upgraders.len() < current_version as usize { + return Err(anyhow!("In upgrade_database: Insufficient upgraders provided.")); + } + let mut db_version = create_or_get_version(tx, current_version) + .context("In upgrade_database: Failed to get database version.")?; + while db_version < current_version { + db_version = upgraders[db_version as usize](tx).with_context(|| { + format!("In upgrade_database: Trying to upgrade from db version {}.", db_version) + })?; + } + update_version(tx, db_version).context("In upgrade_database.") +} + +#[cfg(test)] +mod test { + use super::*; + use rusqlite::{Connection, TransactionBehavior, NO_PARAMS}; + + #[test] + fn upgrade_database_test() { + let mut conn = Connection::open_in_memory().unwrap(); + conn.execute("ATTACH DATABASE 'file::memory:' as persistent;", NO_PARAMS).unwrap(); + + let upgraders: Vec<_> = (0..30_u32) + .map(move |i| { + move |tx: &Transaction| { + tx.execute( + "INSERT INTO persistent.test (test_field) VALUES(?);", + params![i + 1], + ) + .with_context(|| format!("In upgrade_from_{}_to_{}.", i, i + 1))?; + Ok(i + 1) + } + }) + .collect(); + + for legacy in &[false, true] { + if *legacy { + conn.execute( + "CREATE TABLE IF NOT EXISTS persistent.keyentry ( + id INTEGER UNIQUE, + key_type INTEGER, + domain INTEGER, + namespace INTEGER, + alias BLOB, + state INTEGER, + km_uuid BLOB);", + NO_PARAMS, + ) + .unwrap(); + } + for from in 1..29 { + for to in from..30 { + conn.execute("DROP TABLE IF EXISTS persistent.version;", NO_PARAMS).unwrap(); + conn.execute("DROP TABLE IF EXISTS persistent.test;", NO_PARAMS).unwrap(); + conn.execute( + "CREATE TABLE IF NOT EXISTS persistent.test ( + id INTEGER PRIMARY KEY, + test_field INTEGER);", + NO_PARAMS, + ) + .unwrap(); + + { + let tx = + conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap(); + create_or_get_version(&tx, from).unwrap(); + tx.commit().unwrap(); + } + { + let tx = + conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap(); + upgrade_database(&tx, to, &upgraders).unwrap(); + tx.commit().unwrap(); + } + + // In the legacy database case all upgraders starting from 0 have to run. So + // after the upgrade step, the expectations need to be adjusted. + let from = if *legacy { 0 } else { from }; + + // There must be exactly to - from rows. + assert_eq!( + to - from, + conn.query_row( + "SELECT COUNT(test_field) FROM persistent.test;", + NO_PARAMS, + |row| row.get(0) + ) + .unwrap() + ); + // Each row must have the correct relation between id and test_field. If this + // is not the case, the upgraders were not executed in the correct order. + assert_eq!( + to - from, + conn.query_row( + "SELECT COUNT(test_field) FROM persistent.test + WHERE id = test_field - ?;", + params![from], + |row| row.get(0) + ) + .unwrap() + ); + } + } + } + } + + #[test] + fn create_or_get_version_new_database() { + let mut conn = Connection::open_in_memory().unwrap(); + conn.execute("ATTACH DATABASE 'file::memory:' as persistent;", NO_PARAMS).unwrap(); + { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap(); + let version = create_or_get_version(&tx, 3).unwrap(); + tx.commit().unwrap(); + assert_eq!(version, 3); + } + + // Was the version table created as expected? + assert_eq!( + Ok("version".to_owned()), + conn.query_row( + "SELECT name FROM persistent.sqlite_master + WHERE type = 'table' AND name = 'version';", + NO_PARAMS, + |row| row.get(0), + ) + ); + + // There is exactly one row in the version table. + assert_eq!( + Ok(1), + conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row + .get(0)) + ); + + // The version must be set to 3 + assert_eq!( + Ok(3), + conn.query_row( + "SELECT version from persistent.version WHERE id = 0;", + NO_PARAMS, + |row| row.get(0) + ) + ); + + // Will subsequent calls to create_or_get_version still return the same version even + // if the current version changes. + { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap(); + let version = create_or_get_version(&tx, 5).unwrap(); + tx.commit().unwrap(); + assert_eq!(version, 3); + } + + // There is still exactly one row in the version table. + assert_eq!( + Ok(1), + conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row + .get(0)) + ); + + // Bump the version. + { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap(); + update_version(&tx, 5).unwrap(); + tx.commit().unwrap(); + } + + // Now the version should have changed. + { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap(); + let version = create_or_get_version(&tx, 7).unwrap(); + tx.commit().unwrap(); + assert_eq!(version, 5); + } + + // There is still exactly one row in the version table. + assert_eq!( + Ok(1), + conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row + .get(0)) + ); + + // The version must be set to 5 + assert_eq!( + Ok(5), + conn.query_row( + "SELECT version from persistent.version WHERE id = 0;", + NO_PARAMS, + |row| row.get(0) + ) + ); + } + + #[test] + fn create_or_get_version_legacy_database() { + let mut conn = Connection::open_in_memory().unwrap(); + conn.execute("ATTACH DATABASE 'file::memory:' as persistent;", NO_PARAMS).unwrap(); + // A legacy (version 0) database is detected if the keyentry table exists but no + // version table. + conn.execute( + "CREATE TABLE IF NOT EXISTS persistent.keyentry ( + id INTEGER UNIQUE, + key_type INTEGER, + domain INTEGER, + namespace INTEGER, + alias BLOB, + state INTEGER, + km_uuid BLOB);", + NO_PARAMS, + ) + .unwrap(); + + { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap(); + let version = create_or_get_version(&tx, 3).unwrap(); + tx.commit().unwrap(); + // In the legacy case, version 0 must be returned. + assert_eq!(version, 0); + } + + // Was the version table created as expected? + assert_eq!( + Ok("version".to_owned()), + conn.query_row( + "SELECT name FROM persistent.sqlite_master + WHERE type = 'table' AND name = 'version';", + NO_PARAMS, + |row| row.get(0), + ) + ); + + // There is exactly one row in the version table. + assert_eq!( + Ok(1), + conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row + .get(0)) + ); + + // The version must be set to 0 + assert_eq!( + Ok(0), + conn.query_row( + "SELECT version from persistent.version WHERE id = 0;", + NO_PARAMS, + |row| row.get(0) + ) + ); + + // Will subsequent calls to create_or_get_version still return the same version even + // if the current version changes. + { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap(); + let version = create_or_get_version(&tx, 5).unwrap(); + tx.commit().unwrap(); + assert_eq!(version, 0); + } + + // There is still exactly one row in the version table. + assert_eq!( + Ok(1), + conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row + .get(0)) + ); + + // Bump the version. + { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap(); + update_version(&tx, 5).unwrap(); + tx.commit().unwrap(); + } + + // Now the version should have changed. + { + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap(); + let version = create_or_get_version(&tx, 7).unwrap(); + tx.commit().unwrap(); + assert_eq!(version, 5); + } + + // There is still exactly one row in the version table. + assert_eq!( + Ok(1), + conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row + .get(0)) + ); + + // The version must be set to 5 + assert_eq!( + Ok(5), + conn.query_row( + "SELECT version from persistent.version WHERE id = 0;", + NO_PARAMS, + |row| row.get(0) + ) + ); + } +}