mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
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:
parent
fe717c42b5
commit
3f11d4e3fa
13 changed files with 155 additions and 11 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
21
v3/pkg/w32/wda.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue