op-loader 0.5.0

TUI for configuring 1password secrets for injection into your shell environment
use anyhow::{Context, Result};
use rand_core::RngCore;
use security_framework::os::macos::keychain::SecKeychain;
use security_framework::passwords::{
    delete_generic_password, get_generic_password, set_generic_password,
};

const SERVICE: &str = "op-loader cache key";
const ACCOUNT: &str = "default";

pub fn get_or_create_key() -> Result<[u8; 32]> {
    if let Some(existing) = try_get_key()? {
        return Ok(existing);
    }

    let mut key = [0u8; 32];
    rand_core::OsRng.fill_bytes(&mut key);

    set_generic_password(SERVICE, ACCOUNT, &key)
        .context("Failed to store cache key in Keychain")?;

    Ok(key)
}

pub fn delete_key() -> Result<()> {
    if get_generic_password(SERVICE, ACCOUNT).is_ok() {
        delete_generic_password(SERVICE, ACCOUNT)
            .context("Failed to delete cache key from Keychain")?;
    }
    Ok(())
}

fn try_get_key() -> Result<Option<[u8; 32]>> {
    match get_generic_password(SERVICE, ACCOUNT) {
        Ok(bytes) => {
            if bytes.len() != 32 {
                anyhow::bail!(
                    "Invalid Keychain cache key length: expected 32 bytes, got {}",
                    bytes.len()
                );
            }
            let mut key = [0u8; 32];
            key.copy_from_slice(&bytes);
            Ok(Some(key))
        }
        Err(_) => Ok(None),
    }
}

pub fn assert_keychain_available() -> Result<()> {
    SecKeychain::default().context("Failed to access default Keychain")?;
    Ok(())
}