Skip to main content

bear_cli/cloudkit/
auth.rs

1use std::fs;
2use std::path::PathBuf;
3
4use anyhow::{Context, Result, anyhow};
5use serde::{Deserialize, Serialize};
6
7use crate::config::app_support_dir;
8
9const KEYCHAIN_SERVICE: &str = "bear-cli";
10const KEYCHAIN_ACCOUNT: &str = "ckWebAuthToken";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AuthConfig {
14    pub ck_web_auth_token: String,
15}
16
17impl AuthConfig {
18    /// Load from Keychain first, fall back to the config file.
19    pub fn load() -> Result<Self> {
20        if let Ok(token) = keychain_get() {
21            return Ok(Self {
22                ck_web_auth_token: token,
23            });
24        }
25        Self::load_from_file()
26    }
27
28    /// Save to both Keychain and the config file.
29    pub fn save(&self) -> Result<()> {
30        let _ = keychain_set(&self.ck_web_auth_token); // best-effort
31        self.save_to_file()
32    }
33
34    fn config_path() -> Result<PathBuf> {
35        Ok(app_support_dir()?.join("auth.json"))
36    }
37
38    fn load_from_file() -> Result<Self> {
39        let path = Self::config_path()?;
40        let contents = fs::read_to_string(&path).with_context(|| {
41            format!(
42                "auth token not found — run `bear auth <token>` first (checked {})",
43                path.display()
44            )
45        })?;
46        serde_json::from_str(&contents).context("invalid auth config")
47    }
48
49    fn save_to_file(&self) -> Result<()> {
50        let path = Self::config_path()?;
51        fs::create_dir_all(path.parent().unwrap())?;
52        let json = serde_json::to_string_pretty(self)?;
53        // Atomic write
54        let tmp = path.with_extension("tmp");
55        fs::write(&tmp, json)?;
56        fs::rename(&tmp, &path)?;
57        Ok(())
58    }
59}
60
61fn keychain_get() -> Result<String> {
62    let output = std::process::Command::new("security")
63        .args([
64            "find-generic-password",
65            "-s",
66            KEYCHAIN_SERVICE,
67            "-a",
68            KEYCHAIN_ACCOUNT,
69            "-w",
70        ])
71        .output()?;
72    if !output.status.success() {
73        return Err(anyhow!("keychain lookup failed"));
74    }
75    Ok(String::from_utf8(output.stdout)?.trim().to_string())
76}
77
78fn keychain_set(token: &str) -> Result<()> {
79    // Delete existing entry first (ignore errors)
80    let _ = std::process::Command::new("security")
81        .args([
82            "delete-generic-password",
83            "-s",
84            KEYCHAIN_SERVICE,
85            "-a",
86            KEYCHAIN_ACCOUNT,
87        ])
88        .output();
89
90    let status = std::process::Command::new("security")
91        .args([
92            "add-generic-password",
93            "-s",
94            KEYCHAIN_SERVICE,
95            "-a",
96            KEYCHAIN_ACCOUNT,
97            "-w",
98            token,
99        ])
100        .status()?;
101    if !status.success() {
102        return Err(anyhow!("failed to write token to Keychain"));
103    }
104    Ok(())
105}