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>
This commit is contained in:
Lea Anthony 2025-08-09 17:12:39 +10:00 committed by GitHub
commit 3f11d4e3fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 155 additions and 11 deletions

View file

@ -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)

View file

@ -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

View file

@ -17,6 +17,7 @@ After processing, the content will be moved to the main changelog and this file
## Added
<!-- New features, capabilities, or enhancements -->
- 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
<!-- Changes in existing functionality -->

View file

@ -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{

View file

@ -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

View file

@ -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() {
}

View file

@ -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) {}

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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)

View file

@ -188,7 +188,8 @@ var (
procRedrawWindow = moduser32.NewProc("RedrawWindow")
procRegisterWindowMessageW = moduser32.NewProc("RegisterWindowMessageW")
procRegisterWindowMessageW = moduser32.NewProc("RegisterWindowMessageW")
procSetWindowDisplayAffinity = moduser32.NewProc("SetWindowDisplayAffinity")
mainThread HANDLE
)

21
v3/pkg/w32/wda.go Normal file
View file

@ -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
}

View file

@ -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)
}
}