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, read_private_file, write_private_file};
11
12pub struct CredentialStorage {
17 service: String,
18 user: String,
19}
20
21impl CredentialStorage {
22 pub(crate) fn new(service: impl Into<String>, user: impl Into<String>) -> Self {
24 Self { service: service.into(), user: user.into() }
25 }
26
27 pub(crate) fn store_with_mode(&self, value: &str, mode: AuthCredentialsStoreMode) -> Result<()> {
29 match mode.effective_mode() {
30 AuthCredentialsStoreMode::Keyring => match self.store_keyring(value) {
31 Ok(()) => {
32 if let Err(err) = self.store_file(value) {
33 tracing::warn!(
34 "Failed to write encrypted file backup for {}/{}: {}",
35 self.service,
36 self.user,
37 err
38 );
39 }
40 Ok(())
41 }
42 Err(err) => {
43 tracing::warn!(
44 "Failed to store credential in OS keyring for {}/{}; falling back to encrypted file storage: {}",
45 self.service,
46 self.user,
47 err
48 );
49 self.store_file(value).context("failed to store credential in encrypted file")
50 }
51 },
52 AuthCredentialsStoreMode::File => self.store_file(value),
53 AuthCredentialsStoreMode::Auto => unreachable!("effective_mode() resolves Auto"),
54 }
55 }
56
57 pub(crate) fn store_exact_with_mode(&self, value: &str, mode: AuthCredentialsStoreMode) -> Result<()> {
64 match mode.effective_mode() {
65 AuthCredentialsStoreMode::Keyring => self.store_keyring(value),
66 AuthCredentialsStoreMode::File => self.store_file(value),
67 AuthCredentialsStoreMode::Auto => unreachable!("effective_mode() resolves Auto"),
68 }
69 }
70
71 pub(crate) fn store_json<T: serde::Serialize>(&self, value: &T, mode: AuthCredentialsStoreMode) -> Result<()> {
73 let serialized = serde_json::to_string(value).context("failed to serialize credential")?;
74 self.store_with_mode(&serialized, mode)
75 }
76
77 pub(crate) fn store_json_exact_with_mode<T: serde::Serialize>(
79 &self,
80 value: &T,
81 mode: AuthCredentialsStoreMode,
82 ) -> Result<()> {
83 let serialized = serde_json::to_string(value).context("failed to serialize credential")?;
84 self.store_exact_with_mode(&serialized, mode)
85 }
86
87 pub fn store(&self, value: &str) -> Result<()> {
89 self.store_with_mode(value, AuthCredentialsStoreMode::Auto)
90 }
91
92 pub(crate) fn load_with_mode(&self, mode: AuthCredentialsStoreMode) -> Result<Option<String>> {
94 match mode.effective_mode() {
95 AuthCredentialsStoreMode::Keyring => match self.load_keyring() {
96 Ok(Some(value)) => Ok(Some(value)),
97 Ok(None) => self.load_file(),
98 Err(err) => {
99 tracing::warn!(
100 "Failed to read credential from OS keyring for {}/{}; falling back to encrypted file storage: {}",
101 self.service,
102 self.user,
103 err
104 );
105 self.load_file()
106 }
107 },
108 AuthCredentialsStoreMode::File => self.load_file(),
109 AuthCredentialsStoreMode::Auto => unreachable!("effective_mode() resolves Auto"),
110 }
111 }
112
113 pub(crate) fn load_exact_with_mode(&self, mode: AuthCredentialsStoreMode) -> Result<Option<String>> {
118 match mode.effective_mode() {
119 AuthCredentialsStoreMode::Keyring => self.load_keyring(),
120 AuthCredentialsStoreMode::File => self.load_file(),
121 AuthCredentialsStoreMode::Auto => unreachable!("effective_mode() resolves Auto"),
122 }
123 }
124
125 pub(crate) fn load_json<T: serde::de::DeserializeOwned>(
127 &self,
128 mode: AuthCredentialsStoreMode,
129 ) -> Result<Option<T>> {
130 let Some(serialized) = self.load_with_mode(mode)? else {
131 return Ok(None);
132 };
133 serde_json::from_str(&serialized)
134 .context("failed to deserialize credential")
135 .map(Some)
136 }
137
138 pub(crate) fn load_json_exact_with_mode<T: serde::de::DeserializeOwned>(
140 &self,
141 mode: AuthCredentialsStoreMode,
142 ) -> Result<Option<T>> {
143 let Some(serialized) = self.load_exact_with_mode(mode)? else {
144 return Ok(None);
145 };
146 serde_json::from_str(&serialized)
147 .context("failed to deserialize credential")
148 .map(Some)
149 }
150
151 pub fn load(&self) -> Result<Option<String>> {
153 self.load_with_mode(AuthCredentialsStoreMode::Auto)
154 }
155
156 pub(crate) fn clear_with_mode(&self, mode: AuthCredentialsStoreMode) -> Result<()> {
158 match mode.effective_mode() {
159 AuthCredentialsStoreMode::Keyring => {
160 let mut errors = Vec::new();
161
162 if let Err(err) = self.clear_keyring() {
163 errors.push(err.to_string());
164 }
165 if let Err(err) = self.clear_file() {
166 errors.push(err.to_string());
167 }
168
169 if errors.is_empty() {
170 Ok(())
171 } else {
172 Err(anyhow!("Failed to clear credential from secure storage: {}", errors.join("; ")))
173 }
174 }
175 AuthCredentialsStoreMode::File => self.clear_file(),
176 AuthCredentialsStoreMode::Auto => unreachable!("effective_mode() resolves Auto"),
177 }
178 }
179
180 pub(crate) fn clear_exact_with_mode(&self, mode: AuthCredentialsStoreMode) -> Result<()> {
187 match mode.effective_mode() {
188 AuthCredentialsStoreMode::Keyring => self.clear_keyring(),
189 AuthCredentialsStoreMode::File => self.clear_file(),
190 AuthCredentialsStoreMode::Auto => unreachable!("effective_mode() resolves Auto"),
191 }
192 }
193
194 pub fn clear(&self) -> Result<()> {
196 self.clear_with_mode(AuthCredentialsStoreMode::Auto)
197 }
198
199 fn store_keyring(&self, value: &str) -> Result<()> {
204 let entry = keyring::entry(&self.service, &self.user).context("Failed to access OS keyring")?;
205 entry.set_password(value).context("Failed to store credential in OS keyring")?;
206 tracing::debug!("Credential stored in OS keyring for {}/{}", self.service, self.user);
207 Ok(())
208 }
209
210 fn load_keyring(&self) -> Result<Option<String>> {
211 let entry = match keyring::entry(&self.service, &self.user) {
212 Ok(e) => e,
213 Err(_) => return Ok(None),
214 };
215
216 match entry.get_password() {
217 Ok(value) => Ok(Some(value)),
218 Err(keyring_core::Error::NoEntry) => Ok(None),
219 Err(e) => Err(anyhow!("Failed to read from keyring: {e}")),
220 }
221 }
222
223 fn clear_keyring(&self) -> Result<()> {
224 let entry = match keyring::entry(&self.service, &self.user) {
225 Ok(e) => e,
226 Err(_) => return Ok(()),
227 };
228
229 match entry.delete_credential() {
230 Ok(_) => {
231 tracing::debug!("Credential cleared from keyring for {}/{}", self.service, self.user);
232 }
233 Err(keyring_core::Error::NoEntry) => {}
234 Err(e) => return Err(anyhow!("Failed to clear keyring entry: {e}")),
235 }
236
237 Ok(())
238 }
239
240 fn store_file(&self, value: &str) -> Result<()> {
241 let path = self.file_path()?;
242 let encrypted = encryption::encrypt(value)?;
243 let payload = serde_json::to_vec_pretty(&encrypted).context("failed to serialize encrypted credential")?;
244 write_private_file(&path, &payload).context("failed to write encrypted credential file")?;
245 Ok(())
246 }
247
248 fn load_file(&self) -> Result<Option<String>> {
249 let path = self.file_path()?;
250 let Some(data) = read_private_file(&path).context("failed to read encrypted credential file")? else {
251 return Ok(None);
252 };
253
254 let encrypted: encryption::EncryptedCredential =
255 serde_json::from_slice(&data).context("failed to decode encrypted credential file")?;
256 encryption::decrypt(&encrypted).map(Some)
257 }
258
259 fn clear_file(&self) -> Result<()> {
260 let path = self.file_path()?;
261 match fs::remove_file(path) {
262 Ok(()) => Ok(()),
263 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
264 Err(err) => Err(anyhow!("failed to delete encrypted credential file: {err}")),
265 }
266 }
267
268 fn file_path(&self) -> Result<std::path::PathBuf> {
269 use sha2::Digest as _;
270
271 let mut hasher = sha2::Sha256::new();
272 hasher.update(self.service.as_bytes());
273 hasher.update([0]);
274 hasher.update(self.user.as_bytes());
275 let digest = hasher.finalize();
276 let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest);
277
278 Ok(auth_storage_dir()?.join(format!("credential_{encoded}.json")))
279 }
280
281 #[cfg(test)]
282 pub(crate) fn file_path_for_tests(&self) -> Result<std::path::PathBuf> {
283 self.file_path()
284 }
285}