use super::ios_accessibility::{account_name, item_class, legacy_class, target_class};
use crate::tests::*;
use log::*;
use security_framework::passwords::{
delete_generic_password, get_generic_password, set_generic_password,
};
use std::sync::{Mutex, MutexGuard};
static LOCKED_SERVICE: &str = "locked.keychain-rs\tio";
static LEGACY_KEY: &str = "locked-legacy@keychain-rs.io";
static PROMOTED_KEY: &str = "locked-promoted@keychain-rs.io";
static ABSENT_KEY: &str = "locked-absent@keychain-rs.io";
static REPAIR_KEY: &str = "locked-repair@keychain-rs.io";
static WRITE_KEY: &str = "locked-write@keychain-rs.io";
static LEGACY_VALUE: &str = "negative control";
static PROMOTED_VALUE: &str = "promoted secret";
static REPAIR_VALUE: &str = "repair fixture";
static WRITE_VALUE: &str = "provisioned while locked";
static FIXTURES: [&str; 5] = [LEGACY_KEY, PROMOTED_KEY, ABSENT_KEY, REPAIR_KEY, WRITE_KEY];
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum ReadOutcome {
Value,
Mismatch,
Locked,
Absent,
Failed,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum WriteOutcome {
Wrote,
Locked,
Failed,
}
struct Phase {
probes: u32,
legacy: Option<ReadOutcome>,
legacy_value_reads: u32,
promoted: Option<ReadOutcome>,
promoted_value_reads: u32,
promoted_other_reads: u32,
promoted_promotions: u64,
absent: Option<ReadOutcome>,
absent_claims: u32,
write: Option<WriteOutcome>,
repair: Option<bool>,
}
impl Phase {
const fn new() -> Self {
Phase {
probes: 0,
legacy: None,
legacy_value_reads: 0,
promoted: None,
promoted_value_reads: 0,
promoted_other_reads: 0,
promoted_promotions: 0,
absent: None,
absent_claims: 0,
write: None,
repair: None,
}
}
}
struct State {
seeded: bool,
locked: Phase,
unlocked: Phase,
}
static STATE: Mutex<State> = Mutex::new(State {
seeded: false,
locked: Phase::new(),
unlocked: Phase::new(),
});
fn state() -> MutexGuard<'static, State> {
STATE.lock().unwrap_or_else(|e| e.into_inner())
}
fn account(key: &str) -> String {
account_name(LOCKED_SERVICE, key)
}
fn read(manager: &KeyringManager, key: &str, expect: &str) -> ReadOutcome {
match manager.with_keyring(LOCKED_SERVICE, key, |kr| kr.get_value()) {
Ok(ref v) if v == expect => ReadOutcome::Value,
Ok(v) => {
error!("LOCKTEST {} read {:?}, expected {:?}", key, v, expect);
ReadOutcome::Mismatch
}
Err(KeyringError::Locked) => ReadOutcome::Locked,
Err(KeyringError::NoPasswordFound) => ReadOutcome::Absent,
Err(e) => {
error!("LOCKTEST {} read failed: {:?}", key, e);
ReadOutcome::Failed
}
}
}
fn seed_legacy(key: &str, value: &str) -> bool {
let account = account(key);
let _ = delete_generic_password("", &account);
if let Err(e) = set_generic_password("", &account, value.as_bytes()) {
error!("LOCKTEST could not seed {}: {}", key, e);
return false;
}
let class = item_class(&account);
if class.as_deref() != Some(legacy_class().as_str()) {
error!(
"LOCKTEST {} seeded with class {:?}, expected {:?}",
key,
class,
legacy_class()
);
return false;
}
true
}
fn check(pass: bool, step: &str, detail: String) -> bool {
if pass {
info!("LOCKTEST PASS {}: {}", step, detail);
} else {
error!("LOCKTEST FAIL {}: {}", step, detail);
}
pass
}
#[no_mangle]
pub extern "C" fn locked_device_seed() -> i32 {
super::ios::init_logging();
let manager = match KeyringManager::new_secure(TEST_APPLICATION) {
Ok(manager) => manager,
Err(e) => {
error!("LOCKTEST no keychain: {}", e);
return 0;
}
};
let mut ok = true;
ok &= seed_legacy(LEGACY_KEY, LEGACY_VALUE);
ok &= seed_legacy(PROMOTED_KEY, PROMOTED_VALUE);
let promoting = manager.with_keyring(LOCKED_SERVICE, PROMOTED_KEY, |kr| kr.get_value());
if !matches!(promoting.as_deref(), Ok(v) if v == PROMOTED_VALUE) {
error!("LOCKTEST promoting read returned {:?}", promoting);
ok = false;
}
let class = item_class(&account(PROMOTED_KEY));
if class.as_deref() != Some(target_class().as_str()) {
error!(
"LOCKTEST {} did not promote, class is {:?}",
PROMOTED_KEY, class
);
ok = false;
}
ok &= seed_legacy(REPAIR_KEY, REPAIR_VALUE);
let _ = manager.with_keyring(LOCKED_SERVICE, ABSENT_KEY, |kr| kr.delete_value());
let absent = manager.with_keyring(LOCKED_SERVICE, ABSENT_KEY, |kr| kr.get_value());
if !matches!(absent, Err(KeyringError::NoPasswordFound)) {
error!("LOCKTEST absent key answered {:?} while unlocked", absent);
ok = false;
}
let _ = manager.with_keyring(LOCKED_SERVICE, WRITE_KEY, |kr| kr.delete_value());
state().seeded = ok;
if ok {
info!("LOCKTEST seeded, lock the device now");
} else {
error!("LOCKTEST seeding failed; a device before its first unlock cannot seed, which is step 4 itself");
}
ok as i32
}
#[no_mangle]
pub extern "C" fn locked_device_probe(protected_data_available: i32) {
let available = protected_data_available != 0;
let manager = match KeyringManager::new_secure(TEST_APPLICATION) {
Ok(manager) => manager,
Err(e) => {
error!("LOCKTEST no keychain: {}", e);
return;
}
};
let before = crate::ios::promotion_count();
let promoted = read(&manager, PROMOTED_KEY, PROMOTED_VALUE);
let promotions = crate::ios::promotion_count() - before;
let absent = read(&manager, ABSENT_KEY, "");
let legacy = if available {
None
} else {
Some(read(&manager, LEGACY_KEY, LEGACY_VALUE))
};
info!(
"LOCKTEST protected_data={} promoted={:?} absent={:?} legacy={:?} promotions={}",
available, promoted, absent, legacy, promotions
);
let mut state = state();
let phase = if available {
&mut state.unlocked
} else {
&mut state.locked
};
phase.probes += 1;
phase.promoted = Some(promoted);
if promoted == ReadOutcome::Value {
phase.promoted_value_reads += 1;
} else {
phase.promoted_other_reads += 1;
}
phase.promoted_promotions += promotions;
phase.absent = Some(absent);
if absent == ReadOutcome::Absent {
phase.absent_claims += 1;
}
if let Some(legacy) = legacy {
phase.legacy = Some(legacy);
if legacy == ReadOutcome::Value {
phase.legacy_value_reads += 1;
}
}
if available || phase.repair.is_some() {
return;
}
let write =
match manager.with_keyring(LOCKED_SERVICE, WRITE_KEY, |kr| kr.set_value(WRITE_VALUE)) {
Ok(()) => WriteOutcome::Wrote,
Err(KeyringError::Locked) => WriteOutcome::Locked,
Err(e) => {
error!("LOCKTEST write while locked failed: {:?}", e);
WriteOutcome::Failed
}
};
let repair = crate::ios::promote_accessibility(&account(REPAIR_KEY));
info!("LOCKTEST while locked: write={:?} repair={}", write, repair);
phase.write = Some(write);
phase.repair = Some(repair);
}
#[no_mangle]
pub extern "C" fn locked_device_report() -> i32 {
let manager = match KeyringManager::new_secure(TEST_APPLICATION) {
Ok(manager) => manager,
Err(e) => {
error!("LOCKTEST no keychain: {}", e);
return 0;
}
};
let state = state();
let locked = &state.locked;
if locked.probes == 0 {
error!("LOCKTEST no probe ran while protected data was unavailable, nothing is proven");
return 0;
}
info!(
"LOCKTEST {} probes while locked, {} while unlocked",
locked.probes, state.unlocked.probes
);
if !state.seeded {
let verdict = check(
locked.write != Some(WriteOutcome::Wrote)
&& locked.promoted_value_reads == 0
&& locked.absent_claims == 0,
"nothing was seeded, so nothing should have answered",
format!(
"the write answered {:?}, and reads returned a value on {} of {} locked probes",
locked.write, locked.promoted_value_reads, locked.probes
),
);
info!("LOCKTEST no step was observed; reseed by relaunching while the device is unlocked");
return verdict as i32;
}
let mut ok = true;
ok &= check(
locked.legacy_value_reads == 0 && locked.legacy == Some(ReadOutcome::Locked),
"step 1 negative control",
format!(
"an unpromoted item answered {:?} while locked, returning its value on {} of {} probes",
locked.legacy, locked.legacy_value_reads, locked.probes
),
);
ok &= check(
locked.promoted_value_reads > 0 && locked.promoted_other_reads == 0,
"step 2 the fix",
format!(
"the promoted item returned its value on {} of {} locked probes, last answer {:?}",
locked.promoted_value_reads, locked.probes, locked.promoted
),
);
ok &= check(
locked.promoted_promotions == 0,
"step 2 promotion converged",
format!(
"locked reads of the promoted item issued {} accessibility updates",
locked.promoted_promotions
),
);
ok &= check(
locked.absent_claims == 0 && locked.absent == Some(ReadOutcome::Locked),
"step 3 absence is never claimed under lock",
format!(
"a key that was never written answered {:?} while locked, claiming absence on {} of {} probes",
locked.absent, locked.absent_claims, locked.probes
),
);
ok &= check(
locked.write == Some(WriteOutcome::Wrote),
"step 4 a new key can be provisioned while locked",
format!("a write to an absent key answered {:?}", locked.write),
);
ok &= check(
locked.repair == Some(false),
"step 5 repair under lock is inert",
format!(
"promote_accessibility answered {:?} while locked",
locked.repair
),
);
let raw = get_generic_password("", &account(REPAIR_KEY));
ok &= check(
matches!(&raw, Ok(v) if v == REPAIR_VALUE.as_bytes()),
"step 5 the item survived the failed repair",
format!(
"it holds {:?}",
raw.as_ref().map(|v| String::from_utf8_lossy(v))
),
);
let repaired = read(&manager, REPAIR_KEY, REPAIR_VALUE);
let repaired_class = item_class(&account(REPAIR_KEY));
ok &= check(
repaired == ReadOutcome::Value
&& repaired_class.as_deref() == Some(target_class().as_str()),
"step 5 the item promotes once unlocked",
format!(
"it read {:?} and now holds class {:?}",
repaired, repaired_class
),
);
let legacy = read(&manager, LEGACY_KEY, LEGACY_VALUE);
ok &= check(
legacy == ReadOutcome::Value,
"the negative control was a lock artifact",
format!("the same item read {:?} once unlocked", legacy),
);
for key in FIXTURES {
let _ = manager.with_keyring(LOCKED_SERVICE, key, |kr| kr.delete_value());
}
if ok {
info!("LOCKTEST locked device test PASSED");
} else {
error!("LOCKTEST locked device test FAILED");
}
ok as i32
}