Skip to content

Commit

Permalink
Perform presence detection on Windows using UserConsentVerifier inter…
Browse files Browse the repository at this point in the history
…op (#1890)
  • Loading branch information
RebeccaMahany authored Oct 16, 2024
1 parent 5027715 commit fb83c23
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 13 deletions.
5 changes: 4 additions & 1 deletion ee/presencedetection/presencedetection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//go:build !darwin
// +build !darwin
//go:build linux
// +build linux

package presencedetection

Expand Down
178 changes: 178 additions & 0 deletions ee/presencedetection/presencedetection_windows.go
Original file line number Diff line number Diff line change
@@ -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<UserConsentVerificationResult>
// -- 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<UserConsentVerificationResult>
)
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)
}
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down

0 comments on commit fb83c23

Please sign in to comment.