diff --git a/ee/presencedetection/presencedetection.go b/ee/presencedetection/presencedetection.go index 7a77d1042..394f3aae9 100644 --- a/ee/presencedetection/presencedetection.go +++ b/ee/presencedetection/presencedetection.go @@ -6,7 +6,10 @@ import ( "time" ) -const DetectionFailedDurationValue = -1 * time.Second +const ( + DetectionFailedDurationValue = -1 * time.Second + DetectionTimeout = 1 * time.Minute +) type PresenceDetector struct { lastDetection time.Time diff --git a/ee/presencedetection/presencedetection_other.go b/ee/presencedetection/presencedetection_linux.go similarity index 85% rename from ee/presencedetection/presencedetection_other.go rename to ee/presencedetection/presencedetection_linux.go index bcb742bf0..023ea8268 100644 --- a/ee/presencedetection/presencedetection_other.go +++ b/ee/presencedetection/presencedetection_linux.go @@ -1,5 +1,5 @@ -//go:build !darwin -// +build !darwin +//go:build linux +// +build linux package presencedetection diff --git a/ee/presencedetection/presencedetection_windows.go b/ee/presencedetection/presencedetection_windows.go new file mode 100644 index 000000000..5c18ee290 --- /dev/null +++ b/ee/presencedetection/presencedetection_windows.go @@ -0,0 +1,178 @@ +//go:build windows +// +build windows + +package presencedetection + +import ( + "errors" + "fmt" + "sync" + "syscall" + "time" + "unsafe" + + ole "github.com/go-ole/go-ole" + "github.com/kolide/systray" + "github.com/saltosystems/winrt-go" + "github.com/saltosystems/winrt-go/windows/foundation" +) + +// GUIDs retrieved from: +// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/um/UserConsentVerifierInterop.idl +var ( + iUserConsentVerifierStaticsGuid = ole.NewGUID("AF4F3F91-564C-4DDC-B8B5-973447627C65") // Windows.Security.Credentials.UI.UserConsentVerifier + iUserConsentVerifierInteropGuid = ole.NewGUID("39E050C3-4E74-441A-8DC0-B81104DF949C") // UserConsentVerifierInterop +) + +// Signatures were generated following the guidance in +// https://learn.microsoft.com/en-us/uwp/winrt-cref/winrt-type-system#guid-generation-for-parameterized-types. +const ( + userConsentVerificationResultSignature = "enum(Windows.Security.Credentials.UI.UserConsentVerificationResult;i4)" // i4 is underlying type of int32 +) + +// Values for result come from https://learn.microsoft.com/en-us/uwp/api/windows.security.credentials.ui.userconsentverificationresult?view=winrt-26100 +const ( + resultVerified uintptr = 0x0 + resultDeviceNotPresent uintptr = 0x1 + resultNotConfiguredForUser uintptr = 0x2 + resultDisabledByPolicy uintptr = 0x3 + resultDeviceBusy uintptr = 0x4 + resultRetriesExhausted uintptr = 0x5 + resultCanceled uintptr = 0x6 +) + +var resultErrorMessageMap = map[uintptr]string{ + resultDeviceNotPresent: "There is no authentication device available.", + resultNotConfiguredForUser: "An authentication verifier device is not configured for this user.", + resultDisabledByPolicy: "Group policy has disabled authentication device verification.", + resultDeviceBusy: "The authentication device is performing an operation and is unavailable.", + resultRetriesExhausted: "After 10 attempts, the original verification request and all subsequent attempts at the same verification were not verified.", + resultCanceled: "The verification operation was canceled.", +} + +// IUserConsentVerifierInterop is the interop interface for UserConsentVerifier. Both are documented here: +// https://learn.microsoft.com/en-us/uwp/api/windows.security.credentials.ui.userconsentverifier?view=winrt-26100#desktop-apps-using-cwinrt +type IUserConsentVerifierInterop struct { + ole.IInspectable +} + +func (v *IUserConsentVerifierInterop) VTable() *IUserConsentVerifierInteropVTable { + return (*IUserConsentVerifierInteropVTable)(unsafe.Pointer(v.RawVTable)) +} + +type IUserConsentVerifierInteropVTable struct { + ole.IInspectableVtbl + RequestVerificationForWindowAsync uintptr +} + +var roInitialize = sync.OnceFunc(func() { + // Call ole.RoInitialize(1) only once + ole.RoInitialize(1) +}) + +// Detect prompts the user via Hello. +func Detect(reason string) (bool, error) { + roInitialize() + + if err := requestVerification(reason); err != nil { + return false, fmt.Errorf("requesting verification: %w", err) + } + + return true, nil +} + +// requestVerification calls Windows.Security.Credentials.UI.UserConsentVerifier.RequestVerificationAsync via the interop interface. +// See: https://learn.microsoft.com/en-us/windows/win32/api/userconsentverifierinterop/nf-userconsentverifierinterop-iuserconsentverifierinterop-requestverificationforwindowasync +func requestVerification(reason string) error { + // Get access to UserConsentVerifier via factory + factory, err := ole.RoGetActivationFactory("Windows.Security.Credentials.UI.UserConsentVerifier", iUserConsentVerifierStaticsGuid) + if err != nil { + return fmt.Errorf("getting activation factory for UserConsentVerifier: %w", err) + } + defer factory.Release() + + // Query for the interop interface, which we need to actually interact with this method + verifierObj, err := factory.QueryInterface(iUserConsentVerifierInteropGuid) + if err != nil { + return fmt.Errorf("getting UserConsentVerifier from factory: %w", err) + } + defer verifierObj.Release() + verifier := (*IUserConsentVerifierInterop)(unsafe.Pointer(verifierObj)) + + // Get the current window handle (a HWND) from systray + windowHandle, err := systray.WindowHandle() + if err != nil { + return fmt.Errorf("getting current window handle: %w", err) + } + + // Create hstring for "reason" message + reasonHString, err := ole.NewHString(reason) + if err != nil { + return fmt.Errorf("creating reason hstring: %w", err) + } + defer ole.DeleteHString(reasonHString) + + // RequestVerificationForWindowAsync returns Windows.Foundation.IAsyncOperation + // -- prepare the return values. + refiid := winrt.ParameterizedInstanceGUID(foundation.GUIDIAsyncOperation, userConsentVerificationResultSignature) + var requestVerificationAsyncOperation *foundation.IAsyncOperation + + // In a lot of places, when passing HSTRINGs into `syscall.SyscallN`, we see it passed in + // as `uintptr(unsafe.Pointer(&reasonHString))`. However, this does not work for + // RequestVerificationForWindowAsync -- the window displays an incorrectly-encoded message. + // + // We CAN pass the message in as `uintptr(unsafe.Pointer(reasonHString))` -- this works, and + // is a choice that the ole library makes in several places (see RoActivateInstance, + // RoGetActiviationFactory). However, Golang's unsafeptr analysis flags this as potentially + // unsafe use of a pointer. + // + // To avoid the unsafeptr warning, we therefore pass in the message as simply `uintptr(reasonHString)`. + // This is safe and effective. + + requestVerificationReturn, _, _ := syscall.SyscallN( + verifier.VTable().RequestVerificationForWindowAsync, + uintptr(unsafe.Pointer(verifier)), // Reference to our interop + uintptr(windowHandle), // HWND to our application's window + uintptr(reasonHString), // The message to include in the verification request + uintptr(unsafe.Pointer(ole.NewGUID(refiid))), // REFIID -- reference to the interface identifier for the return value (below) + uintptr(unsafe.Pointer(&requestVerificationAsyncOperation)), // Return value -- Windows.Foundation.IAsyncOperation + ) + if requestVerificationReturn != 0 { + return fmt.Errorf("calling RequestVerificationForWindowAsync: %w", ole.NewError(requestVerificationReturn)) + } + + // Wait for async operation to complete + iid := winrt.ParameterizedInstanceGUID(foundation.GUIDAsyncOperationCompletedHandler, userConsentVerificationResultSignature) + statusChan := make(chan foundation.AsyncStatus) + handler := foundation.NewAsyncOperationCompletedHandler(ole.NewGUID(iid), func(instance *foundation.AsyncOperationCompletedHandler, asyncInfo *foundation.IAsyncOperation, asyncStatus foundation.AsyncStatus) { + statusChan <- asyncStatus + }) + defer handler.Release() + requestVerificationAsyncOperation.SetCompleted(handler) + + select { + case operationStatus := <-statusChan: + if operationStatus != foundation.AsyncStatusCompleted { + return fmt.Errorf("RequestVerificationForWindowAsync operation did not complete: status %d", operationStatus) + } + case <-time.After(DetectionTimeout): + return errors.New("timed out waiting for RequestVerificationForWindowAsync operation to complete") + } + + // Retrieve the results from the async operation + resPtr, err := requestVerificationAsyncOperation.GetResults() + if err != nil { + return fmt.Errorf("getting results of RequestVerificationForWindowAsync: %w", err) + } + + verificationResult := uintptr(resPtr) + if verificationResult == resultVerified { + return nil + } + + if errMsg, ok := resultErrorMessageMap[verificationResult]; ok { + return fmt.Errorf("requesting verification failed: %s", errMsg) + } + + return fmt.Errorf("RequestVerificationForWindowAsync failed with unknown result %+v", verificationResult) +} diff --git a/go.mod b/go.mod index 68293242b..51dff27f0 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/mixer/clock v0.0.0-20170901150240-b08e6b4da7ea github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/osquery/osquery-go v0.0.0-20231130195733-61ac79279aaa - github.com/peterbourgon/ff/v3 v3.0.0 + github.com/peterbourgon/ff/v3 v3.1.2 github.com/pkg/errors v0.9.1 github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516 @@ -51,8 +51,9 @@ require ( github.com/golang-migrate/migrate/v4 v4.16.2 github.com/golang/snappy v0.0.4 github.com/kolide/goleveldb v0.0.0-20240514204455-8d30cd4d31c6 - github.com/kolide/systray v1.10.5-0.20241011144003-35bc09a9664f + github.com/kolide/systray v1.10.5-0.20241015212527-e67ebef13666 github.com/kolide/toast v1.0.2 + github.com/saltosystems/winrt-go v0.0.0-20240510082706-db61b37f5877 github.com/shirou/gopsutil/v3 v3.23.3 github.com/spf13/pflag v1.0.5 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 @@ -92,13 +93,12 @@ require ( github.com/BurntSushi/toml v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logfmt/logfmt v0.4.0 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-tpm v0.3.3 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect - github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 4a0e7a3ff..274b83a08 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,9 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -177,12 +178,11 @@ github.com/kolide/kit v0.0.0-20240411131714-94dd1939cf50 h1:N7RaYBPTK5o4y2z1z8kl github.com/kolide/kit v0.0.0-20240411131714-94dd1939cf50/go.mod h1:pFbEKXFww1uqu4RRO7qCnUmQ2EIwKYRzUqpJbODNlfc= github.com/kolide/krypto v0.1.1-0.20231229162826-db516b7e0121 h1:f7APX9VNsCkD/tdlAjbU4A22FyfTOCF6QadlvnzZElg= github.com/kolide/krypto v0.1.1-0.20231229162826-db516b7e0121/go.mod h1:/0sxd3OIxciTlMTeZI/9WTaUHsx/K/+3f+NbD5dywTY= -github.com/kolide/systray v1.10.5-0.20241011144003-35bc09a9664f h1:v3y9fZGNWLyX21t4Oh0J/mR0AUD2IT8XdSMEz7727+w= -github.com/kolide/systray v1.10.5-0.20241011144003-35bc09a9664f/go.mod h1:FwK9yUmU3JO+vA7TOLQSFRgEQ3euLxOqic5qlBtFrik= +github.com/kolide/systray v1.10.5-0.20241015212527-e67ebef13666 h1:Vyr01yyC8ju6wVvNj1MrxSbyJTFp+489Z7N4kPh8eLw= +github.com/kolide/systray v1.10.5-0.20241015212527-e67ebef13666/go.mod h1:FwK9yUmU3JO+vA7TOLQSFRgEQ3euLxOqic5qlBtFrik= github.com/kolide/toast v1.0.2 h1:BQlIfO3wbKIEWfF0c8v4UkdhSIZYnSWaKkZl+Yarptk= github.com/kolide/toast v1.0.2/go.mod h1:OguLiOUf57YSEuZqjfk4uP4KdT0QOblGoySOI8F1I0Y= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -231,8 +231,8 @@ github.com/osquery/osquery-go v0.0.0-20231130195733-61ac79279aaa h1:bDsjvyU27AQG github.com/osquery/osquery-go v0.0.0-20231130195733-61ac79279aaa/go.mod h1:mLJRc1Go8uP32LRALGvWj2lVJ+hDYyIfxDzVa+C5Yo8= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= -github.com/peterbourgon/ff/v3 v3.0.0 h1:eQzEmNahuOjQXfuegsKQTSTDbf4dNvr/eNLrmJhiH7M= -github.com/peterbourgon/ff/v3 v3.0.0/go.mod h1:UILIFjRH5a/ar8TjXYLTkIvSvekZqPm5Eb/qbGk6CT0= +github.com/peterbourgon/ff/v3 v3.1.2 h1:0GNhbRhO9yHA4CC27ymskOsuRpmX0YQxwxM9UPiP6JM= +github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -258,6 +258,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/saltosystems/winrt-go v0.0.0-20240510082706-db61b37f5877 h1:h+mGFGCgqpe2xqFpYtXSqDg3uJ1nYugFb5VQhTHvyL4= +github.com/saltosystems/winrt-go v0.0.0-20240510082706-db61b37f5877/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/samber/slog-multi v1.0.2 h1:6BVH9uHGAsiGkbbtQgAOQJMpKgV8unMrHhhJaw+X1EQ=