encrypted_password.rs

  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}