logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
29
crates/askpass/Cargo.toml
Normal file
29
crates/askpass/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "askpass"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/askpass.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
net.workspace = true
|
||||
smol.workspace = true
|
||||
log.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
zeroize.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["log"]
|
||||
1
crates/askpass/LICENSE-GPL
Symbolic link
1
crates/askpass/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
392
crates/askpass/src/askpass.rs
Normal file
392
crates/askpass/src/askpass.rs
Normal file
@@ -0,0 +1,392 @@
|
||||
mod encrypted_password;
|
||||
|
||||
pub use encrypted_password::{EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
|
||||
|
||||
use net::async_net::UnixListener;
|
||||
use smol::lock::Mutex;
|
||||
use util::fs::make_file_executable;
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::ops::ControlFlow;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::{
|
||||
AsyncBufReadExt as _, AsyncWriteExt as _, FutureExt as _, SinkExt, StreamExt, io::BufReader,
|
||||
select_biased,
|
||||
};
|
||||
use gpui::{AsyncApp, BackgroundExecutor, Task};
|
||||
use smol::fs;
|
||||
use util::{ResultExt as _, debug_panic, maybe, paths::PathExt, shell::ShellKind};
|
||||
|
||||
/// Path to the program used for askpass
|
||||
///
|
||||
/// On Unix and remote servers, this defaults to the current executable
|
||||
/// On Windows, this is set to the CLI variant of zed
|
||||
static ASKPASS_PROGRAM: OnceLock<std::path::PathBuf> = OnceLock::new();
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum AskPassResult {
|
||||
CancelledByUser,
|
||||
Timedout,
|
||||
}
|
||||
|
||||
pub struct AskPassDelegate {
|
||||
tx: mpsc::UnboundedSender<(String, oneshot::Sender<EncryptedPassword>)>,
|
||||
executor: BackgroundExecutor,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
impl AskPassDelegate {
|
||||
pub fn new(
|
||||
cx: &mut AsyncApp,
|
||||
password_prompt: impl Fn(String, oneshot::Sender<EncryptedPassword>, &mut AsyncApp)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
) -> Self {
|
||||
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);
|
||||
}
|
||||
});
|
||||
Self {
|
||||
tx,
|
||||
_task: task,
|
||||
executor: cx.background_executor().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ask_password(&mut self, prompt: String) -> Task<Option<EncryptedPassword>> {
|
||||
let mut this_tx = self.tx.clone();
|
||||
self.executor.spawn(async move {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
this_tx.send((prompt, tx)).await.ok()?;
|
||||
rx.await.ok()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AskPassSession {
|
||||
#[cfg(target_os = "windows")]
|
||||
secret: std::sync::Arc<std::sync::Mutex<Option<EncryptedPassword>>>,
|
||||
askpass_task: PasswordProxy,
|
||||
askpass_opened_rx: Option<oneshot::Receiver<()>>,
|
||||
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
const ASKPASS_SCRIPT_NAME: &str = if cfg!(target_os = "windows") {
|
||||
"askpass.ps1"
|
||||
} else {
|
||||
"askpass.sh"
|
||||
};
|
||||
|
||||
impl AskPassSession {
|
||||
/// This will create a new AskPassSession.
|
||||
/// You must retain this session until the master process exits.
|
||||
#[must_use]
|
||||
pub async fn new(executor: BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let secret = std::sync::Arc::new(std::sync::Mutex::new(None));
|
||||
|
||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||
|
||||
let askpass_opened_tx = Arc::new(Mutex::new(Some(askpass_opened_tx)));
|
||||
|
||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
||||
let kill_tx = Arc::new(Mutex::new(Some(askpass_kill_master_tx)));
|
||||
|
||||
let get_password = {
|
||||
let executor = executor.clone();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let askpass_secret = secret.clone();
|
||||
move |prompt| {
|
||||
let prompt = delegate.ask_password(prompt);
|
||||
let kill_tx = kill_tx.clone();
|
||||
let askpass_opened_tx = askpass_opened_tx.clone();
|
||||
#[cfg(target_os = "windows")]
|
||||
let askpass_secret = askpass_secret.clone();
|
||||
executor.spawn(async move {
|
||||
if let Some(askpass_opened_tx) = askpass_opened_tx.lock().await.take() {
|
||||
askpass_opened_tx.send(()).ok();
|
||||
}
|
||||
if let Some(password) = prompt.await {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
askpass_secret.lock().unwrap().replace(password.clone());
|
||||
}
|
||||
ControlFlow::Continue(Ok(password))
|
||||
} else {
|
||||
if let Some(kill_tx) = kill_tx.lock().await.take() {
|
||||
kill_tx.send(()).ok();
|
||||
}
|
||||
ControlFlow::Break(())
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
let askpass_task = PasswordProxy::new(Box::new(get_password), executor.clone()).await?;
|
||||
|
||||
Ok(Self {
|
||||
#[cfg(target_os = "windows")]
|
||||
secret,
|
||||
|
||||
askpass_task,
|
||||
askpass_kill_master_rx: Some(askpass_kill_master_rx),
|
||||
askpass_opened_rx: Some(askpass_opened_rx),
|
||||
executor,
|
||||
})
|
||||
}
|
||||
|
||||
// This will run the askpass task forever, resolving as many authentication requests as needed.
|
||||
// The caller is responsible for examining the result of their own commands and cancelling this
|
||||
// future when this is no longer needed. Note that this can only be called once, but due to the
|
||||
// drop order this takes an &mut, so you can `drop()` it after you're done with the master process.
|
||||
pub async fn run(&mut self) -> AskPassResult {
|
||||
// This is the default timeout setting used by VSCode.
|
||||
let connection_timeout = Duration::from_secs(17);
|
||||
let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once");
|
||||
let askpass_kill_master_rx = self
|
||||
.askpass_kill_master_rx
|
||||
.take()
|
||||
.expect("Only call run once");
|
||||
let executor = self.executor.clone();
|
||||
|
||||
select_biased! {
|
||||
_ = askpass_opened_rx.fuse() => {
|
||||
// Note: this await can only resolve after we are dropped.
|
||||
askpass_kill_master_rx.await.ok();
|
||||
AskPassResult::CancelledByUser
|
||||
}
|
||||
|
||||
_ = futures::FutureExt::fuse(executor.timer(connection_timeout)) => {
|
||||
AskPassResult::Timedout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This will return the password that was last set by the askpass script.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_password(&self) -> Option<EncryptedPassword> {
|
||||
self.secret.lock().ok()?.clone()
|
||||
}
|
||||
|
||||
pub fn script_path(&self) -> impl AsRef<OsStr> {
|
||||
self.askpass_task.script_path()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PasswordProxy {
|
||||
_task: Task<()>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
askpass_script_path: std::path::PathBuf,
|
||||
#[cfg(target_os = "windows")]
|
||||
askpass_helper: String,
|
||||
}
|
||||
|
||||
impl PasswordProxy {
|
||||
pub async fn new(
|
||||
mut get_password: Box<
|
||||
dyn FnMut(String) -> Task<ControlFlow<(), Result<EncryptedPassword>>>
|
||||
+ 'static
|
||||
+ Send
|
||||
+ Sync,
|
||||
>,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Result<Self> {
|
||||
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);
|
||||
let current_exec =
|
||||
std::env::current_exe().context("Failed to determine current zed executable path.")?;
|
||||
|
||||
// TODO: inferred from the use of powershell.exe in askpass_helper_script
|
||||
let shell_kind = if cfg!(windows) {
|
||||
ShellKind::PowerShell
|
||||
} else {
|
||||
ShellKind::Posix
|
||||
};
|
||||
let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec);
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = generate_askpass_script(shell_kind, askpass_program, &askpass_socket)?;
|
||||
let _task = executor.spawn(async move {
|
||||
maybe!(async move {
|
||||
let listener =
|
||||
UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
|
||||
|
||||
while let Ok((mut stream, _)) = listener.accept().await {
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(&mut stream);
|
||||
if reader.read_until(b'\0', &mut buffer).await.is_err() {
|
||||
buffer.clear();
|
||||
}
|
||||
let prompt = String::from_utf8_lossy(&buffer).into_owned();
|
||||
let password = get_password(prompt).await;
|
||||
match password {
|
||||
ControlFlow::Continue(password) => {
|
||||
if let Ok(password) = password
|
||||
&& let Ok(decrypted) =
|
||||
password.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)
|
||||
{
|
||||
stream.write_all(decrypted.as_bytes()).await.log_err();
|
||||
}
|
||||
}
|
||||
ControlFlow::Break(()) => {
|
||||
// note: we expect the caller to drop this task when it's done.
|
||||
// We need to keep the stream open until the caller is done to avoid
|
||||
// spurious errors from ssh.
|
||||
std::future::pending::<()>().await;
|
||||
drop(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(temp_dir);
|
||||
Result::<_, anyhow::Error>::Ok(())
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
});
|
||||
|
||||
fs::write(&askpass_script_path, askpass_script)
|
||||
.await
|
||||
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
|
||||
make_file_executable(&askpass_script_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("marking askpass script executable at {askpass_script_path:?}")
|
||||
})?;
|
||||
// todo(shell): There might be no powershell on the system
|
||||
#[cfg(target_os = "windows")]
|
||||
let askpass_helper = format!(
|
||||
"powershell.exe -ExecutionPolicy Bypass -File \"{}\"",
|
||||
askpass_script_path.display()
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
_task,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
askpass_script_path,
|
||||
#[cfg(target_os = "windows")]
|
||||
askpass_helper,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn script_path(&self) -> impl AsRef<OsStr> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
&self.askpass_script_path
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
&self.askpass_helper
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The main function for when Zed is running in netcat mode for use in askpass.
|
||||
/// Called from both the remote server binary and the zed binary in their respective main functions.
|
||||
pub fn main(socket: &str) {
|
||||
use net::UnixStream;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::exit;
|
||||
|
||||
let mut stream = match UnixStream::connect(socket) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
eprintln!("Error connecting to socket {}: {}", socket, err);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
if let Err(err) = io::stdin().read_to_end(&mut buffer) {
|
||||
eprintln!("Error reading from stdin: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
while buffer.last().is_some_and(|&b| b == b'\n' || b == b'\r') {
|
||||
buffer.pop();
|
||||
}
|
||||
if buffer.last() != Some(&b'\0') {
|
||||
buffer.push(b'\0');
|
||||
}
|
||||
|
||||
if let Err(err) = stream.write_all(&buffer) {
|
||||
eprintln!("Error writing to socket: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let mut response = Vec::new();
|
||||
if let Err(err) = stream.read_to_end(&mut response) {
|
||||
eprintln!("Error reading from socket: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if let Err(err) = io::stdout().write_all(&response) {
|
||||
eprintln!("Error writing to stdout: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_askpass_program(path: std::path::PathBuf) {
|
||||
if ASKPASS_PROGRAM.set(path).is_err() {
|
||||
debug_panic!("askpass program has already been set");
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn generate_askpass_script(
|
||||
shell_kind: ShellKind,
|
||||
askpass_program: &std::path::Path,
|
||||
askpass_socket: &std::path::Path,
|
||||
) -> Result<String> {
|
||||
let askpass_program = shell_kind.prepend_command_prefix(
|
||||
askpass_program
|
||||
.to_str()
|
||||
.context("Askpass program is on a non-utf8 path")?,
|
||||
);
|
||||
let askpass_program = shell_kind
|
||||
.try_quote_prefix_aware(&askpass_program)
|
||||
.context("Failed to shell-escape Askpass program path")?;
|
||||
let askpass_socket = askpass_socket
|
||||
.try_shell_safe(shell_kind)
|
||||
.context("Failed to shell-escape Askpass socket path")?;
|
||||
let print_args = "printf '%s\\0' \"$@\"";
|
||||
let shebang = "#!/bin/sh";
|
||||
Ok(format!(
|
||||
"{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn generate_askpass_script(
|
||||
shell_kind: ShellKind,
|
||||
askpass_program: &std::path::Path,
|
||||
askpass_socket: &std::path::Path,
|
||||
) -> Result<String> {
|
||||
let askpass_program = shell_kind.prepend_command_prefix(
|
||||
askpass_program
|
||||
.to_str()
|
||||
.context("Askpass program is on a non-utf8 path")?,
|
||||
);
|
||||
let askpass_program = shell_kind
|
||||
.try_quote_prefix_aware(&askpass_program)
|
||||
.context("Failed to shell-escape Askpass program path")?;
|
||||
let askpass_socket = askpass_socket
|
||||
.try_shell_safe(shell_kind)
|
||||
.context("Failed to shell-escape Askpass socket path")?;
|
||||
Ok(format!(
|
||||
r#"
|
||||
$ErrorActionPreference = 'Stop';
|
||||
($args -join [char]0) | {askpass_program} --askpass={askpass_socket} 2> $null
|
||||
"#,
|
||||
))
|
||||
}
|
||||
102
crates/askpass/src/encrypted_password.rs
Normal file
102
crates/askpass/src/encrypted_password.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
//! 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);
|
||||
|
||||
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);
|
||||
}
|
||||
if len != 0 {
|
||||
unsafe {
|
||||
CryptProtectMemory(
|
||||
value.as_mut_ptr() as _,
|
||||
padded_length,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(Self(value, len))
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
Ok(Self(String::from(password).into(), len))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the docs for [EncryptedPassword]; please take care of not storing the plaintext string in memory for extended
|
||||
/// periods of time.
|
||||
pub struct IKnowWhatIAmDoingAndIHaveReadTheDocs;
|
||||
|
||||
impl EncryptedPassword {
|
||||
pub fn decrypt(mut self, _: IKnowWhatIAmDoingAndIHaveReadTheDocs) -> Result<String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use anyhow::Context;
|
||||
use windows::Win32::Security::Cryptography::{
|
||||
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
CryptUnprotectMemory,
|
||||
};
|
||||
assert_eq!(
|
||||
self.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
|
||||
0,
|
||||
"Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
|
||||
self.0.len(),
|
||||
CRYPTPROTECTMEMORY_BLOCK_SIZE
|
||||
);
|
||||
if self.1 != 0 {
|
||||
unsafe {
|
||||
CryptUnprotectMemory(
|
||||
self.0.as_mut_ptr() as _,
|
||||
self.0.len().try_into()?,
|
||||
CRYPTPROTECTMEMORY_SAME_PROCESS,
|
||||
)
|
||||
.context("while decrypting a SSH password")?
|
||||
};
|
||||
|
||||
{
|
||||
// Remove padding
|
||||
_ = self.0.drain(self.1 as usize..);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user