vtcode_auth/credentials/
storage.rs1use anyhow::{Context, Result, anyhow};
4use base64::Engine;
5use std::fs;
6
7use super::encryption;
8use super::keyring;
9use super::mode::AuthCredentialsStoreMode;
10use crate::storage_paths::auth_storage_dir;
11use crate::storage_paths::write_private_file;
12
13pub struct CredentialStorage {
18 service: String,
19 user: String,
20}
21
22impl CredentialStorage {
23 pub fn new(service: impl Into<String>, user: impl Into<String>) -> Self {
25 Self { service: service.into(), user: user.into() }
26 }
27
28 pub fn store_with_mode(&self, value: &str, mode: AuthCredentialsStoreMode) -> Result<()> {
30 match mode.effective_mode() {
31 AuthCredentialsStoreMode::Keyring => match self.store_keyring(value) {
32 Ok(()) => {
33 if let Err(err) = self.store_file(value) {
34 tracing::warn!(
35 "Failed to write encrypted file backup for {}/{}: {}",
36 self.service,
37 self.user,
38 err
39 );
40 }
41 Ok(())
42 }
43 Err(err) => {
44 tracing::warn!(
45 "Failed to store credential in OS keyring for {}/{}; falling back to encrypted file storage: {}",
46 self.service,
47 self.user,
48 err
49 );
50 self.store_file(value).context("failed to store credential in encrypted file")
51 }
52 },
53 AuthCredentialsStoreMode::File => self.store_file(value),
54 AuthCredentialsStoreMode::Auto => unreachable!("effective_mode() resolves Auto"),
55 }
56 }
57
58 pub fn store(&self, value: &str) -> Result<()> {
60 self.store_with_mode(value, AuthCredentialsStoreMode::Auto)
61 }
62
63 pub fn load_with_mode(&self, mode: AuthCredentialsStoreMode) -> Result<Option<String>> {
65 match mode.effective_mode() {
66 AuthCredentialsStoreMode::Keyring => match self.load_keyring() {
67 Ok(Some(value)) => Ok(Some(value)),
68 Ok(None) => self.load_file(),
69 Err(err) => {
70 tracing::warn!(
71 "Failed to read credential from OS keyring for {}/{}; falling back to encrypted file storage: {}",
72 self.service,
73 self.user,
74 err
75 );
76 self.load_file()
77 }
78 },
79 AuthCredentialsStoreMode::File => self.load_file(),
80 AuthCredentialsStoreMode::Auto => unreachable!("effective_mode() resolves Auto"),
81 }
82 }
83
84 pub fn load(&self) -> Result<Option<String>> {
86 self.load_with_mode(AuthCredentialsStoreMode::Auto)
87 }
88
89 pub fn clear_with_mode(&self, mode: AuthCredentialsStoreMode) -> Result<()> {
91 match mode.effective_mode() {
92 AuthCredentialsStoreMode::Keyring => {
93 let mut errors = Vec::new();
94
95 if let Err(err) = self.clear_keyring() {
96 errors.push(err.to_string());
97 }
98 if let Err(err) = self.clear_file() {
99 errors.push(err.to_string());
100 }
101
102 if errors.is_empty() {
103 Ok(())
104 } else {
105 Err(anyhow!("Failed to clear credential from secure storage: {}", errors.join("; ")))
106 }
107 }
108 AuthCredentialsStoreMode::File => self.clear_file(),
109 AuthCredentialsStoreMode::Auto => unreachable!("effective_mode() resolves Auto"),
110 }
111 }
112
113 pub fn clear(&self) -> Result<()> {
115 self.clear_with_mode(AuthCredentialsStoreMode::Auto)
116 }
117
118 fn store_keyring(&self, value: &str) -> Result<()> {
123 let entry = keyring::entry(&self.service, &self.user).context("Failed to access OS keyring")?;
124 entry.set_password(value).context("Failed to store credential in OS keyring")?;
125 tracing::debug!("Credential stored in OS keyring for {}/{}", self.service, self.user);
126 Ok(())
127 }
128
129 fn load_keyring(&self) -> Result<Option<String>> {
130 let entry = match keyring::entry(&self.service, &self.user) {
131 Ok(e) => e,
132 Err(_) => return Ok(None),
133 };
134
135 match entry.get_password() {
136 Ok(value) => Ok(Some(value)),
137 Err(keyring_core::Error::NoEntry) => Ok(None),
138 Err(e) => Err(anyhow!("Failed to read from keyring: {e}")),
139 }
140 }
141
142 fn clear_keyring(&self) -> Result<()> {
143 let entry = match keyring::entry(&self.service, &self.user) {
144 Ok(e) => e,
145 Err(_) => return Ok(()),
146 };
147
148 match entry.delete_credential() {
149 Ok(_) => {
150 tracing::debug!("Credential cleared from keyring for {}/{}", self.service, self.user);
151 }
152 Err(keyring_core::Error::NoEntry) => {}
153 Err(e) => return Err(anyhow!("Failed to clear keyring entry: {e}")),
154 }
155
156 Ok(())
157 }
158
159 fn store_file(&self, value: &str) -> Result<()> {
160 let path = self.file_path()?;
161 let encrypted = encryption::encrypt(value)?;
162 let payload = serde_json::to_vec_pretty(&encrypted).context("failed to serialize encrypted credential")?;
163 write_private_file(&path, &payload).context("failed to write encrypted credential file")?;
164 Ok(())
165 }
166
167 fn load_file(&self) -> Result<Option<String>> {
168 let path = self.file_path()?;
169 let data = match fs::read(&path) {
170 Ok(data) => data,
171 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
172 Err(err) => return Err(anyhow!("failed to read encrypted credential file: {err}")),
173 };
174
175 let encrypted: encryption::EncryptedCredential =
176 serde_json::from_slice(&data).context("failed to decode encrypted credential file")?;
177 encryption::decrypt(&encrypted).map(Some)
178 }
179
180 fn clear_file(&self) -> Result<()> {
181 let path = self.file_path()?;
182 match fs::remove_file(path) {
183 Ok(()) => Ok(()),
184 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
185 Err(err) => Err(anyhow!("failed to delete encrypted credential file: {err}")),
186 }
187 }
188
189 fn file_path(&self) -> Result<std::path::PathBuf> {
190 use sha2::Digest as _;
191
192 let mut hasher = sha2::Sha256::new();
193 hasher.update(self.service.as_bytes());
194 hasher.update([0]);
195 hasher.update(self.user.as_bytes());
196 let digest = hasher.finalize();
197 let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest);
198
199 Ok(auth_storage_dir()?.join(format!("credential_{encoded}.json")))
200 }
201}