enigma-storage 0.0.1

Encrypted local storage for Enigma with mandatory at-rest encryption and cross-platform key vault providers.
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::ptr;

use rand::RngCore;
use windows::core::Error;
use windows::core::PCWSTR;
use windows::Win32::Foundation::LocalFree;
use windows::Win32::Security::Cryptography::{CryptProtectData, CryptUnprotectData, CRYPT_INTEGER_BLOB};

use crate::error::{EnigmaStorageError, Result};
use crate::key_provider::{KeyProvider, MasterKey};

pub struct WindowsDpapiKeyProvider {
    root: PathBuf,
}

impl WindowsDpapiKeyProvider {
    pub fn new<P: AsRef<Path>>(root: P) -> Self {
        WindowsDpapiKeyProvider {
            root: root.as_ref().to_path_buf(),
        }
    }

    fn blob_path(&self) -> PathBuf {
        self.root.join(".enigma_storage_dpapi_blob")
    }

    fn ensure_root(&self) -> Result<()> {
        fs::create_dir_all(&self.root).map_err(|e| EnigmaStorageError::KeyProviderError(e.to_string()))
    }

    fn protect(&self, data: &[u8]) -> Result<Vec<u8>> {
        let mut input = CRYPT_INTEGER_BLOB {
            cbData: data.len() as u32,
            pbData: data.as_ptr() as *mut u8,
        };
        let mut output = CRYPT_INTEGER_BLOB {
            cbData: 0,
            pbData: ptr::null_mut(),
        };
        let status = unsafe {
            CryptProtectData(
                &mut input,
                PCWSTR::null(),
                ptr::null(),
                ptr::null_mut(),
                ptr::null_mut(),
                0,
                &mut output,
            )
        };
        if !status.as_bool() {
            let err = Error::from_win32();
            return Err(EnigmaStorageError::KeyProviderError(format!("DPAPI protect failed: {}", err)));
        }
        let slice = unsafe { std::slice::from_raw_parts(output.pbData, output.cbData as usize) };
        let result = slice.to_vec();
        unsafe {
            if !output.pbData.is_null() {
                LocalFree(output.pbData as isize);
            }
        }
        Ok(result)
    }

    fn unprotect(&self, data: &[u8]) -> Result<Vec<u8>> {
        let mut input = CRYPT_INTEGER_BLOB {
            cbData: data.len() as u32,
            pbData: data.as_ptr() as *mut u8,
        };
        let mut output = CRYPT_INTEGER_BLOB {
            cbData: 0,
            pbData: ptr::null_mut(),
        };
        let status = unsafe {
            CryptUnprotectData(
                &mut input,
                ptr::null_mut(),
                ptr::null(),
                ptr::null_mut(),
                ptr::null_mut(),
                0,
                &mut output,
            )
        };
        if !status.as_bool() {
            let err = Error::from_win32();
            return Err(EnigmaStorageError::KeyProviderError(format!("DPAPI unprotect failed: {}", err)));
        }
        let slice = unsafe { std::slice::from_raw_parts(output.pbData, output.cbData as usize) };
        let result = slice.to_vec();
        unsafe {
            if !output.pbData.is_null() {
                LocalFree(output.pbData as isize);
            }
        }
        Ok(result)
    }
}

impl KeyProvider for WindowsDpapiKeyProvider {
    fn get_or_create_master_key(&self) -> Result<MasterKey> {
        self.ensure_root()?;
        let path = self.blob_path();
        if path.exists() {
            return self.get_master_key();
        }
        let mut key = [0u8; 32];
        rand::thread_rng().fill_bytes(&mut key);
        let blob = self.protect(&key)?;
        fs::write(&path, blob).map_err(|e| EnigmaStorageError::KeyProviderError(e.to_string()))?;
        Ok(MasterKey::new(key))
    }

    fn get_master_key(&self) -> Result<MasterKey> {
        let data = fs::read(self.blob_path()).map_err(|e| EnigmaStorageError::KeyProviderError(e.to_string()))?;
        let plain = self.unprotect(&data)?;
        if plain.len() != 32 {
            return Err(EnigmaStorageError::InvalidKey);
        }
        let mut key = [0u8; 32];
        key.copy_from_slice(&plain);
        Ok(MasterKey::new(key))
    }
}