keyring-manager 0.10.0

Cross-platform library for managing passwords
Documentation
use crate::error::{KeyringError, Result};
use crate::Keyring;
use byteorder::{ByteOrder, LittleEndian};
use std::ffi::OsStr;
use std::iter::once;
use std::mem::MaybeUninit;
use std::os::windows::ffi::OsStrExt;
use std::slice;
use std::str;
use winapi::shared::minwindef::FILETIME;
use winapi::shared::winerror::{ERROR_NOT_FOUND, ERROR_NO_SUCH_LOGON_SESSION};
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::wincred::{
    CredDeleteW, CredEnumerateW, CredFree, CredReadW, CredWriteW, CREDENTIALW,
    CRED_ENUMERATE_ALL_CREDENTIALS, CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC, PCREDENTIALW,
    PCREDENTIAL_ATTRIBUTEW,
};

// DWORD is u32
// LPCWSTR is *const u16
// BOOL is i32 (false = 0, true = 1)
// PCREDENTIALW = *mut CREDENTIALW

// Note: decision to concatenate user and service name
// to create target is because Windows assumes one user
// per service. See issue here: https://github.com/jaraco/keyring/issues/47

pub struct WindowsKeyringManager {
    application: String,
}

impl WindowsKeyringManager {
    pub fn new(application: &str) -> Result<Self> {
        Ok(Self {
            application: application.to_owned(),
        })
    }

    pub fn with_keyring<F, T>(&self, service: &str, key: &str, func: F) -> Result<T>
    where
        F: FnOnce(&mut dyn Keyring) -> Result<T>,
    {
        let mut kr = WindowsKeyring::new(&self.application, service, key)?;
        func(&mut kr)
    }

    pub fn list_keys(&self, service: &str) -> Result<Vec<String>> {
        let application = crate::escape(&self.application);
        let service = crate::escape(service);

        // The filter is only a narrowing hint; escape() passes '*' and '?' through, so an
        // application name containing either would under-match. Enumerate everything instead.
        let wildcards = application.contains('*') || application.contains('?');
        let filter_wstr = to_wstr(&format!("{}\t*", application));
        let (filter, flags) = if wildcards {
            (std::ptr::null(), CRED_ENUMERATE_ALL_CREDENTIALS)
        } else {
            (filter_wstr.as_ptr(), 0)
        };

        let mut count: u32 = 0;
        let mut pcredentials: *mut PCREDENTIALW = std::ptr::null_mut();
        if unsafe { CredEnumerateW(filter, flags, &mut count, &mut pcredentials) } == 0 {
            return match unsafe { GetLastError() } {
                // No credential matched the filter, which is an empty list, not an error.
                // (map_error would read this as a missing password.)
                ERROR_NOT_FOUND => Ok(Vec::new()),
                code => Err(map_error(code)),
            };
        }

        let credentials = unsafe { slice::from_raw_parts(pcredentials, count as usize) };
        let out = credentials
            .iter()
            .filter_map(|pcred| {
                let target_name = unsafe { from_wstr((**pcred).TargetName) };
                // Target names are 'application\tkey\tservice'; escaping keeps tabs out of the parts.
                let parts: Vec<&str> = target_name.split('\t').collect();
                match parts[..] {
                    [app, key, svc] if app == application && svc == service => {
                        Some(crate::unescape(key))
                    }
                    _ => None,
                }
            })
            .collect();

        unsafe { CredFree(pcredentials as *mut _) };

        out
    }
}

// Classify the failure of a wincred call. Every caller goes through here so a failure
// names its cause instead of collapsing to an opaque vault error.
fn map_error(code: u32) -> KeyringError {
    match code {
        ERROR_NOT_FOUND => KeyringError::NoPasswordFound,
        // A network logon session has no credential set at all, so the vault is simply
        // not there. This is what a CI runner without an interactive session reports.
        ERROR_NO_SUCH_LOGON_SESSION => KeyringError::NoBackendFound,
        code => KeyringError::WindowsVaultError(code),
    }
}

fn last_error() -> KeyringError {
    map_error(unsafe { GetLastError() })
}

pub struct WindowsKeyring<'a> {
    application: &'a str,
    service: &'a str,
    key: &'a str,
}

impl<'a> WindowsKeyring<'a> {
    fn new(application: &'a str, service: &'a str, key: &'a str) -> Result<WindowsKeyring<'a>> {
        Ok(Self {
            application,
            service,
            key,
        })
    }

    fn get_account_name(&self) -> String {
        [
            crate::escape(self.application),
            crate::escape(self.key),
            crate::escape(self.service),
        ]
        .join("\t")
    }
}

impl<'a> Keyring for WindowsKeyring<'a> {
    fn set_value(&mut self, value: &str) -> Result<()> {
        // Setting values of credential

        let flags = 0;
        let cred_type = CRED_TYPE_GENERIC;
        let target_name: String = self.get_account_name();
        let mut target_name = to_wstr(&target_name);

        // empty string for comments, and target alias,
        // I don't use here
        let mut empty_str = to_wstr("");

        // Ignored by CredWriteW
        let last_written = FILETIME {
            dwLowDateTime: 0,
            dwHighDateTime: 0,
        };

        // In order to allow editing of the password
        // from within Windows, the password must be
        // transformed into utf16. (but because it's a
        // blob, it then needs to be passed to windows
        // as an array of bytes).
        let blob_u16 = to_wstr_no_null(value);
        let mut blob = vec![0; blob_u16.len() * 2];
        LittleEndian::write_u16_into(&blob_u16, &mut blob);

        let blob_len = blob.len() as u32;
        let persist = CRED_PERSIST_ENTERPRISE;
        let attribute_count = 0;
        let attributes: PCREDENTIAL_ATTRIBUTEW = std::ptr::null_mut();
        let mut key = to_wstr(self.key);

        let mut credential = CREDENTIALW {
            Flags: flags,
            Type: cred_type,
            TargetName: target_name.as_mut_ptr(),
            Comment: empty_str.as_mut_ptr(),
            LastWritten: last_written,
            CredentialBlobSize: blob_len,
            CredentialBlob: blob.as_mut_ptr(),
            Persist: persist,
            AttributeCount: attribute_count,
            Attributes: attributes,
            TargetAlias: empty_str.as_mut_ptr(),
            UserName: key.as_mut_ptr(),
        };
        // raw pointer to credential, is coerced from &mut
        let pcredential: PCREDENTIALW = &mut credential;

        // Call windows API
        match unsafe { CredWriteW(pcredential, 0) } {
            0 => Err(last_error()),
            _ => Ok(()),
        }
    }

    fn get_value(&self) -> Result<String> {
        // passing uninitialized pcredential.
        // Should be ok; it's freed by a windows api
        // call CredFree.
        let mut pcredential = MaybeUninit::uninit();

        let target_name: String = self.get_account_name();
        let target_name = to_wstr(&target_name);

        let cred_type = CRED_TYPE_GENERIC;

        // Windows api call
        match unsafe { CredReadW(target_name.as_ptr(), cred_type, 0, pcredential.as_mut_ptr()) } {
            0 => Err(last_error()),
            _ => {
                let pcredential = unsafe { pcredential.assume_init() };
                // Dereferencing pointer to credential
                let credential: CREDENTIALW = unsafe { *pcredential };

                // get blob by creating an array from the pointer
                // and the length reported back from the credential
                let blob_pointer: *const u8 = credential.CredentialBlob;
                let blob_len: usize = credential.CredentialBlobSize as usize;

                // blob needs to be transformed from bytes to an
                // array of u16, which will then be transformed into
                // a utf8 string. As noted above, this is to allow
                // editing of the password from within the vault order
                // or other windows programs, which operate in utf16
                let mut blob_u16 = vec![0; blob_len / 2];
                if !blob_pointer.is_null() && blob_len > 0 {
                    let blob: &[u8] = unsafe { slice::from_raw_parts(blob_pointer, blob_len) };
                    LittleEndian::read_u16_into(blob, &mut blob_u16);
                }

                // Now can get utf8 string from the array
                // Not a win32 failure: the stored blob isn't utf-16, so some other program wrote it.
                let password = String::from_utf16(&blob_u16)
                    .map(|pass| pass.to_string())
                    .map_err(|e| {
                        KeyringError::Generic(format!("credential blob is not valid utf-16: {}", e))
                    });

                // Free the credential
                unsafe {
                    CredFree(pcredential as *mut _);
                }

                password
            }
        }
    }

    fn delete_value(&mut self) -> Result<()> {
        let target_name: String = self.get_account_name();

        let cred_type = CRED_TYPE_GENERIC;
        let target_name = to_wstr(&target_name);

        match unsafe { CredDeleteW(target_name.as_ptr(), cred_type, 0) } {
            0 => Err(last_error()),
            _ => Ok(()),
        }
    }
}

// helper function for turning utf8 strings to windows
// utf16
fn to_wstr(s: &str) -> Vec<u16> {
    OsStr::new(s).encode_wide().chain(once(0)).collect()
}

fn to_wstr_no_null(s: &str) -> Vec<u16> {
    OsStr::new(s).encode_wide().collect()
}

// Caller must pass a valid NUL-terminated wide string owned by the credential blob.
unsafe fn from_wstr(s: *const u16) -> String {
    if s.is_null() {
        return String::new();
    }
    let mut len = 0;
    while *s.add(len) != 0 {
        len += 1;
    }
    String::from_utf16_lossy(slice::from_raw_parts(s, len))
}