Skip to content

Instantly share code, notes, and snippets.

@ShikiSuen
Last active March 7, 2024 17:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ShikiSuen/493babbcc032c6b9bb6332537051f2cb to your computer and use it in GitHub Desktop.
Save ShikiSuen/493babbcc032c6b9bb6332537051f2cb to your computer and use it in GitHub Desktop.
A Swift script to check whether there's any process abusing the SecureEventInput in the background.
// (c) 2023 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
/// A Swift script to check whether a non-system process is abusing the SecureEventInput.
import AppKit
import Combine
import IOKit
public class SecureEventInputSputnik {
public let shared = SecureEventInputSputnik()
public init() {
oobe()
}
public static func getIORegListResults() -> String? {
// Don't generate results under any of the following situations:
// - Hibernation / LoggedOut / SwitchedOut / ScreenSaver situations.
guard NSWorkspace.activationFlags.isEmpty else { return nil }
var resultDictionaryCF: Unmanaged<CFMutableDictionary>?
defer { resultDictionaryCF = nil }
/// Regarding the parameter in IORegistryGetRootEntry:
/// Both kIOMasterPortDefault and kIOMainPortDefault are 0.
/// The latter one is similar to what `git` had done: changing "Master" to "Main".
let statusSucceeded = IORegistryEntryCreateCFProperties(
IORegistryGetRootEntry(0), &resultDictionaryCF, kCFAllocatorDefault, IOOptionBits(0)
)
let dict: CFMutableDictionary? = resultDictionaryCF?.takeRetainedValue()
guard statusSucceeded == KERN_SUCCESS else { return nil }
guard let dict: [CFString: Any] = dict as? [CFString: Any] else { return nil }
return (dict.description)
}
/// Find all non-system processes using the SecureEventInput.
/// - Parameter abusersOnly: List only non-frontmost processes.
/// - **Reason to Use**: Non-frontmost processes of such are considered abusers of SecureEventInput,
/// hindering 3rd-party input methods from being switched to by the user.
/// They are also hindering users from accessing the menu of all 3rd-party input methods.
/// There are Apple's internal business reasons why macOS always has lack of certain crucial input methods,
/// plus that some some IMEs in macOS have certain bugs / defects for decades and are unlikely to be solved,
/// making the sense that why there are needs of 3rd-party input methods.
/// - **How to Use**: For example, one can use an NSTimer to run this function
/// with `abusersOnly: true` every 15~60 seconds. Once the result dictionary is not empty,
/// you may either warn the users to restart the matched process or directly terminate it.
/// Note that you cannot terminate a process if your app is Sandboxed.
/// - Returns: Matched results as a dictionary in `[Int32: NSRunningApplication]` format. The keys are PIDs.
/// - Remark: The`"com.apple.SecurityAgent"` won't be included in the result since it is a system process.
/// Also, "com.apple.loginwindow" should be excluded as long as the system screen saver engine is running.
public static func getRunningSecureInputApps(abusersOnly: Bool = false) -> [Int32: NSRunningApplication] {
var result = [Int32: NSRunningApplication]()
guard let rawData = getIORegListResults() else { return result }
rawData.enumerateLines { currentLine, _ in
guard currentLine.contains("kCGSSessionSecureInputPID") else { return }
guard let filteredNumStr = Int32(currentLine.filter("0123456789".contains)) else { return }
guard let matchedApp = NSRunningApplication(processIdentifier: filteredNumStr) else { return }
guard matchedApp.bundleIdentifier != "com.apple.SecurityAgent" else { return }
guard !(matchedApp.isLoginWindowWithLockedScreenOrScreenSaver) else { return }
if abusersOnly {
guard !matchedApp.isActive else { return }
}
result[filteredNumStr] = matchedApp
}
return result
}
}
public extension NSWorkspace {
struct ActivationFlags: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let hibernating = ActivationFlags(rawValue: 1 << 0)
public static let desktopLocked = ActivationFlags(rawValue: 1 << 1)
public static let sessionSwitchedOut = ActivationFlags(rawValue: 1 << 2)
public static let screenSaverRunning = ActivationFlags(rawValue: 1 << 3)
}
static var activationFlags: ActivationFlags = []
}
public extension NSRunningApplication {
var isLoginWindowWithLockedScreenOrScreenSaver: Bool {
guard bundleIdentifier == "com.apple.loginwindow" else { return false }
return !NSWorkspace.activationFlags.isEmpty
}
}
extension SecureEventInputSputnik {
private static var combinePoolCocoa = [any NSObjectProtocol]()
@available(macOS 10.15, *)
private static var combinePool = Set<AnyCancellable>()
func oobe() {
if #available(macOS 10.15, *) {
DistributedNotificationCenter.default()
.publisher(for: .init(rawValue: "com.apple.screenIsLocked"))
.sink { _ in NSWorkspace.activationFlags.insert(.desktopLocked) }
.store(in: &Self.combinePool)
DistributedNotificationCenter.default()
.publisher(for: .init(rawValue: "com.apple.screenIsUnlocked"))
.sink { _ in NSWorkspace.activationFlags.remove(.desktopLocked) }
.store(in: &Self.combinePool)
DistributedNotificationCenter.default()
.publisher(for: .init(rawValue: "com.apple.screensaver.didstart"))
.sink { _ in NSWorkspace.activationFlags.insert(.screenSaverRunning) }
.store(in: &Self.combinePool)
DistributedNotificationCenter.default()
.publisher(for: .init(rawValue: "com.apple.screensaver.didstop"))
.sink { _ in NSWorkspace.activationFlags.remove(.screenSaverRunning) }
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.willSleepNotification)
.sink { _ in NSWorkspace.activationFlags.insert(.hibernating) }
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.didWakeNotification)
.sink { _ in NSWorkspace.activationFlags.remove(.hibernating) }
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.sessionDidResignActiveNotification)
.sink { _ in NSWorkspace.activationFlags.insert(.sessionSwitchedOut) }
.store(in: &Self.combinePool)
NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.sessionDidBecomeActiveNotification)
.sink { _ in NSWorkspace.activationFlags.remove(.sessionSwitchedOut) }
.store(in: &Self.combinePool)
} else {
Self.combinePoolCocoa.append(
DistributedNotificationCenter.default()
.addObserver(forName: .init("com.apple.screenIsLocked"), object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.insert(.desktopLocked)
}
)
Self.combinePoolCocoa.append(
DistributedNotificationCenter.default()
.addObserver(forName: .init("com.apple.screenIsUnlocked"), object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.remove(.desktopLocked)
}
)
Self.combinePoolCocoa.append(
DistributedNotificationCenter.default()
.addObserver(forName: .init("com.apple.screensaver.didstart"), object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.insert(.screenSaverRunning)
}
)
Self.combinePoolCocoa.append(
DistributedNotificationCenter.default()
.addObserver(forName: .init("com.apple.screensaver.didstop"), object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.remove(.screenSaverRunning)
}
)
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.insert(.hibernating)
}
)
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.remove(.hibernating)
}
)
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.sessionDidResignActiveNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.insert(.sessionSwitchedOut)
}
)
Self.combinePoolCocoa.append(
NSWorkspace.shared.notificationCenter
.addObserver(forName: NSWorkspace.sessionDidBecomeActiveNotification, object: nil, queue: .main) { _ in
NSWorkspace.activationFlags.remove(.sessionSwitchedOut)
}
)
}
}
}
@ShikiSuen
Copy link
Author

ShikiSuen commented Oct 21, 2023

My copy of this article in Juejin: https://juejin.cn/post/7291937079877304359

The code above is rewritten using IOKit in order to make sure the investigation result is accurate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment