summaryrefslogtreecommitdiff
path: root/macos/Sources/Features/Secure Input/SecureInput.swift
blob: f999ce5caedb49c5afdea5cdf971a24c7f72c162 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import Carbon
import Cocoa
import OSLog

// Manages the secure keyboard input state. Secure keyboard input is an old Carbon
// API still in use by applications such as Webkit. From the old Carbon docs:
// "When secure event input mode is enabled, keyboard input goes only to the
// application with keyboard focus and is not echoed to other applications that
// might be using the event monitor target to watch keyboard input."
//
// Secure input is global and stateful so you need a singleton class to manage
// it. You have to yield secure input on application deactivation (because
// it'll affect other apps) and reacquire on reactivation, and every enable
// needs to be balanced with a disable.
class SecureInput : ObservableObject {
    static let shared = SecureInput()

    private static let logger = Logger(
        subsystem: Bundle.main.bundleIdentifier!,
        category: String(describing: SecureInput.self)
    )

    // True if you want to enable secure input globally.
    var global: Bool = false {
        didSet {
            apply()
        }
    }

    // The scoped objects and whether they're currently in focus.
    private var scoped: [ObjectIdentifier: Bool] = [:]

    // This is set to true when we've successfully called EnableSecureInput.
    @Published private(set) var enabled: Bool = false

    // This is true if we want to enable secure input. We want to enable
    // secure input if its enabled globally or any of the scoped objects are
    // in focus.
    private var desired: Bool {
        global || scoped.contains(where: { $0.value })
    }

    private init() {
        // Add notifications for application active/resign so we can disable
        // secure input. This is only useful for global enabling of secure
        // input.
        let center = NotificationCenter.default
        center.addObserver(
            self,
            selector: #selector(onDidResignActive(notification:)),
            name: NSApplication.didResignActiveNotification,
            object: nil)
        center.addObserver(
            self,
            selector: #selector(onDidBecomeActive(notification:)),
            name: NSApplication.didBecomeActiveNotification,
            object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)

        // Reset our state so that we can ensure we set the proper secure input
        // system state
        scoped.removeAll()
        global = false
        apply()
    }

    // Add a scoped object that has secure input enabled. The focused value will
    // determine if the object currently has focus. This is used so that secure
    // input is only enabled while the object is focused.
    func setScoped(_ object: ObjectIdentifier, focused: Bool) {
        scoped[object] = focused
        apply()
    }

    // Remove a scoped object completely.
    func removeScoped(_ object: ObjectIdentifier) {
        scoped[object] = nil
        apply()
    }

    private func apply() {
        // If we aren't active then we don't do anything. The become/resign
        // active notifications will handle applying for us.
        guard NSApp.isActive else { return }

        // We only need to apply if we're not in our desired state
        guard enabled != desired else { return }

        let err: OSStatus
        if (enabled) {
            err = DisableSecureEventInput()
        } else {
            err = EnableSecureEventInput()
        }
        if (err == noErr) {
            enabled = desired
            Self.logger.debug("secure input state=\(self.enabled)")
            return
        }

        Self.logger.warning("secure input apply failed err=\(err)")
    }

    // MARK: Notifications

    @objc private func onDidBecomeActive(notification: NSNotification) {
        // We only want to re-enable if we're not already enabled and we
        // desire to be enabled.
        guard !enabled && desired else { return }
        let err = EnableSecureEventInput()
        if (err == noErr) {
            enabled = true
            Self.logger.debug("secure input enabled on activation")
            return
        }

        Self.logger.warning("secure input apply failed err=\(err)")
    }

    @objc private func onDidResignActive(notification: NSNotification) {
        // We only want to disable if we're enabled.
        guard enabled else { return }
        let err = DisableSecureEventInput()
        if (err == noErr) {
            enabled = false
            Self.logger.debug("secure input disabled on deactivation")
            return
        }

        Self.logger.warning("secure input apply failed err=\(err)")
    }
}