1//! This module provides [EncryptedPassword] for storage of passwords in memory.
2//! On Windows that's implemented with CryptProtectMemory/CryptUnprotectMemory; on other platforms it just falls through
3//! to string for now.
4//!
5//! The "safety" of this module lies in exploiting visibility rules of Rust:
6//! 1. No outside module has access to the internal representation of [EncryptedPassword].
7//! 2. [EncryptedPassword] cannot be converted into a [String] or any other plaintext representation.
8//! All use cases that do need such functionality (of which we have two right now) are implemented within this module.
9//!
10//! Note that this is not bulletproof.
11//! 1. [ProcessExt] is implemented for [smol::process::Command], which is a builder for smol processes.
12//! Before the process itself is spawned the contents of [EncryptedPassword] are unencrypted in env var storage of said builder.
13//! 2. We're also sending plaintext passwords over RPC with [proto::AskPassResponse]. Go figure how great that is.
14//!
15//! Still, the goal of this module is to not have passwords laying around nilly-willy in memory.
16//! We do not claim that it is fool-proof.
17use anyhow::Result;
18use zeroize::Zeroize;
19
20type LengthWithoutPadding = u32;
21#[derive(Clone)]
22pub struct EncryptedPassword(Vec<u8>, LengthWithoutPadding);
23
24pub trait ProcessExt {
25 fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self;
26}
27
28impl ProcessExt for smol::process::Command {
29 fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self {
30 if let Ok(password) = decrypt(value) {
31 self.env(name, password);
32 }
33 self
34 }
35}
36
37impl TryFrom<EncryptedPassword> for proto::AskPassResponse {
38 type Error = anyhow::Error;
39 fn try_from(pw: EncryptedPassword) -> Result<Self, Self::Error> {
40 let pw = decrypt(pw)?;
41 Ok(Self { response: pw })
42 }
43}
44
45impl Drop for EncryptedPassword {
46 fn drop(&mut self) {
47 self.0.zeroize();
48 self.1.zeroize();
49 }
50}
51
52impl TryFrom<&str> for EncryptedPassword {
53 type Error = anyhow::Error;
54 fn try_from(password: &str) -> Result<EncryptedPassword> {
55 let len: u32 = password.len().try_into()?;
56 #[cfg(windows)]
57 {
58 use windows::Win32::Security::Cryptography::{
59 CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, CryptProtectMemory,
60 };
61 let mut value = password.bytes().collect::<Vec<_>>();
62 let padded_length = len.next_multiple_of(CRYPTPROTECTMEMORY_BLOCK_SIZE);
63 if padded_length != len {
64 value.resize(padded_length as usize, 0);
65 }
66 unsafe {
67 CryptProtectMemory(
68 value.as_mut_ptr() as _,
69 len,
70 CRYPTPROTECTMEMORY_SAME_PROCESS,
71 )?;
72 }
73 Ok(Self(value, len))
74 }
75 #[cfg(not(windows))]
76 Ok(Self(String::from(password).into(), len))
77 }
78}
79
80pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
81 #[cfg(windows)]
82 {
83 use anyhow::Context;
84 use windows::Win32::Security::Cryptography::{
85 CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, CryptUnprotectMemory,
86 };
87 assert_eq!(
88 password.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
89 0,
90 "Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
91 password.0.len(),
92 CRYPTPROTECTMEMORY_BLOCK_SIZE
93 );
94 unsafe {
95 CryptUnprotectMemory(
96 password.0.as_mut_ptr() as _,
97 password.1,
98 CRYPTPROTECTMEMORY_SAME_PROCESS,
99 )
100 .context("while decrypting a SSH password")?
101 };
102
103 {
104 // Remove padding
105 _ = password.0.drain(password.1 as usize..);
106 }
107 Ok(String::from_utf8(std::mem::take(&mut password.0))?)
108 }
109 #[cfg(not(windows))]
110 Ok(String::from_utf8(std::mem::take(&mut password.0))?)
111}