1use std::collections::HashMap;
2use std::future::Future;
3use std::path::PathBuf;
4use std::pin::Pin;
5use std::sync::{Arc, LazyLock};
6
7use anyhow::Result;
8use futures::FutureExt as _;
9use gpui::{App, AsyncApp};
10use release_channel::ReleaseChannel;
11
12/// An environment variable whose presence indicates that the system keychain
13/// should be used in development.
14///
15/// By default, running Zed in development uses the development credentials
16/// provider. Setting this environment variable allows you to interact with the
17/// system keychain (for instance, if you need to test something).
18///
19/// Only works in development. Setting this environment variable in other
20/// release channels is a no-op.
21static ZED_DEVELOPMENT_USE_KEYCHAIN: LazyLock<bool> = LazyLock::new(|| {
22 std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").is_ok_and(|value| !value.is_empty())
23});
24
25/// A provider for credentials.
26///
27/// Used to abstract over reading and writing credentials to some form of
28/// persistence (like the system keychain).
29pub trait CredentialsProvider: Send + Sync {
30 /// Reads the credentials from the provider.
31 fn read_credentials<'a>(
32 &'a self,
33 url: &'a str,
34 cx: &'a AsyncApp,
35 ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>>;
36
37 /// Writes the credentials to the provider.
38 fn write_credentials<'a>(
39 &'a self,
40 url: &'a str,
41 username: &'a str,
42 password: &'a [u8],
43 cx: &'a AsyncApp,
44 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
45
46 /// Deletes the credentials from the provider.
47 fn delete_credentials<'a>(
48 &'a self,
49 url: &'a str,
50 cx: &'a AsyncApp,
51 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
52}
53
54impl dyn CredentialsProvider {
55 /// Returns the global [`CredentialsProvider`].
56 pub fn global(cx: &App) -> Arc<Self> {
57 // The `CredentialsProvider` trait has `Send + Sync` bounds on it, so it
58 // seems like this is a false positive from Clippy.
59 #[allow(clippy::arc_with_non_send_sync)]
60 Self::new(cx)
61 }
62
63 fn new(cx: &App) -> Arc<Self> {
64 let use_development_provider = match ReleaseChannel::try_global(cx) {
65 Some(ReleaseChannel::Dev) => {
66 // In development we default to using the development
67 // credentials provider to avoid getting spammed by relentless
68 // keychain access prompts.
69 //
70 // However, if the `ZED_DEVELOPMENT_USE_KEYCHAIN` environment
71 // variable is set, we will use the actual keychain.
72 !*ZED_DEVELOPMENT_USE_KEYCHAIN
73 }
74 Some(ReleaseChannel::Nightly | ReleaseChannel::Preview | ReleaseChannel::Stable)
75 | None => false,
76 };
77
78 if use_development_provider {
79 Arc::new(DevelopmentCredentialsProvider::new())
80 } else {
81 Arc::new(KeychainCredentialsProvider)
82 }
83 }
84}
85
86/// A credentials provider that stores credentials in the system keychain.
87struct KeychainCredentialsProvider;
88
89impl CredentialsProvider for KeychainCredentialsProvider {
90 fn read_credentials<'a>(
91 &'a self,
92 url: &'a str,
93 cx: &'a AsyncApp,
94 ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
95 async move { cx.update(|cx| cx.read_credentials(url))?.await }.boxed_local()
96 }
97
98 fn write_credentials<'a>(
99 &'a self,
100 url: &'a str,
101 username: &'a str,
102 password: &'a [u8],
103 cx: &'a AsyncApp,
104 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
105 async move {
106 cx.update(move |cx| cx.write_credentials(url, username, password))?
107 .await
108 }
109 .boxed_local()
110 }
111
112 fn delete_credentials<'a>(
113 &'a self,
114 url: &'a str,
115 cx: &'a AsyncApp,
116 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
117 async move { cx.update(move |cx| cx.delete_credentials(url))?.await }.boxed_local()
118 }
119}
120
121/// A credentials provider that stores credentials in a local file.
122///
123/// This MUST only be used in development, as this is not a secure way of storing
124/// credentials on user machines.
125///
126/// Its existence is purely to work around the annoyance of having to constantly
127/// re-allow access to the system keychain when developing Zed.
128struct DevelopmentCredentialsProvider {
129 path: PathBuf,
130}
131
132impl DevelopmentCredentialsProvider {
133 fn new() -> Self {
134 let path = paths::config_dir().join("development_credentials");
135
136 Self { path }
137 }
138
139 fn load_credentials(&self) -> Result<HashMap<String, (String, Vec<u8>)>> {
140 let json = std::fs::read(&self.path)?;
141 let credentials: HashMap<String, (String, Vec<u8>)> = serde_json::from_slice(&json)?;
142
143 Ok(credentials)
144 }
145
146 fn save_credentials(&self, credentials: &HashMap<String, (String, Vec<u8>)>) -> Result<()> {
147 let json = serde_json::to_string(credentials)?;
148 std::fs::write(&self.path, json)?;
149
150 Ok(())
151 }
152}
153
154impl CredentialsProvider for DevelopmentCredentialsProvider {
155 fn read_credentials<'a>(
156 &'a self,
157 url: &'a str,
158 _cx: &'a AsyncApp,
159 ) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
160 async move {
161 Ok(self
162 .load_credentials()
163 .unwrap_or_default()
164 .get(url)
165 .cloned())
166 }
167 .boxed_local()
168 }
169
170 fn write_credentials<'a>(
171 &'a self,
172 url: &'a str,
173 username: &'a str,
174 password: &'a [u8],
175 _cx: &'a AsyncApp,
176 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
177 async move {
178 let mut credentials = self.load_credentials().unwrap_or_default();
179 credentials.insert(url.to_string(), (username.to_string(), password.to_vec()));
180
181 self.save_credentials(&credentials)
182 }
183 .boxed_local()
184 }
185
186 fn delete_credentials<'a>(
187 &'a self,
188 url: &'a str,
189 _cx: &'a AsyncApp,
190 ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
191 async move {
192 let mut credentials = self.load_credentials()?;
193 credentials.remove(url);
194
195 self.save_credentials(&credentials)
196 }
197 .boxed_local()
198 }
199}