//! Native template functions implemented in Rust. //! //! These are built-in template functions that don't require plugins: //! - `secure()` - encrypts/decrypts values using the EncryptionManager //! - `keychain()` / `keyring()` - accesses system keychain use crate::events::{ Color, FormInput, FormInputBanner, FormInputBase, FormInputMarkdown, FormInputText, PluginContext, RenderPurpose, TemplateFunction, TemplateFunctionArg, TemplateFunctionPreviewType, }; use crate::manager::PluginManager; use crate::template_callback::PluginTemplateCallback; use base64::Engine; use base64::prelude::BASE64_STANDARD; use keyring::Error::NoEntry; use log::{debug, info}; use std::collections::HashMap; use std::sync::Arc; use yaak_common::platform::{OperatingSystem, get_os}; use yaak_crypto::manager::EncryptionManager; use yaak_templates::error::Error::RenderError; use yaak_templates::error::Result; use yaak_templates::{FnArg, Parser, Token, Tokens, Val, transform_args}; pub(crate) fn template_function_secure() -> TemplateFunction { TemplateFunction { name: "secure".to_string(), preview_type: Some(TemplateFunctionPreviewType::None), description: Some("Securely store encrypted text".to_string()), aliases: None, preview_args: None, args: vec![TemplateFunctionArg::FormInput(FormInput::Text( FormInputText { multi_line: Some(true), password: Some(true), base: FormInputBase { name: "value".to_string(), label: Some("Value".to_string()), ..Default::default() }, ..Default::default() }, ))], } } pub(crate) fn template_function_keyring() -> TemplateFunction { struct Meta { description: String, service_label: String, account_label: String, } let meta = match get_os() { OperatingSystem::MacOS => Meta { description: "Access application passwords from the macOS Login keychain".to_string(), service_label: "Where".to_string(), account_label: "Account".to_string(), }, OperatingSystem::Windows => Meta { description: "Access a secret via Windows Credential Manager".to_string(), service_label: "Target".to_string(), account_label: "Username".to_string(), }, _ => Meta { description: "Access a secret via [Secret Service](https://specifications.freedesktop.org/secret-service/latest/) (eg. Gnome keyring or KWallet)".to_string(), service_label: "Collection".to_string(), account_label: "Account".to_string(), }, }; TemplateFunction { name: "keychain".to_string(), preview_type: Some(TemplateFunctionPreviewType::Live), description: Some(meta.description), aliases: Some(vec!["keyring".to_string()]), preview_args: Some(vec!["service".to_string(), "account".to_string()]), args: vec![ TemplateFunctionArg::FormInput(FormInput::Banner(FormInputBanner { inputs: Some(vec![FormInput::Markdown(FormInputMarkdown { content: "For most cases, prefer the [`secure(…)`](https://yaak.app/help/encryption) template function, which encrypts values using a key stored in keychain".to_string(), hidden: None, })]), color: Some(Color::Info), hidden: None, })), TemplateFunctionArg::FormInput(FormInput::Text(FormInputText { base: FormInputBase { name: "service".to_string(), label: Some(meta.service_label), description: Some("App or URL for the password".to_string()), ..Default::default() }, ..Default::default() })), TemplateFunctionArg::FormInput(FormInput::Text(FormInputText { base: FormInputBase { name: "account".to_string(), label: Some(meta.account_label), description: Some("Username or email address".to_string()), ..Default::default() }, ..Default::default() })), ], } } pub fn template_function_secure_run( encryption_manager: &EncryptionManager, args: HashMap, plugin_context: &PluginContext, ) -> Result { match plugin_context.workspace_id.clone() { Some(wid) => { let value = args.get("value").map(|v| v.to_owned()).unwrap_or_default(); let value = match value { serde_json::Value::String(s) => s, _ => return Ok("".to_string()), }; if value.is_empty() { return Ok("".to_string()); } let value = match value.strip_prefix("YENC_") { None => { return Err(RenderError("Could not decrypt non-encrypted value".to_string())); } Some(v) => v, }; let value = BASE64_STANDARD.decode(&value).unwrap(); let r = match encryption_manager.decrypt(&wid, value.as_slice()) { Ok(r) => Ok(r), Err(e) => Err(RenderError(e.to_string())), }?; let r = String::from_utf8(r).map_err(|e| RenderError(e.to_string()))?; Ok(r) } _ => Err(RenderError("workspace_id missing from plugin context".to_string())), } } pub fn template_function_secure_transform_arg( encryption_manager: &EncryptionManager, plugin_context: &PluginContext, arg_name: &str, arg_value: &str, ) -> Result { if arg_name != "value" { return Ok(arg_value.to_string()); } match plugin_context.workspace_id.clone() { Some(wid) => { if arg_value.is_empty() { return Ok("".to_string()); } if arg_value.starts_with("YENC_") { // Already encrypted, so do nothing return Ok(arg_value.to_string()); } let r = encryption_manager .encrypt(&wid, arg_value.as_bytes()) .map_err(|e| RenderError(e.to_string()))?; let r = BASE64_STANDARD.encode(r); Ok(format!("YENC_{}", r)) } _ => Err(RenderError("workspace_id missing from plugin context".to_string())), } } pub fn decrypt_secure_template_function( encryption_manager: &EncryptionManager, plugin_context: &PluginContext, template: &str, ) -> Result { let mut parsed = Parser::new(template).parse()?; let mut new_tokens: Vec = Vec::new(); for token in parsed.tokens.iter() { match token { Token::Tag { val: Val::Fn { name, args } } if name == "secure" => { let mut args_map = HashMap::new(); for a in args { match a.clone().value { Val::Str { text } => { args_map.insert(a.name.to_string(), serde_json::Value::String(text)); } _ => continue, } } new_tokens.push(Token::Raw { text: template_function_secure_run( encryption_manager, args_map, plugin_context, )?, }); } t => { new_tokens.push(t.clone()); continue; } }; } parsed.tokens = new_tokens; Ok(parsed.to_string()) } pub fn encrypt_secure_template_function( plugin_manager: Arc, encryption_manager: Arc, plugin_context: &PluginContext, template: &str, ) -> Result { let decrypted = decrypt_secure_template_function(&encryption_manager, plugin_context, template)?; let tokens = Tokens { tokens: vec![Token::Tag { val: Val::Fn { name: "secure".to_string(), args: vec![FnArg { name: "value".to_string(), value: Val::Str { text: decrypted }, }], }, }], }; Ok(transform_args( tokens, &PluginTemplateCallback::new( plugin_manager, encryption_manager, plugin_context, RenderPurpose::Preview, ), )? .to_string()) } pub fn template_function_keychain_run(args: HashMap) -> Result { let service = args.get("service").and_then(|v| v.as_str()).unwrap_or_default().to_owned(); let user = args.get("account").and_then(|v| v.as_str()).unwrap_or_default().to_owned(); debug!("Getting password for service {} and user {}", service, user); let entry = match keyring::Entry::new(&service, &user) { Ok(e) => e, Err(e) => { debug!("Failed to initialize keyring entry for '{}' and '{}' {:?}", service, user, e); return Ok("".to_string()); // Don't fail for invalid args } }; match entry.get_password() { Ok(p) => Ok(p), Err(NoEntry) => { info!("No password found for '{}' and '{}'", service, user); Ok("".to_string()) // Don't fail for missing passwords } Err(e) => Err(RenderError(e.to_string())), } }