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 if len != 0 {
67 unsafe {
68 CryptProtectMemory(
69 value.as_mut_ptr() as _,
70 padded_length,
71 CRYPTPROTECTMEMORY_SAME_PROCESS,
72 )?;
73 }
74 }
75 Ok(Self(value, len))
76 }
77 #[cfg(not(windows))]
78 Ok(Self(String::from(password).into(), len))
79 }
80}
81
82pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
83 #[cfg(windows)]
84 {
85 use anyhow::Context;
86 use windows::Win32::Security::Cryptography::{
87 CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, CryptUnprotectMemory,
88 };
89 assert_eq!(
90 password.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
91 0,
92 "Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
93 password.0.len(),
94 CRYPTPROTECTMEMORY_BLOCK_SIZE
95 );
96 if password.1 != 0 {
97 unsafe {
98 CryptUnprotectMemory(
99 password.0.as_mut_ptr() as _,
100 password.0.len().try_into()?,
101 CRYPTPROTECTMEMORY_SAME_PROCESS,
102 )
103 .context("while decrypting a SSH password")?
104 };
105
106 {
107 // Remove padding
108 _ = password.0.drain(password.1 as usize..);
109 }
110 }
111
112 Ok(String::from_utf8(std::mem::take(&mut password.0))?)
113 }
114 #[cfg(not(windows))]
115 Ok(String::from_utf8(std::mem::take(&mut password.0))?)
116}