From cf175b238396f1d81b17a5ff348065a47aea2ba5 Mon Sep 17 00:00:00 2001 From: Umut Dag Date: Tue, 31 Mar 2026 11:09:16 +0300 Subject: [PATCH] feat(clipboard-manager): add support for writing secrets add `write_secret` command that writes a text into the clipboard and hints clipboard managers to not save the text. uses `arboard`'s `SetExtLinux`, `SetExtWindows`, and `SetExtApple` extensions for each platform. --- Cargo.lock | 12 ++--- .../api/src-tauri/capabilities/desktop.json | 3 +- examples/api/src/views/Clipboard.svelte | 8 ++++ plugins/clipboard-manager/api-iife.js | 2 +- plugins/clipboard-manager/build.rs | 1 + plugins/clipboard-manager/guest-js/index.ts | 20 ++++++++- .../autogenerated/commands/write_secret.toml | 13 ++++++ .../permissions/autogenerated/reference.md | 26 +++++++++++ .../permissions/schemas/schema.json | 12 +++++ plugins/clipboard-manager/src/commands.rs | 10 +++++ plugins/clipboard-manager/src/desktop.rs | 20 +++++++++ plugins/clipboard-manager/src/lib.rs | 7 ++- plugins/clipboard-manager/src/secret.rs | 45 +++++++++++++++++++ 13 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 plugins/clipboard-manager/permissions/autogenerated/commands/write_secret.toml create mode 100644 plugins/clipboard-manager/src/secret.rs diff --git a/Cargo.lock b/Cargo.lock index 6b8b4fc3be..9631a6517c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,9 +286,9 @@ dependencies = [ [[package]] name = "arboard" -version = "3.5.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", "image", @@ -300,7 +300,7 @@ dependencies = [ "objc2-foundation 0.3.0", "parking_lot", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "wl-clipboard-rs", "x11rb", ] @@ -5329,7 +5329,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5386,7 +5386,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -7213,7 +7213,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.1.3", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] diff --git a/examples/api/src-tauri/capabilities/desktop.json b/examples/api/src-tauri/capabilities/desktop.json index 82d8354f53..2102165049 100644 --- a/examples/api/src-tauri/capabilities/desktop.json +++ b/examples/api/src-tauri/capabilities/desktop.json @@ -11,6 +11,7 @@ "global-shortcut:allow-register", "global-shortcut:allow-unregister-all", { "identifier": "fs:allow-watch", "allow": ["*", "**/*"] }, - "fs:allow-unwatch" + "fs:allow-unwatch", + "clipboard-manager:allow-write-secret" ] } diff --git a/examples/api/src/views/Clipboard.svelte b/examples/api/src/views/Clipboard.svelte index f16a7d7167..05737c41bb 100644 --- a/examples/api/src/views/Clipboard.svelte +++ b/examples/api/src/views/Clipboard.svelte @@ -36,6 +36,13 @@ } } + function writeSecret() { + clipboard.writeSecret(text).then(() => { + onMessage("Wrote secret to the clipboard") + }) + .catch(onMessage) + } + async function read() { try { const image = await clipboard.readImage() @@ -64,6 +71,7 @@ bind:value={text} /> + diff --git a/plugins/clipboard-manager/api-iife.js b/plugins/clipboard-manager/api-iife.js index 845708b061..4cb00b81ae 100644 --- a/plugins/clipboard-manager/api-iife.js +++ b/plugins/clipboard-manager/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_CLIPBOARD_MANAGER__=function(e){"use strict";var n;async function t(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}"function"==typeof SuppressedError&&SuppressedError;class r{get rid(){return function(e,n,t,r){if("function"==typeof n?e!==n||!r:!n.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===t?r:"a"===t?r.call(e):r?r.value:n.get(e)}(this,n,"f")}constructor(e){n.set(this,void 0),function(e,n,t){if("function"==typeof n||!n.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");n.set(e,t)}(this,n,e)}async close(){return t("plugin:resources|close",{rid:this.rid})}}n=new WeakMap;class a extends r{constructor(e){super(e)}static async new(e,n,r){return t("plugin:image|new",{rgba:i(e),width:n,height:r}).then(e=>new a(e))}static async fromBytes(e){return t("plugin:image|from_bytes",{bytes:i(e)}).then(e=>new a(e))}static async fromPath(e){return t("plugin:image|from_path",{path:e}).then(e=>new a(e))}async rgba(){return t("plugin:image|rgba",{rid:this.rid}).then(e=>new Uint8Array(e))}async size(){return t("plugin:image|size",{rid:this.rid})}}function i(e){return null==e?null:"string"==typeof e?e:e instanceof a?e.rid:e}return e.clear=async function(){await t("plugin:clipboard-manager|clear")},e.readImage=async function(){return await t("plugin:clipboard-manager|read_image").then(e=>new a(e))},e.readText=async function(){return await t("plugin:clipboard-manager|read_text")},e.writeHtml=async function(e,n){await t("plugin:clipboard-manager|write_html",{html:e,altText:n})},e.writeImage=async function(e){await t("plugin:clipboard-manager|write_image",{image:i(e)})},e.writeText=async function(e,n){await t("plugin:clipboard-manager|write_text",{label:n?.label,text:e})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_PLUGIN_CLIPBOARD_MANAGER__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_CLIPBOARD_MANAGER__=function(e){"use strict";var t;async function n(e,t={},n){return window.__TAURI_INTERNALS__.invoke(e,t,n)}"function"==typeof SuppressedError&&SuppressedError;class r{get rid(){return function(e,t,n,r){if("function"==typeof t?e!==t||!r:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?r:"a"===n?r.call(e):r?r.value:t.get(e)}(this,t,"f")}constructor(e){t.set(this,void 0),function(e,t,n){if("function"==typeof t||!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");t.set(e,n)}(this,t,e)}async close(){return n("plugin:resources|close",{rid:this.rid})}}t=new WeakMap;class a extends r{constructor(e){super(e)}static async new(e,t,r){return n("plugin:image|new",{rgba:i(e),width:t,height:r}).then(e=>new a(e))}static async fromBytes(e){return n("plugin:image|from_bytes",{bytes:i(e)}).then(e=>new a(e))}static async fromPath(e){return n("plugin:image|from_path",{path:e}).then(e=>new a(e))}async rgba(){return n("plugin:image|rgba",{rid:this.rid}).then(e=>new Uint8Array(e))}async size(){return n("plugin:image|size",{rid:this.rid})}}function i(e){return null==e?null:"string"==typeof e?e:e instanceof a?e.rid:e}return e.clear=async function(){await n("plugin:clipboard-manager|clear")},e.readImage=async function(){return await n("plugin:clipboard-manager|read_image").then(e=>new a(e))},e.readText=async function(){return await n("plugin:clipboard-manager|read_text")},e.writeHtml=async function(e,t){await n("plugin:clipboard-manager|write_html",{html:e,altText:t})},e.writeImage=async function(e){await n("plugin:clipboard-manager|write_image",{image:i(e)})},e.writeSecret=async function(e){await n("plugin:clipboard-manager|write_secret",{text:e})},e.writeText=async function(e,t){await n("plugin:clipboard-manager|write_text",{label:t?.label,text:e})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_PLUGIN_CLIPBOARD_MANAGER__})} diff --git a/plugins/clipboard-manager/build.rs b/plugins/clipboard-manager/build.rs index 9bbeddfcbd..c12a71d8a1 100644 --- a/plugins/clipboard-manager/build.rs +++ b/plugins/clipboard-manager/build.rs @@ -9,6 +9,7 @@ const COMMANDS: &[&str] = &[ "read_image", "write_html", "clear", + "write_secret", ]; fn main() { diff --git a/plugins/clipboard-manager/guest-js/index.ts b/plugins/clipboard-manager/guest-js/index.ts index a37bbfab1f..28094ef2f7 100644 --- a/plugins/clipboard-manager/guest-js/index.ts +++ b/plugins/clipboard-manager/guest-js/index.ts @@ -148,4 +148,22 @@ async function clear(): Promise { await invoke('plugin:clipboard-manager|clear') } -export { writeText, readText, writeHtml, clear, readImage, writeImage } +/** + * Writes text to clipboard and sets hints for clipboard managers to exclude it + * from history + * + * @returns A promise indicating the success or failure of the operation. + */ +async function writeSecret(text: string): Promise { + await invoke('plugin:clipboard-manager|write_secret', { text }) +} + +export { + writeText, + readText, + writeHtml, + clear, + readImage, + writeImage, + writeSecret +} diff --git a/plugins/clipboard-manager/permissions/autogenerated/commands/write_secret.toml b/plugins/clipboard-manager/permissions/autogenerated/commands/write_secret.toml new file mode 100644 index 0000000000..6c28172a0a --- /dev/null +++ b/plugins/clipboard-manager/permissions/autogenerated/commands/write_secret.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-write-secret" +description = "Enables the write_secret command without any pre-configured scope." +commands.allow = ["write_secret"] + +[[permission]] +identifier = "deny-write-secret" +description = "Denies the write_secret command without any pre-configured scope." +commands.deny = ["write_secret"] diff --git a/plugins/clipboard-manager/permissions/autogenerated/reference.md b/plugins/clipboard-manager/permissions/autogenerated/reference.md index 98a7fa961f..d4c2430ff2 100644 --- a/plugins/clipboard-manager/permissions/autogenerated/reference.md +++ b/plugins/clipboard-manager/permissions/autogenerated/reference.md @@ -148,6 +148,32 @@ Denies the write_image command without any pre-configured scope. +`clipboard-manager:allow-write-secret` + + + + +Enables the write_secret command without any pre-configured scope. + + + + + + + +`clipboard-manager:deny-write-secret` + + + + +Denies the write_secret command without any pre-configured scope. + + + + + + + `clipboard-manager:allow-write-text` diff --git a/plugins/clipboard-manager/permissions/schemas/schema.json b/plugins/clipboard-manager/permissions/schemas/schema.json index 891c6f0d8d..ee24470338 100644 --- a/plugins/clipboard-manager/permissions/schemas/schema.json +++ b/plugins/clipboard-manager/permissions/schemas/schema.json @@ -354,6 +354,18 @@ "const": "deny-write-image", "markdownDescription": "Denies the write_image command without any pre-configured scope." }, + { + "description": "Enables the write_secret command without any pre-configured scope.", + "type": "string", + "const": "allow-write-secret", + "markdownDescription": "Enables the write_secret command without any pre-configured scope." + }, + { + "description": "Denies the write_secret command without any pre-configured scope.", + "type": "string", + "const": "deny-write-secret", + "markdownDescription": "Denies the write_secret command without any pre-configured scope." + }, { "description": "Enables the write_text command without any pre-configured scope.", "type": "string", diff --git a/plugins/clipboard-manager/src/commands.rs b/plugins/clipboard-manager/src/commands.rs index a8dd94ac06..0d5ac6d214 100644 --- a/plugins/clipboard-manager/src/commands.rs +++ b/plugins/clipboard-manager/src/commands.rs @@ -78,3 +78,13 @@ pub(crate) async fn clear( ) -> Result<()> { clipboard.clear() } + +#[command] +#[cfg(desktop)] +pub(crate) async fn write_secret( + _app: AppHandle, + clipboard: State<'_, Clipboard>, + text: &str, +) -> Result<()> { + clipboard.write_secret(text) +} diff --git a/plugins/clipboard-manager/src/desktop.rs b/plugins/clipboard-manager/src/desktop.rs index f3570cc0c7..2b220a4503 100644 --- a/plugins/clipboard-manager/src/desktop.rs +++ b/plugins/clipboard-manager/src/desktop.rs @@ -8,6 +8,9 @@ use tauri::{image::Image, plugin::PluginApi, AppHandle, Runtime}; use std::{borrow::Cow, sync::Mutex}; +#[cfg(desktop)] +use crate::secret::ExcludeSecret; + pub fn init( app: &AppHandle, _api: PluginApi, @@ -120,4 +123,21 @@ impl Clipboard { clipboard.lock().unwrap().take(); } } + + /// This is the same as write_text but it will set hints using [`arboard::SetExtLinux`], [`arboard::SetExtWindows`], or [`arboard::SetExtApple`] depending on the platform. + #[cfg(desktop)] + pub fn write_secret<'a, T: Into>>(&self, text: T) -> crate::Result<()> { + match &self.clipboard { + Ok(clipboard) => clipboard + .lock() + .unwrap() + .as_mut() + .unwrap() + .set() + .exclude_secret() + .text(text) + .map_err(Into::into), + Err(e) => Err(crate::Error::Clipboard(e.to_string())), + } + } } diff --git a/plugins/clipboard-manager/src/lib.rs b/plugins/clipboard-manager/src/lib.rs index 0cbb4e41ec..4f0400901d 100644 --- a/plugins/clipboard-manager/src/lib.rs +++ b/plugins/clipboard-manager/src/lib.rs @@ -22,6 +22,9 @@ mod mobile; mod commands; mod error; +#[cfg(desktop)] +mod secret; + pub use error::{Error, Result}; #[cfg(desktop)] @@ -49,7 +52,9 @@ pub fn init() -> TauriPlugin { commands::read_image, commands::write_image, commands::write_html, - commands::clear + commands::clear, + #[cfg(desktop)] + commands::write_secret, ]) .setup(|app, api| { #[cfg(mobile)] diff --git a/plugins/clipboard-manager/src/secret.rs b/plugins/clipboard-manager/src/secret.rs new file mode 100644 index 0000000000..78c3484f3e --- /dev/null +++ b/plugins/clipboard-manager/src/secret.rs @@ -0,0 +1,45 @@ +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "android", target_os = "emscripten")), +))] +use arboard::SetExtLinux; + +#[cfg(windows)] +use arboard::SetExtWindows; + +#[cfg(target_os = "macos")] +use arboard::SetExtApple; + +/// Trait to expose exclude from history functionality from [`arboard`] crate's `SetExt*` extensions. +/// On Linux, it calls [`arboard::SetExtLinux::exclude_from_history`] +/// On MacOS, it calls [`arboard::SetExtApple::exclude_from_history`] +/// On Windows, it calls [`arboard::SetExtWindows::exclude_from_history`], [`arboard::SetExtWindows::exclude_from_cloud`], and [`arboard::SetExtWindows::exclude_from_monitoring`] +pub trait ExcludeSecret<'clipboard> { + fn exclude_secret(self) -> arboard::Set<'clipboard>; +} + +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "android", target_os = "emscripten")) +))] +impl<'clipboard> ExcludeSecret<'clipboard> for arboard::Set<'clipboard> { + fn exclude_secret(self) -> arboard::Set<'clipboard> { + self.exclude_from_history() + } +} + +#[cfg(windows)] +impl<'clipboard> ExcludeSecret<'clipboard> for arboard::Set<'clipboard> { + fn exclude_secret(self) -> arboard::Set<'clipboard> { + self.exclude_from_history() + .exclude_from_cloud() + .exclude_from_monitoring() + } +} + +#[cfg(target_os = "macos")] +impl<'clipboard> ExcludeSecret<'clipboard> for arboard::Set<'clipboard> { + fn exclude_secret(self) -> arboard::Set<'clipboard> { + self.exclude_from_history() + } +}