From 3f11d4e3fa2639cc7bfd5b676d44db3eb18b55b9 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 9 Aug 2025 17:12:39 +1000 Subject: [PATCH] feat: Add Content Protection for Windows and macOS (#4485) * feat: Add Content Protection for Windows and macOS - Prevents window contents from being captured by screen recording/sharing software - Windows: Uses WDA_EXCLUDEFROMCAPTURE on Windows 10 2004+, falls back to WDA_MONITOR - macOS: Uses NSWindowSharingType to set window as read-only for screen sharing - Added ContentProtectionEnabled option to WebviewWindowOptions - Added SetContentProtection() method for runtime control - Added comprehensive documentation and example usage * fix changelog.mdx * Misc fixes * Misc fixes * Update v3/pkg/w32/user32.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Remove debug line --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- docs/src/content/docs/changelog.mdx | 2 +- docs/src/content/docs/learn/windows.mdx | 52 ++++++++++++++++++++ v3/UNRELEASED_CHANGELOG.md | 1 + v3/examples/window/main.go | 12 +++++ v3/pkg/application/webview_window.go | 12 +++++ v3/pkg/application/webview_window_darwin.go | 20 ++++++++ v3/pkg/application/webview_window_linux.go | 9 ++-- v3/pkg/application/webview_window_options.go | 5 ++ v3/pkg/application/webview_window_windows.go | 15 ++++++ v3/pkg/application/window.go | 1 + v3/pkg/w32/user32.go | 3 +- v3/pkg/w32/wda.go | 21 ++++++++ v3/tasks/cleanup/cleanup.go | 13 +++-- 13 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 v3/pkg/w32/wda.go diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index 7b8b934f2..562b5a5ad 100644 --- a/docs/src/content/docs/changelog.mdx +++ b/docs/src/content/docs/changelog.mdx @@ -181,7 +181,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add method `Close` on `sqlite` service to close the DB manually by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) - Add cancellation support for query methods on `sqlite` service by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) - Add prepared statement support to `sqlite` service with JS bindings by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) -- Gin support by [Lea Anthony](https://github.com/leaanthony) in [PR](https://github.com/wailsapp/wails/pull/3537) based on the original work of [@AnalogJ](https://github.com/AnalogJ) in PR[https://github.com/wailsapp/wails/pull/3537] +- Gin support by [Lea Anthony](https://github.com/leaanthony) in [PR](https://github.com/wailsapp/wails/pull/3537) based on the original work of [@AnalogJ](https://github.com/AnalogJ) in this [PR](https://github.com/wailsapp/wails/pull/3537) - Fix auto save and password auto save always enabled by [@oSethoum](https://github.com/osethoum) in [#4134](https://github.com/wailsapp/wails/pull/4134) - Add `SetMenu()` on window to allow for setting a menu on a window by [@leaanthony](https://github.com/leaanthony) - Add Notification support by [@popaprozac](https://github.com/popaprozac) in [#4098](https://github.com/wailsapp/wails/pull/4098) diff --git a/docs/src/content/docs/learn/windows.mdx b/docs/src/content/docs/learn/windows.mdx index d9f3d9160..e8d1ab76c 100644 --- a/docs/src/content/docs/learn/windows.mdx +++ b/docs/src/content/docs/learn/windows.mdx @@ -196,12 +196,64 @@ Windows provide many methods for controlling their appearance and behavior: - `IsMaximised() bool` - Checks if window is maximized - `IsFullscreen() bool` - Checks if window is fullscreen +#### Security and Privacy +- `SetContentProtection(bool)` - Enables/disables content protection to prevent screen capture + #### Platform-Specific Methods **Windows Only:** - `Flash(bool)` - Flashes the taskbar button - `SnapAssist()` - Triggers Windows 11 Snap Assist (Win+Z) +## Content Protection + +Content Protection prevents window contents from being captured by screen recording or screen sharing software. This is useful for applications that display sensitive information. + +### Enabling Content Protection + +You can enable content protection when creating a window: + +```go +window := app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "Secure Window", + ContentProtectionEnabled: true, +}) +``` + +Or enable/disable it dynamically at runtime: + +```go +// Enable content protection +window.SetContentProtection(true) + +// Disable content protection +window.SetContentProtection(false) +``` + +### Platform Support + +- **Windows**: Uses `WDA_EXCLUDEFROMCAPTURE` on Windows 10 version 2004+ (build 19041+). Falls back to `WDA_MONITOR` on older versions (legacy mode providing partial protection that may not block all capture paths). +- **macOS**: Sets `NSWindow.sharingType` to `NSWindowSharingNone` when enabled (prevents capture) and `NSWindowSharingReadOnly` when disabled (allows capture). +- **Linux**: Currently not implemented (no-op). + +### Use Cases + +Content protection is ideal for: +- Banking and financial applications +- Password managers +- Medical record systems +- Document viewers with confidential information +- Communication apps with private messages + +### Important Notes + +1. Content protection does not prevent physical photography of the screen +2. On Windows, the fallback for older versions provides partial protection +3. Some screen capture tools may still be able to capture protected content using low-level APIs +4. This feature should be part of a comprehensive security strategy, not the only protection +5. Detached DevTools or inspector windows are separate instances and will not inherit content protection automatically + + ## Advanced Window Management ### Multi-Window Applications diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 34fc871cd..7e25f7814 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -17,6 +17,7 @@ After processing, the content will be moved to the main changelog and this file ## Added +- Add Content Protection on Windows/Mac by [@leaanthony](https://github.com/leaanthony) based on the original work of [@Taiterbase](https://github.com/Taiterbase) in this [PR](https://github.com/wailsapp/wails/pull/4241) ## Changed diff --git a/v3/examples/window/main.go b/v3/examples/window/main.go index e9bcd1a67..46a271c49 100644 --- a/v3/examples/window/main.go +++ b/v3/examples/window/main.go @@ -121,6 +121,18 @@ func main() { windowCounter++ }) if runtime.GOOS != "linux" { + myMenu.Add("New WebviewWindow (Content Protection Enabled)"). + OnClick(func(ctx *application.Context) { + app.Window.NewWithOptions(application.WebviewWindowOptions{ + MinimiseButtonState: application.ButtonDisabled, + ContentProtectionEnabled: true, + }). + SetTitle("WebviewWindow "+strconv.Itoa(windowCounter)). + SetRelativePosition(rand.Intn(1000), rand.Intn(800)). + SetURL("https://wails.io"). + Show() + windowCounter++ + }) myMenu.Add("New WebviewWindow (Disable Minimise)"). OnClick(func(ctx *application.Context) { app.Window.NewWithOptions(application.WebviewWindowOptions{ diff --git a/v3/pkg/application/webview_window.go b/v3/pkg/application/webview_window.go index 63552affd..cef9206a3 100644 --- a/v3/pkg/application/webview_window.go +++ b/v3/pkg/application/webview_window.go @@ -112,6 +112,7 @@ type ( toggleMenuBar() setMenu(menu *Menu) snapAssist() + setContentProtection(enabled bool) } ) @@ -520,6 +521,17 @@ func (w *WebviewWindow) SetResizable(b bool) Window { return w } +func (w *WebviewWindow) SetContentProtection(b bool) Window { + if w.impl == nil { + w.options.ContentProtectionEnabled = b + } else { + InvokeSync(func() { + w.impl.setContentProtection(b) + }) + } + return w +} + // Resizable returns true if the window is resizable. func (w *WebviewWindow) Resizable() bool { return !w.options.DisableResize diff --git a/v3/pkg/application/webview_window_darwin.go b/v3/pkg/application/webview_window_darwin.go index 28f1dc127..8d5a85a8e 100644 --- a/v3/pkg/application/webview_window_darwin.go +++ b/v3/pkg/application/webview_window_darwin.go @@ -818,6 +818,19 @@ static void setIgnoreMouseEvents(void *nsWindow, bool ignore) { [window setIgnoresMouseEvents:ignore]; } +static void setContentProtection(void *nsWindow, bool enabled) { + NSWindow *window = (__bridge NSWindow *)nsWindow; + if( ! [window respondsToSelector:@selector(setSharingType:)]) { + return; + } + + if( enabled ) { + [window setSharingType:NSWindowSharingNone]; + } else { + [window setSharingType:NSWindowSharingReadOnly]; + } +} + */ import "C" import ( @@ -1222,6 +1235,9 @@ func (w *macosWebviewWindow) run() { //w.setZoom(options.Zoom) w.enableDevTools() + // Content Protection + w.setContentProtection(options.ContentProtectionEnabled) + w.setBackgroundColour(options.BackgroundColour) switch macOptions.Backdrop { @@ -1422,6 +1438,10 @@ func (w *macosWebviewWindow) setIgnoreMouseEvents(ignore bool) { C.setIgnoreMouseEvents(w.nsWindow, C.bool(ignore)) } +func (w *macosWebviewWindow) setContentProtection(enabled bool) { + C.setContentProtection(w.nsWindow, C.bool(enabled)) +} + func (w *macosWebviewWindow) cut() { } diff --git a/v3/pkg/application/webview_window_linux.go b/v3/pkg/application/webview_window_linux.go index f15bb905d..7f82f8b84 100644 --- a/v3/pkg/application/webview_window_linux.go +++ b/v3/pkg/application/webview_window_linux.go @@ -425,7 +425,8 @@ func (w *linuxWebviewWindow) hide() { w.windowHide() } -func (w *linuxWebviewWindow) showMenuBar() {} -func (w *linuxWebviewWindow) hideMenuBar() {} -func (w *linuxWebviewWindow) toggleMenuBar() {} -func (w *linuxWebviewWindow) snapAssist() {} // No-op on Linux +func (w *linuxWebviewWindow) showMenuBar() {} +func (w *linuxWebviewWindow) hideMenuBar() {} +func (w *linuxWebviewWindow) toggleMenuBar() {} +func (w *linuxWebviewWindow) snapAssist() {} // No-op on Linux +func (w *linuxWebviewWindow) setContentProtection(enabled bool) {} diff --git a/v3/pkg/application/webview_window_options.go b/v3/pkg/application/webview_window_options.go index 05a25bd0c..89be6c878 100644 --- a/v3/pkg/application/webview_window_options.go +++ b/v3/pkg/application/webview_window_options.go @@ -135,6 +135,11 @@ type WebviewWindowOptions struct { // IgnoreMouseEvents will ignore mouse events in the window (Windows + Mac only) IgnoreMouseEvents bool + + // ContentProtectionEnabled specifies whether content protection is enabled, preventing screen capture and recording. + // Effective on Windows and macOS only; no-op on Linux. + // Best-effort protection with platform-specific caveats (see docs). + ContentProtectionEnabled bool } type RGBA struct { diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 7f6739a60..8cdfa9c74 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -380,6 +380,9 @@ func (w *windowsWebviewWindow) run() { globalApplication.fatal("unable to create window") } + // Process ContentProtection + w.setContentProtection(w.parent.options.ContentProtectionEnabled) + // Ensure correct window size in case the scale factor of current screen is different from the initial one. // This could happen when using the default window position and the window launches on a secondary monitor. currentScreen, _ := w.getScreen() @@ -2442,3 +2445,15 @@ func (w *windowsWebviewWindow) snapAssist() { // Release Windows key w32.KeybdEvent(byte(w32.VK_LWIN), 0, w32.KEYEVENTF_KEYUP, 0) } + +func (w *windowsWebviewWindow) setContentProtection(enabled bool) { + var affinity uint32 = w32.WDA_EXCLUDEFROMCAPTURE + if !enabled { + affinity = w32.WDA_NONE + } + if ok := w32.SetWindowDisplayAffinity(w.hwnd, affinity); !ok { + // Note: wrapper already falls back to WDA_MONITOR on older Windows. + globalApplication.warning("SetWindowDisplayAffinity failed: window=%v, affinity=%v", + w.parent.id, affinity) + } +} diff --git a/v3/pkg/application/window.go b/v3/pkg/application/window.go index 01d1bf2e5..04c508f3a 100644 --- a/v3/pkg/application/window.go +++ b/v3/pkg/application/window.go @@ -93,6 +93,7 @@ type Window interface { ZoomReset() Window SetMenu(menu *Menu) SnapAssist() + SetContentProtection(protection bool) Window NativeWindow() unsafe.Pointer SetEnabled(enabled bool) Flash(enabled bool) diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index a6b8f2519..95463d86b 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -188,7 +188,8 @@ var ( procRedrawWindow = moduser32.NewProc("RedrawWindow") - procRegisterWindowMessageW = moduser32.NewProc("RegisterWindowMessageW") + procRegisterWindowMessageW = moduser32.NewProc("RegisterWindowMessageW") + procSetWindowDisplayAffinity = moduser32.NewProc("SetWindowDisplayAffinity") mainThread HANDLE ) diff --git a/v3/pkg/w32/wda.go b/v3/pkg/w32/wda.go new file mode 100644 index 000000000..0faf91a82 --- /dev/null +++ b/v3/pkg/w32/wda.go @@ -0,0 +1,21 @@ +//go:build windows + +package w32 + +const ( + WDA_NONE = 0x00000000 + WDA_MONITOR = 0x00000001 + WDA_EXCLUDEFROMCAPTURE = 0x00000011 // windows 10 2004+ +) + +func SetWindowDisplayAffinity(hwnd uintptr, affinity uint32) bool { + if affinity == WDA_EXCLUDEFROMCAPTURE && !IsWindowsVersionAtLeast(10, 0, 19041) { + // for older windows versions, use WDA_MONITOR + affinity = WDA_MONITOR + } + ret, _, _ := procSetWindowDisplayAffinity.Call( + hwnd, + uintptr(affinity), + ) + return ret != 0 +} diff --git a/v3/tasks/cleanup/cleanup.go b/v3/tasks/cleanup/cleanup.go index f06813abf..bc6fd90ba 100644 --- a/v3/tasks/cleanup/cleanup.go +++ b/v3/tasks/cleanup/cleanup.go @@ -19,16 +19,19 @@ type CleanupPattern struct { var cleanupPatterns = []CleanupPattern{ // Test binaries from examples {Type: "prefix", Pattern: "testbuild-", TargetFiles: true, Description: "test binary"}, - + // Go test binaries {Type: "suffix", Pattern: ".test", TargetFiles: true, Description: "Go test binary"}, - + // Package artifacts from packaging tests (only in internal/commands directory) // Note: Only clean these from the commands directory, not from test temp directories - + {Type: "exact", Pattern: "myapp.ARCHLINUX", TargetFiles: true, Description: "Linux ARCHLINUX package"}, + {Type: "exact", Pattern: "myapp.DEB", TargetFiles: true, Description: "Linux DEB package"}, + {Type: "exact", Pattern: "myapp.RPM", TargetFiles: true, Description: "Linux RPM package"}, + // Test template directories from template tests {Type: "prefix", Pattern: "test-template-", TargetFiles: false, Description: "test template directory"}, - + // CLI test binaries (files named exactly "appimage_testfiles") {Type: "exact", Pattern: "appimage_testfiles", TargetFiles: true, Description: "CLI test binary"}, } @@ -91,4 +94,4 @@ func main() { } fmt.Printf("Cleanup completed. Removed %d items.\n", cleanedCount) -} \ No newline at end of file +}