Skip to main content

verifyos_cli/parsers/
macho_scanner.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, thiserror::Error)]
5pub enum UsageScanError {
6    #[error("Failed to read executable: {0}")]
7    Io(#[from] std::io::Error),
8    #[error("Unable to resolve app executable")]
9    MissingExecutable,
10}
11
12#[derive(Debug, Default)]
13pub struct UsageScan {
14    pub required_keys: HashSet<&'static str>,
15    pub requires_location_key: bool,
16    pub evidence: HashSet<&'static str>,
17}
18
19pub fn scan_usage_from_app_bundle(app_bundle_path: &Path) -> Result<UsageScan, UsageScanError> {
20    let executable =
21        resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
22    scan_usage_from_executable(&executable)
23}
24
25fn resolve_executable_path(app_bundle_path: &Path) -> Option<PathBuf> {
26    let app_name = app_bundle_path
27        .file_name()
28        .and_then(|n| n.to_str())
29        .unwrap_or("")
30        .trim_end_matches(".app");
31
32    if app_name.is_empty() {
33        return None;
34    }
35
36    let executable_path = app_bundle_path.join(app_name);
37    if executable_path.exists() {
38        Some(executable_path)
39    } else {
40        None
41    }
42}
43
44fn scan_usage_from_executable(path: &Path) -> Result<UsageScan, UsageScanError> {
45    let bytes = std::fs::read(path)?;
46    let mut scan = UsageScan::default();
47
48    for (signature, requirement) in SIGNATURES {
49        if contains_subslice(&bytes, signature.as_bytes()) {
50            scan.evidence.insert(*signature);
51            match requirement {
52                Requirement::Key(key) => {
53                    scan.required_keys.insert(*key);
54                }
55                Requirement::AnyLocation => {
56                    scan.requires_location_key = true;
57                }
58            }
59        }
60    }
61
62    Ok(scan)
63}
64
65fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
66    haystack
67        .windows(needle.len())
68        .any(|window| window == needle)
69}
70
71#[derive(Clone, Copy)]
72enum Requirement {
73    Key(&'static str),
74    AnyLocation,
75}
76
77const SIGNATURES: &[(&str, Requirement)] = &[
78    (
79        "AVCaptureDevice",
80        Requirement::Key("NSCameraUsageDescription"),
81    ),
82    (
83        "AVAudioSession",
84        Requirement::Key("NSMicrophoneUsageDescription"),
85    ),
86    (
87        "AVAudioRecorder",
88        Requirement::Key("NSMicrophoneUsageDescription"),
89    ),
90    (
91        "PHPhotoLibrary",
92        Requirement::Key("NSPhotoLibraryUsageDescription"),
93    ),
94    (
95        "PHPhotoLibraryAddOnly",
96        Requirement::Key("NSPhotoLibraryAddUsageDescription"),
97    ),
98    ("CLLocationManager", Requirement::AnyLocation),
99    (
100        "CBCentralManager",
101        Requirement::Key("NSBluetoothAlwaysUsageDescription"),
102    ),
103    (
104        "CBPeripheralManager",
105        Requirement::Key("NSBluetoothAlwaysUsageDescription"),
106    ),
107    (
108        "CBPeripheral",
109        Requirement::Key("NSBluetoothPeripheralUsageDescription"),
110    ),
111    ("LAContext", Requirement::Key("NSFaceIDUsageDescription")),
112    (
113        "EKEventStore",
114        Requirement::Key("NSCalendarsUsageDescription"),
115    ),
116    (
117        "EKReminder",
118        Requirement::Key("NSRemindersUsageDescription"),
119    ),
120    (
121        "CNContactStore",
122        Requirement::Key("NSContactsUsageDescription"),
123    ),
124    (
125        "SFSpeechRecognizer",
126        Requirement::Key("NSSpeechRecognitionUsageDescription"),
127    ),
128    (
129        "CMMotionManager",
130        Requirement::Key("NSMotionUsageDescription"),
131    ),
132    ("CMPedometer", Requirement::Key("NSMotionUsageDescription")),
133    (
134        "MPMediaLibrary",
135        Requirement::Key("NSAppleMusicUsageDescription"),
136    ),
137    (
138        "HKHealthStore",
139        Requirement::Key("NSHealthShareUsageDescription"),
140    ),
141];