Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perform presence detection on Windows using UserConsentVerifier interop #1890

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
31506a0
Proof-of-concept for Windows Hello
RebeccaMahany Sep 12, 2024
c62471b
Merge remote-tracking branch 'upstream/main' into becca/windows-hello
RebeccaMahany Sep 13, 2024
c9e9fe9
Remove debug call
RebeccaMahany Sep 23, 2024
4a8e67b
Rename files and function signature to match interface
RebeccaMahany Sep 23, 2024
c6ad847
Merge remote-tracking branch 'upstream/main' into becca/windows-hello
RebeccaMahany Sep 23, 2024
109d1ed
Add documentation, comment out unused func
RebeccaMahany Sep 23, 2024
9a3b793
Refactor and split up into Register + Detect
RebeccaMahany Sep 23, 2024
1fa02bf
Simplify
RebeccaMahany Sep 23, 2024
262fce2
Call initialize only once, set up manager reference fresh each time
RebeccaMahany Sep 23, 2024
64b0d22
Strip down to just the working parts
RebeccaMahany Sep 23, 2024
00c8c6d
Try RequestVerificationAsync
RebeccaMahany Sep 23, 2024
356e03a
Create window via win32 api
RebeccaMahany Sep 23, 2024
459d98b
Fix call to GetModuleHandleW
RebeccaMahany Sep 23, 2024
86d7c4b
Call RequestVerificationForWindowAsync
RebeccaMahany Oct 14, 2024
d54a300
Tidy, document
RebeccaMahany Oct 14, 2024
7d9d2cd
Merge remote-tracking branch 'upstream/main' into becca/windows-user-…
RebeccaMahany Oct 14, 2024
6623056
Get REFIID correct, finally
RebeccaMahany Oct 14, 2024
dcb933c
Call ShowWindow
RebeccaMahany Oct 14, 2024
378e5d0
Get window handle from systray
RebeccaMahany Oct 15, 2024
aba1700
Merge remote-tracking branch 'upstream/main' into becca/windows-user-…
RebeccaMahany Oct 15, 2024
bf6c76e
Rename file
RebeccaMahany Oct 16, 2024
e0bf952
Improve documentation
RebeccaMahany Oct 16, 2024
d9360b2
Merge remote-tracking branch 'upstream/main' into becca/windows-user-…
RebeccaMahany Oct 16, 2024
4d4064e
Another docs update
RebeccaMahany Oct 16, 2024
058dd0a
Fix encoding for message
RebeccaMahany Oct 16, 2024
1b31d53
Add error messages
RebeccaMahany Oct 16, 2024
1d47791
Rename variable for clarity
RebeccaMahany Oct 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
directionless marked this conversation as resolved.
Show resolved Hide resolved
"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
Loading