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            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}