Detailed changes
@@ -813,11 +813,13 @@ dependencies = [
"futures 0.3.31",
"gpui",
"net",
- "parking_lot",
+ "proto",
"smol",
"tempfile",
"util",
+ "windows 0.61.1",
"workspace-hack",
+ "zeroize",
]
[[package]]
@@ -6946,6 +6948,7 @@ dependencies = [
"workspace",
"workspace-hack",
"zed_actions",
+ "zeroize",
"zlog",
]
@@ -13141,6 +13144,7 @@ dependencies = [
"which 6.0.3",
"workspace-hack",
"worktree",
+ "zeroize",
"zlog",
]
@@ -13839,6 +13843,7 @@ name = "recent_projects"
version = "0.1.0"
dependencies = [
"anyhow",
+ "askpass",
"auto_update",
"dap",
"editor",
@@ -717,6 +717,7 @@ windows-core = "0.61"
wit-component = "0.221"
workspace-hack = "0.1.0"
yawc = "0.2.5"
+zeroize = "1.8"
zstd = "0.11"
[workspace.dependencies.windows]
@@ -743,6 +744,7 @@ features = [
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
+ "Win32_Security_Cryptography",
"Win32_Storage_FileSystem",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
@@ -16,8 +16,12 @@ anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
net.workspace = true
-parking_lot.workspace = true
+proto.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true
workspace-hack.workspace = true
+zeroize.workspace = true
+
+[target.'cfg(target_os = "windows")'.dependencies]
+windows.workspace = true
@@ -1,3 +1,9 @@
+mod encrypted_password;
+
+pub use encrypted_password::{EncryptedPassword, ProcessExt};
+
+#[cfg(target_os = "windows")]
+use std::sync::OnceLock;
use std::{ffi::OsStr, time::Duration};
use anyhow::{Context as _, Result};
@@ -10,6 +16,8 @@ use gpui::{AsyncApp, BackgroundExecutor, Task};
use smol::fs;
use util::ResultExt as _;
+use crate::encrypted_password::decrypt;
+
#[derive(PartialEq, Eq)]
pub enum AskPassResult {
CancelledByUser,
@@ -17,16 +25,19 @@ pub enum AskPassResult {
}
pub struct AskPassDelegate {
- tx: mpsc::UnboundedSender<(String, oneshot::Sender<String>)>,
+ tx: mpsc::UnboundedSender<(String, oneshot::Sender<EncryptedPassword>)>,
_task: Task<()>,
}
impl AskPassDelegate {
pub fn new(
cx: &mut AsyncApp,
- password_prompt: impl Fn(String, oneshot::Sender<String>, &mut AsyncApp) + Send + Sync + 'static,
+ password_prompt: impl Fn(String, oneshot::Sender<EncryptedPassword>, &mut AsyncApp)
+ + Send
+ + Sync
+ + 'static,
) -> Self {
- let (tx, mut rx) = mpsc::unbounded::<(String, oneshot::Sender<String>)>();
+ let (tx, mut rx) = mpsc::unbounded::<(String, oneshot::Sender<_>)>();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
while let Some((prompt, channel)) = rx.next().await {
password_prompt(prompt, channel, cx);
@@ -35,7 +46,7 @@ impl AskPassDelegate {
Self { tx, _task: task }
}
- pub async fn ask_password(&mut self, prompt: String) -> Result<String> {
+ pub async fn ask_password(&mut self, prompt: String) -> Result<EncryptedPassword> {
let (tx, rx) = oneshot::channel();
self.tx.send((prompt, tx)).await?;
Ok(rx.await?)
@@ -48,7 +59,7 @@ pub struct AskPassSession {
#[cfg(target_os = "windows")]
askpass_helper: String,
#[cfg(target_os = "windows")]
- secret: std::sync::Arc<parking_lot::Mutex<String>>,
+ secret: std::sync::Arc<OnceLock<EncryptedPassword>>,
_askpass_task: Task<()>,
askpass_opened_rx: Option<oneshot::Receiver<()>>,
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
@@ -68,7 +79,7 @@ impl AskPassSession {
use util::fs::make_file_executable;
#[cfg(target_os = "windows")]
- let secret = std::sync::Arc::new(parking_lot::Mutex::new(String::new()));
+ let secret = std::sync::Arc::new(OnceLock::new());
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
@@ -104,10 +115,12 @@ impl AskPassSession {
.context("getting askpass password")
.log_err()
{
- stream.write_all(password.as_bytes()).await.log_err();
#[cfg(target_os = "windows")]
{
- *askpass_secret.lock() = password;
+ askpass_secret.get_or_init(|| password.clone());
+ }
+ if let Ok(decrypted) = decrypt(password) {
+ stream.write_all(decrypted.as_bytes()).await.log_err();
}
} else {
if let Some(kill_tx) = kill_tx.take() {
@@ -188,8 +201,8 @@ impl AskPassSession {
/// This will return the password that was last set by the askpass script.
#[cfg(target_os = "windows")]
- pub fn get_password(&self) -> String {
- self.secret.lock().clone()
+ pub fn get_password(&self) -> Option<EncryptedPassword> {
+ self.secret.get().cloned()
}
}
@@ -0,0 +1,111 @@
+//! This module provides [EncryptedPassword] for storage of passwords in memory.
+//! On Windows that's implemented with CryptProtectMemory/CryptUnprotectMemory; on other platforms it just falls through
+//! to string for now.
+//!
+//! The "safety" of this module lies in exploiting visibility rules of Rust:
+//! 1. No outside module has access to the internal representation of [EncryptedPassword].
+//! 2. [EncryptedPassword] cannot be converted into a [String] or any other plaintext representation.
+//! All use cases that do need such functionality (of which we have two right now) are implemented within this module.
+//!
+//! Note that this is not bulletproof.
+//! 1. [ProcessExt] is implemented for [smol::process::Command], which is a builder for smol processes.
+//! Before the process itself is spawned the contents of [EncryptedPassword] are unencrypted in env var storage of said builder.
+//! 2. We're also sending plaintext passwords over RPC with [proto::AskPassResponse]. Go figure how great that is.
+//!
+//! Still, the goal of this module is to not have passwords laying around nilly-willy in memory.
+//! We do not claim that it is fool-proof.
+use anyhow::Result;
+use zeroize::Zeroize;
+
+type LengthWithoutPadding = u32;
+#[derive(Clone)]
+pub struct EncryptedPassword(Vec<u8>, LengthWithoutPadding);
+
+pub trait ProcessExt {
+ fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self;
+}
+
+impl ProcessExt for smol::process::Command {
+ fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self {
+ if let Ok(password) = decrypt(value) {
+ self.env(name, password);
+ }
+ self
+ }
+}
+
+impl TryFrom<EncryptedPassword> for proto::AskPassResponse {
+ type Error = anyhow::Error;
+ fn try_from(pw: EncryptedPassword) -> Result<Self, Self::Error> {
+ let pw = decrypt(pw)?;
+ Ok(Self { response: pw })
+ }
+}
+
+impl Drop for EncryptedPassword {
+ fn drop(&mut self) {
+ self.0.zeroize();
+ self.1.zeroize();
+ }
+}
+
+impl TryFrom<&str> for EncryptedPassword {
+ type Error = anyhow::Error;
+ fn try_from(password: &str) -> Result<EncryptedPassword> {
+ let len: u32 = password.len().try_into()?;
+ #[cfg(windows)]
+ {
+ use windows::Win32::Security::Cryptography::{
+ CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, CryptProtectMemory,
+ };
+ let mut value = password.bytes().collect::<Vec<_>>();
+ let padded_length = len.next_multiple_of(CRYPTPROTECTMEMORY_BLOCK_SIZE);
+ if padded_length != len {
+ value.resize(padded_length as usize, 0);
+ }
+ unsafe {
+ CryptProtectMemory(
+ value.as_mut_ptr() as _,
+ len,
+ CRYPTPROTECTMEMORY_SAME_PROCESS,
+ )?;
+ }
+ Ok(Self(value, len))
+ }
+ #[cfg(not(windows))]
+ Ok(Self(String::from(password).into(), len))
+ }
+}
+
+pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
+ #[cfg(windows)]
+ {
+ use anyhow::Context;
+ use windows::Win32::Security::Cryptography::{
+ CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, CryptUnprotectMemory,
+ };
+ assert_eq!(
+ password.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
+ 0,
+ "Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
+ password.0.len(),
+ CRYPTPROTECTMEMORY_BLOCK_SIZE
+ );
+ unsafe {
+ CryptUnprotectMemory(
+ password.0.as_mut_ptr() as _,
+ password.1,
+ CRYPTPROTECTMEMORY_SAME_PROCESS,
+ )
+ .context("while decrypting a SSH password")?
+ };
+
+ {
+ // Remove padding
+ _ = password.0.drain(password.1 as usize..);
+ }
+ Ok(String::from_utf8(std::mem::take(&mut password.0))?)
+ }
+ #[cfg(not(windows))]
+ Ok(String::from_utf8(std::mem::take(&mut password.0))?)
+}
@@ -61,6 +61,7 @@ watch.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
+zeroize.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
@@ -1,3 +1,4 @@
+use askpass::EncryptedPassword;
use editor::Editor;
use futures::channel::oneshot;
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Styled};
@@ -7,13 +8,15 @@ use ui::{
LabelSize, ParentElement, Render, SharedString, StyledExt, StyledTypography, Window, div,
h_flex, v_flex,
};
+use util::maybe;
use workspace::ModalView;
+use zeroize::Zeroize;
pub(crate) struct AskPassModal {
operation: SharedString,
prompt: SharedString,
editor: Entity<Editor>,
- tx: Option<oneshot::Sender<String>>,
+ tx: Option<oneshot::Sender<EncryptedPassword>>,
}
impl EventEmitter<DismissEvent> for AskPassModal {}
@@ -28,7 +31,7 @@ impl AskPassModal {
pub fn new(
operation: SharedString,
prompt: SharedString,
- tx: oneshot::Sender<String>,
+ tx: oneshot::Sender<EncryptedPassword>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -53,10 +56,20 @@ impl AskPassModal {
cx.emit(DismissEvent);
}
- fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
- if let Some(tx) = self.tx.take() {
- tx.send(self.editor.read(cx).text(cx)).ok();
- }
+ fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
+ maybe!({
+ let tx = self.tx.take()?;
+ let mut text = self.editor.update(cx, |this, cx| {
+ let text = this.text(cx);
+ this.clear(window, cx);
+ text
+ });
+ let pw = askpass::EncryptedPassword::try_from(text.as_ref()).ok()?;
+ text.zeroize();
+ tx.send(pw).ok();
+ Some(())
+ });
+
cx.emit(DismissEvent);
}
@@ -89,6 +89,7 @@ util.workspace = true
watch.workspace = true
which.workspace = true
worktree.workspace = true
+zeroize.workspace = true
zlog.workspace = true
workspace-hack.workspace = true
@@ -7,7 +7,7 @@ use crate::{
worktree_store::{WorktreeStore, WorktreeStoreEvent},
};
use anyhow::{Context as _, Result, anyhow, bail};
-use askpass::AskPassDelegate;
+use askpass::{AskPassDelegate, EncryptedPassword};
use buffer_diff::{BufferDiff, BufferDiffEvent};
use client::ProjectId;
use collections::HashMap;
@@ -68,6 +68,7 @@ use worktree::{
File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId,
UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree,
};
+use zeroize::Zeroize;
pub struct GitStore {
state: GitStoreState,
@@ -2106,7 +2107,7 @@ impl GitStore {
.lock()
.insert(envelope.payload.askpass_id, askpass);
- Ok(proto::AskPassResponse { response })
+ response.try_into()
}
async fn handle_check_for_pushed_commits(
@@ -2740,7 +2741,10 @@ fn make_remote_delegate(
prompt,
});
cx.spawn(async move |_, _| {
- tx.send(response.await?.response).ok();
+ let mut response = response.await?.response;
+ tx.send(EncryptedPassword::try_from(response.as_ref())?)
+ .ok();
+ response.zeroize();
anyhow::Ok(())
})
.detach_and_log_err(cx);
@@ -14,6 +14,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
+askpass.workspace = true
auto_update.workspace = true
editor.workspace = true
extension_host.workspace = true
@@ -1,6 +1,7 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{Context as _, Result};
+use askpass::EncryptedPassword;
use auto_update::AutoUpdater;
use editor::Editor;
use extension_host::ExtensionStore;
@@ -118,7 +119,7 @@ pub struct RemoteConnectionPrompt {
nickname: Option<SharedString>,
is_wsl: bool,
status_message: Option<SharedString>,
- prompt: Option<(Entity<Markdown>, oneshot::Sender<String>)>,
+ prompt: Option<(Entity<Markdown>, oneshot::Sender<EncryptedPassword>)>,
cancellation: Option<oneshot::Sender<()>>,
editor: Entity<Editor>,
}
@@ -160,10 +161,10 @@ impl RemoteConnectionPrompt {
self.cancellation = Some(tx);
}
- pub fn set_prompt(
+ fn set_prompt(
&mut self,
prompt: String,
- tx: oneshot::Sender<String>,
+ tx: oneshot::Sender<EncryptedPassword>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -203,8 +204,12 @@ impl RemoteConnectionPrompt {
pub fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some((_, tx)) = self.prompt.take() {
self.status_message = Some("Connecting".into());
+
self.editor.update(cx, |editor, cx| {
- tx.send(editor.text(cx)).ok();
+ let pw = editor.text(cx);
+ if let Ok(secure) = EncryptedPassword::try_from(pw.as_ref()) {
+ tx.send(secure).ok();
+ }
editor.clear(window, cx);
});
}
@@ -438,11 +443,16 @@ impl ModalView for RemoteConnectionModal {
pub struct RemoteClientDelegate {
window: AnyWindowHandle,
ui: WeakEntity<RemoteConnectionPrompt>,
- known_password: Option<String>,
+ known_password: Option<EncryptedPassword>,
}
impl remote::RemoteClientDelegate for RemoteClientDelegate {
- fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp) {
+ fn ask_password(
+ &self,
+ prompt: String,
+ tx: oneshot::Sender<EncryptedPassword>,
+ cx: &mut AsyncApp,
+ ) {
let mut known_password = self.known_password.clone();
if let Some(password) = known_password.take() {
tx.send(password).ok();
@@ -531,7 +541,10 @@ pub fn connect_over_ssh(
cx: &mut App,
) -> Task<Result<Option<Entity<RemoteClient>>>> {
let window = window.window_handle();
- let known_password = connection_options.password.clone();
+ let known_password = connection_options
+ .password
+ .as_deref()
+ .and_then(|pw| EncryptedPassword::try_from(pw).ok());
let (tx, rx) = oneshot::channel();
ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx));
@@ -557,9 +570,10 @@ pub fn connect(
) -> Task<Result<Option<Entity<RemoteClient>>>> {
let window = window.window_handle();
let known_password = match &connection_options {
- RemoteConnectionOptions::Ssh(ssh_connection_options) => {
- ssh_connection_options.password.clone()
- }
+ RemoteConnectionOptions::Ssh(ssh_connection_options) => ssh_connection_options
+ .password
+ .as_deref()
+ .and_then(|pw| pw.try_into().ok()),
_ => None,
};
let (tx, rx) = oneshot::channel();
@@ -644,7 +658,10 @@ pub async fn open_remote_project(
known_password: if let RemoteConnectionOptions::Ssh(options) =
&connection_options
{
- options.password.clone()
+ options
+ .password
+ .as_deref()
+ .and_then(|pw| EncryptedPassword::try_from(pw).ok())
} else {
None
},
@@ -44,6 +44,7 @@ util.workspace = true
which.workspace = true
workspace-hack.workspace = true
+
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
@@ -8,6 +8,7 @@ use crate::{
},
};
use anyhow::{Context as _, Result, anyhow};
+use askpass::EncryptedPassword;
use async_trait::async_trait;
use collections::HashMap;
use futures::{
@@ -60,7 +61,12 @@ pub struct CommandTemplate {
}
pub trait RemoteClientDelegate: Send + Sync {
- fn ask_password(&self, prompt: String, tx: oneshot::Sender<String>, cx: &mut AsyncApp);
+ fn ask_password(
+ &self,
+ prompt: String,
+ tx: oneshot::Sender<EncryptedPassword>,
+ cx: &mut AsyncApp,
+ );
fn get_download_params(
&self,
platform: RemotePlatform,
@@ -1373,6 +1379,7 @@ mod fake {
use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform};
use crate::remote_client::{CommandTemplate, RemoteConnectionOptions};
use anyhow::Result;
+ use askpass::EncryptedPassword;
use async_trait::async_trait;
use collections::HashMap;
use futures::{
@@ -1517,7 +1524,7 @@ mod fake {
pub(super) struct Delegate;
impl RemoteClientDelegate for Delegate {
- fn ask_password(&self, _: String, _: oneshot::Sender<String>, _: &mut AsyncApp) {
+ fn ask_password(&self, _: String, _: oneshot::Sender<EncryptedPassword>, _: &mut AsyncApp) {
unreachable!()
}
@@ -74,6 +74,8 @@ struct SshSocket {
#[cfg(not(target_os = "windows"))]
socket_path: PathBuf,
envs: HashMap<String, String>,
+ #[cfg(target_os = "windows")]
+ password: askpass::EncryptedPassword,
}
macro_rules! shell_script {
@@ -347,7 +349,13 @@ impl SshRemoteConnection {
#[cfg(not(target_os = "windows"))]
let socket = SshSocket::new(connection_options, socket_path)?;
#[cfg(target_os = "windows")]
- let socket = SshSocket::new(connection_options, &temp_dir, askpass.get_password())?;
+ let socket = SshSocket::new(
+ connection_options,
+ &temp_dir,
+ askpass
+ .get_password()
+ .context("Failed to fetch askpass password")?,
+ )?;
drop(askpass);
let ssh_platform = socket.platform().await?;
@@ -674,16 +682,21 @@ impl SshSocket {
}
#[cfg(target_os = "windows")]
- fn new(options: SshConnectionOptions, temp_dir: &TempDir, secret: String) -> Result<Self> {
+ fn new(
+ options: SshConnectionOptions,
+ temp_dir: &TempDir,
+ password: askpass::EncryptedPassword,
+ ) -> Result<Self> {
let askpass_script = temp_dir.path().join("askpass.bat");
std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?;
let mut envs = HashMap::default();
envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into());
envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string());
- envs.insert("ZED_SSH_ASKPASS".into(), secret);
+
Ok(Self {
connection_options: options,
envs,
+ password,
})
}
@@ -737,12 +750,14 @@ impl SshSocket {
#[cfg(target_os = "windows")]
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
+ use askpass::ProcessExt;
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.args(self.connection_options.additional_args())
.envs(self.envs.clone())
+ .encrypted_env("ZED_SSH_ASKPASS", self.password.clone())
}
// On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh.