use kernel::{AxCode, AxError};
use serde::{Deserialize, Serialize};
use wire::{PrivacyScope, PrivacyTarget, PrivacyValue};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Hive {
CurrentUser,
LocalMachine,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Target {
Registry {
hive: Hive,
path: &'static str,
name: &'static str,
},
UserEnvironment { name: &'static str },
ScheduledTask {
path: &'static str,
name: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OperationKind {
RegistryValueHklm,
RegistryValueHkcu,
EnvironmentVariableUser,
ScheduledTaskEnabled,
}
impl Target {
pub(crate) const fn kind(&self) -> OperationKind {
match self {
Self::Registry {
hive: Hive::LocalMachine,
..
} => OperationKind::RegistryValueHklm,
Self::Registry {
hive: Hive::CurrentUser,
..
} => OperationKind::RegistryValueHkcu,
Self::UserEnvironment { .. } => OperationKind::EnvironmentVariableUser,
Self::ScheduledTask { .. } => OperationKind::ScheduledTaskEnabled,
}
}
}
impl OperationKind {
pub(crate) const fn scope(self) -> PrivacyScope {
match self {
Self::RegistryValueHklm | Self::ScheduledTaskEnabled => PrivacyScope::Machine,
Self::RegistryValueHkcu | Self::EnvironmentVariableUser => PrivacyScope::User,
}
}
}
impl From<&Target> for PrivacyTarget {
fn from(target: &Target) -> Self {
let owned = |text: &str| text.to_owned();
match *target {
Target::Registry {
hive: Hive::LocalMachine,
path,
name,
} => Self::RegistryValueHklm {
path: owned(path),
name: owned(name),
},
Target::Registry {
hive: Hive::CurrentUser,
path,
name,
} => Self::RegistryValueHkcu {
path: owned(path),
name: owned(name),
},
Target::UserEnvironment { name } => Self::EnvironmentVariableUser { name: owned(name) },
Target::ScheduledTask { path, name } => Self::ScheduledTaskEnabled {
path: owned(path),
name: owned(name),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
pub(crate) enum RawValue {
Absent,
Present { kind: u32, bytes: Vec<u8> },
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "state", rename_all = "snake_case", deny_unknown_fields)]
pub(crate) enum TaskState {
Absent,
Enabled { definition_sha256: [u8; 32] },
Disabled { definition_sha256: [u8; 32] },
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub(crate) enum Snapshot {
Registry(RawValue),
Task(TaskState),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Reading {
pub(crate) value: Snapshot,
pub(crate) key_existed: bool,
}
const REG_SZ: u32 = 1;
const REG_DWORD: u32 = 4;
impl RawValue {
pub(crate) fn dword(number: u32) -> Self {
Self::Present {
kind: REG_DWORD,
bytes: number.to_le_bytes().to_vec(),
}
}
pub(crate) fn text(text: &str) -> Self {
Self::Present {
kind: REG_SZ,
bytes: text
.encode_utf16()
.chain([0])
.flat_map(u16::to_le_bytes)
.collect(),
}
}
}
impl From<&Snapshot> for PrivacyValue {
fn from(snapshot: &Snapshot) -> Self {
match snapshot {
Snapshot::Registry(RawValue::Absent) => Self::Absent,
Snapshot::Registry(RawValue::Present { kind, bytes }) => present(*kind, bytes),
Snapshot::Task(TaskState::Absent) => Self::TaskAbsent,
Snapshot::Task(TaskState::Enabled { definition_sha256 }) => Self::TaskEnabled {
definition_sha256: hex(definition_sha256),
},
Snapshot::Task(TaskState::Disabled { definition_sha256 }) => Self::TaskDisabled {
definition_sha256: hex(definition_sha256),
},
}
}
}
fn present(kind: u32, bytes: &[u8]) -> PrivacyValue {
let dword = <[u8; 4]>::try_from(bytes)
.ok()
.filter(|_| kind == REG_DWORD);
match (dword, text(kind, bytes)) {
(Some(number), _) => PrivacyValue::Dword {
number: u32::from_le_bytes(number),
},
(None, Some(text)) => PrivacyValue::Text { text },
(None, None) => PrivacyValue::Raw {
kind,
hex: hex(bytes),
},
}
}
fn text(kind: u32, bytes: &[u8]) -> Option<String> {
let units: Vec<u16> = bytes
.chunks(2)
.map(|pair| <[u8; 2]>::try_from(pair).ok().map(u16::from_le_bytes))
.collect::<Option<_>>()?;
let (&last, body) = units.split_last()?;
(kind == REG_SZ && last == 0)
.then(|| {
char::decode_utf16(body.iter().copied())
.collect::<Result<String, _>>()
.ok()
})
.flatten()
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
fn bytes_of_hex(hex: &str) -> Option<Vec<u8>> {
let digit = |byte: u8| match byte {
b'0'..=b'9' => byte.checked_sub(b'0'),
b'a'..=b'f' => byte
.checked_sub(b'a')
.and_then(|value| value.checked_add(10)),
_ => None,
};
hex.as_bytes()
.chunks(2)
.map(|pair| match *pair {
[high, low] => digit(high)?.checked_mul(16)?.checked_add(digit(low)?),
_ => None,
})
.collect()
}
impl TryFrom<&PrivacyValue> for Snapshot {
type Error = AxError;
fn try_from(value: &PrivacyValue) -> Result<Self, AxError> {
let digest =
|hex: &str| bytes_of_hex(hex).and_then(|bytes| <[u8; 32]>::try_from(bytes).ok());
let snapshot = match value {
PrivacyValue::Absent => Some(Self::Registry(RawValue::Absent)),
PrivacyValue::Dword { number } => Some(Self::Registry(RawValue::dword(*number))),
PrivacyValue::Text { text } => Some(Self::Registry(RawValue::text(text))),
PrivacyValue::Raw { kind, hex } => bytes_of_hex(hex)
.map(|bytes| Self::Registry(RawValue::Present { kind: *kind, bytes })),
PrivacyValue::TaskAbsent => Some(Self::Task(TaskState::Absent)),
PrivacyValue::TaskEnabled { definition_sha256 } => digest(definition_sha256)
.map(|definition_sha256| Self::Task(TaskState::Enabled { definition_sha256 })),
PrivacyValue::TaskDisabled { definition_sha256 } => digest(definition_sha256)
.map(|definition_sha256| Self::Task(TaskState::Disabled { definition_sha256 })),
};
snapshot
.filter(|snapshot| PrivacyValue::from(snapshot) == *value)
.ok_or_else(|| {
AxError::failure(
AxCode::InvalidArgs,
"read the value a privacy page confirmed",
"the value is not spelled the way the host spells it",
)
.with_recovery("send back the value the privacy page was answered with, unchanged")
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "test code")]
mod tests {
use super::*;
use proptest::prelude::*;
fn snapshots() -> impl Strategy<Value = Snapshot> {
let bytes = proptest::collection::vec(any::<u8>(), 0..16);
let text = ".{0,6}".prop_map(|text: String| Snapshot::Registry(RawValue::text(&text)));
let digest = any::<[u8; 32]>();
prop_oneof![
Just(Snapshot::Registry(RawValue::Absent)),
(0u32..12, bytes)
.prop_map(|(kind, bytes)| Snapshot::Registry(RawValue::Present { kind, bytes })),
any::<u32>().prop_map(|number| Snapshot::Registry(RawValue::dword(number))),
text,
Just(Snapshot::Task(TaskState::Absent)),
digest.prop_map(|definition_sha256| Snapshot::Task(TaskState::Enabled {
definition_sha256
})),
digest.prop_map(|definition_sha256| Snapshot::Task(TaskState::Disabled {
definition_sha256
})),
]
}
proptest! {
#[test]
fn a_snapshot_survives_its_wire_spelling(snapshot in snapshots()) {
let value = PrivacyValue::from(&snapshot);
prop_assert_eq!(Snapshot::try_from(&value).unwrap(), snapshot);
}
}
#[test]
fn only_the_hosts_spelling_is_accepted() {
let raw = |kind, hex: &str| PrivacyValue::Raw {
kind,
hex: hex.to_owned(),
};
for value in [
raw(REG_DWORD, "01000000"),
raw(REG_SZ, "31000000"),
raw(3, "0g"),
raw(3, "0"),
raw(3, "AB"),
PrivacyValue::TaskEnabled {
definition_sha256: "00".repeat(31),
},
] {
assert_eq!(
*Snapshot::try_from(&value).unwrap_err().code(),
AxCode::InvalidArgs,
"{value:?}"
);
}
}
}