Skip to main content

bwx/touchid/
blob.rs

1//! On-disk wrapper blob for Touch ID-enrolled vault keys.
2//!
3//! Written by `bwx touchid enroll`, read by the agent on unlock. Holds
4//! `CipherString`-wrapped vault keys + the Keychain label of the wrapping
5//! key. The wrapping key itself never touches disk.
6#![allow(clippy::doc_markdown)]
7
8use crate::locked;
9use crate::prelude::Error as BwxError;
10
11const FILENAME: &str = "touchid.json";
12
13#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
14pub struct Blob {
15    /// Keychain `kSecAttrAccount` value pointing at the wrapper key.
16    pub keychain_label: String,
17    /// Main vault key (64 bytes = 32 enc + 32 mac), wrapped with the
18    /// Keychain-held wrapper key.
19    pub wrapped_priv_key: String,
20    /// Per-organization 64-byte symmetric keys, each wrapped the same way.
21    pub wrapped_org_keys: std::collections::BTreeMap<String, String>,
22}
23
24impl Blob {
25    pub fn path() -> std::path::PathBuf {
26        crate::dirs::make_all().ok();
27        data_dir_for_blob().join(FILENAME)
28    }
29
30    pub fn exists() -> bool {
31        Self::path().exists()
32    }
33
34    pub fn load() -> Result<Self, BwxError> {
35        let path = Self::path();
36        let json = std::fs::read_to_string(&path).map_err(|source| {
37            BwxError::LoadConfig {
38                source,
39                file: path.clone(),
40            }
41        })?;
42        serde_json::from_str(&json)
43            .map_err(|source| BwxError::Json { source })
44    }
45
46    pub fn save(&self) -> Result<(), BwxError> {
47        use std::io::Write as _;
48        use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
49        let path = Self::path();
50        if let Some(parent) = path.parent() {
51            std::fs::create_dir_all(parent).map_err(|source| {
52                BwxError::SaveConfig {
53                    source,
54                    file: path.clone(),
55                }
56            })?;
57        }
58        let json = serde_json::to_string(self)
59            .map_err(|source| BwxError::Json { source })?;
60        let mut fh = std::fs::OpenOptions::new()
61            .write(true)
62            .create(true)
63            .truncate(true)
64            .mode(0o600)
65            .open(&path)
66            .map_err(|source| BwxError::SaveConfig {
67                source,
68                file: path.clone(),
69            })?;
70        fh.set_permissions(std::fs::Permissions::from_mode(0o600))
71            .map_err(|source| BwxError::SaveConfig {
72                source,
73                file: path.clone(),
74            })?;
75        fh.write_all(json.as_bytes())
76            .map_err(|source| BwxError::SaveConfig { source, file: path })?;
77        Ok(())
78    }
79
80    pub fn remove() -> Result<(), BwxError> {
81        let path = Self::path();
82        match std::fs::remove_file(&path) {
83            Ok(()) => Ok(()),
84            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
85            Err(source) => Err(BwxError::SaveConfig { source, file: path }),
86        }
87    }
88}
89
90fn data_dir_for_blob() -> std::path::PathBuf {
91    // Reuse the data dir that holds agent.err, agent.out, device_id, etc.
92    let p = crate::dirs::agent_stdout_file();
93    p.parent().map_or_else(
94        || std::path::PathBuf::from("."),
95        std::path::Path::to_path_buf,
96    )
97}
98
99/// Derive a 64-byte wrapping `Keys` from a random 64-byte seed. The
100/// Keychain stores the seed; at use time it is wrapped in a `locked::Keys`
101/// for the existing `CipherString` APIs.
102pub fn keys_from_wrapper_seed(seed: &[u8]) -> locked::Keys {
103    assert_eq!(seed.len(), 64, "wrapper seed must be 64 bytes");
104    let mut buf = locked::Vec::new();
105    buf.extend(seed.iter().copied());
106    locked::Keys::new(buf)
107}