Last active
May 30, 2026 21:41
-
-
Save CherryDT/629bcccba08746c1341cd4e46e23dfbc to your computer and use it in GitHub Desktop.
PoC code for testing Windows Hello for Keyring
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //go:build windows | |
| // +build windows | |
| package main | |
| import ( | |
| "bytes" | |
| "crypto/rand" | |
| "encoding/base64" | |
| "encoding/json" | |
| "errors" | |
| "flag" | |
| "fmt" | |
| "log" | |
| "os" | |
| "os/user" | |
| "path/filepath" | |
| "strings" | |
| "unsafe" | |
| "golang.org/x/sys/windows" | |
| ) | |
| const ( | |
| passportKSPName = "Microsoft Passport Key Storage Provider" | |
| defaultKeyName = "keyring-winhello-poc" | |
| ncryptRSAAlgorithm = "RSA" | |
| bcryptSHA1Alg = "SHA1" | |
| bcryptSHA256Alg = "SHA256" | |
| ncryptLengthProperty = "Length" | |
| ncryptKeyUsageProperty = "Key Usage" | |
| ncryptNgcCacheTypeProperty = "NgcCacheType" | |
| ncryptNgcCacheTypeLegacyProperty = "NgcCacheTypeProperty" | |
| ncryptPinCacheIsGestureRequiredProperty = "PinCacheIsGestureRequired" | |
| ncryptUseContextProperty = "Use Context" | |
| ncryptWindowHandleProperty = "HWND Handle" | |
| ncryptPadPKCS1Flag = 0x00000002 | |
| ncryptPadOAEPFlag = 0x00000004 | |
| ncryptSilentFlag = 0x00000040 | |
| ncryptAllowDecryptFlag = 0x00000001 | |
| ncryptAllowSigningFlag = 0x00000002 | |
| ngcCacheTypeAuthMandatory = 0x00000001 | |
| passportKeyNameDomain = "ByteNess" | |
| passportKeyNameSubdomain = "keyring-winhello" | |
| createKeyUseContext = "Create keyring winhello PoC key" | |
| ) | |
| const ( | |
| nteBadKeyset windows.Errno = 0x80090016 | |
| nteNoKey windows.Errno = 0x8009000d | |
| nteNotFound windows.Errno = 0x80090011 | |
| nteInvalidParameter windows.Errno = 0x80090027 | |
| win32InvalidParameter windows.Errno = 87 | |
| ) | |
| var ( | |
| ncryptDLL = windows.NewLazySystemDLL("ncrypt.dll") | |
| user32DLL = windows.NewLazySystemDLL("user32.dll") | |
| kernelDLL = windows.NewLazySystemDLL("kernel32.dll") | |
| procNCryptOpenStorageProvider = ncryptDLL.NewProc("NCryptOpenStorageProvider") | |
| procNCryptOpenKey = ncryptDLL.NewProc("NCryptOpenKey") | |
| procNCryptCreatePersistedKey = ncryptDLL.NewProc("NCryptCreatePersistedKey") | |
| procNCryptFinalizeKey = ncryptDLL.NewProc("NCryptFinalizeKey") | |
| procNCryptSetProperty = ncryptDLL.NewProc("NCryptSetProperty") | |
| procNCryptEncrypt = ncryptDLL.NewProc("NCryptEncrypt") | |
| procNCryptDecrypt = ncryptDLL.NewProc("NCryptDecrypt") | |
| procNCryptDeleteKey = ncryptDLL.NewProc("NCryptDeleteKey") | |
| procNCryptFreeObject = ncryptDLL.NewProc("NCryptFreeObject") | |
| procGetForegroundWindow = user32DLL.NewProc("GetForegroundWindow") | |
| procIsWindowVisible = user32DLL.NewProc("IsWindowVisible") | |
| procGetConsoleWindow = kernelDLL.NewProc("GetConsoleWindow") | |
| ) | |
| type ncryptHandle uintptr | |
| type bcryptOAEPPaddingInfo struct { | |
| AlgID *uint16 | |
| Label *byte | |
| LabelCB uint32 | |
| } | |
| type options struct { | |
| keyName string | |
| useContext string | |
| stateFile string | |
| padding string | |
| cleanupOnly bool | |
| deleteAfter bool | |
| recreate bool | |
| createOnly bool | |
| unwrapOnly bool | |
| silentUnwrap bool | |
| requireGesture bool | |
| } | |
| type persistedState struct { | |
| LogicalKeyName string `json:"logical_key_name"` | |
| PassportKeyName string `json:"passport_key_name"` | |
| WrapAlg string `json:"wrap_alg"` | |
| WrappedCEKBase64 string `json:"wrapped_cek"` | |
| } | |
| type rsaPaddingMode struct { | |
| name string | |
| hashAlg string | |
| flag uint32 | |
| paddingInfo func() (unsafe.Pointer, error) | |
| } | |
| var rsaPaddingModes = []rsaPaddingMode{ | |
| { | |
| name: "RSA-OAEP-SHA256", | |
| hashAlg: bcryptSHA256Alg, | |
| flag: ncryptPadOAEPFlag, | |
| paddingInfo: func() (unsafe.Pointer, error) { | |
| sha256UTF16, err := windows.UTF16PtrFromString(bcryptSHA256Alg) | |
| if err != nil { | |
| return nil, fmt.Errorf("encode hash algorithm %q: %w", bcryptSHA256Alg, err) | |
| } | |
| paddingInfo := &bcryptOAEPPaddingInfo{ | |
| AlgID: sha256UTF16, | |
| Label: nil, | |
| LabelCB: 0, | |
| } | |
| return unsafe.Pointer(paddingInfo), nil | |
| }, | |
| }, | |
| { | |
| name: "RSA-OAEP-SHA1", | |
| hashAlg: bcryptSHA1Alg, | |
| flag: ncryptPadOAEPFlag, | |
| paddingInfo: func() (unsafe.Pointer, error) { | |
| sha1UTF16, err := windows.UTF16PtrFromString(bcryptSHA1Alg) | |
| if err != nil { | |
| return nil, fmt.Errorf("encode hash algorithm %q: %w", bcryptSHA1Alg, err) | |
| } | |
| paddingInfo := &bcryptOAEPPaddingInfo{ | |
| AlgID: sha1UTF16, | |
| Label: nil, | |
| LabelCB: 0, | |
| } | |
| return unsafe.Pointer(paddingInfo), nil | |
| }, | |
| }, | |
| { | |
| name: "RSA-PKCS1-v1_5", | |
| hashAlg: "", | |
| flag: ncryptPadPKCS1Flag, | |
| paddingInfo: func() (unsafe.Pointer, error) { | |
| return nil, nil | |
| }, | |
| }, | |
| } | |
| var rsaPaddingModeByName = map[string]rsaPaddingMode{} | |
| func init() { | |
| for _, mode := range rsaPaddingModes { | |
| rsaPaddingModeByName[normalizePaddingName(mode.name)] = mode | |
| } | |
| // Friendly aliases for CLI use. | |
| rsaPaddingModeByName["oaep-sha256"] = rsaPaddingModes[0] | |
| rsaPaddingModeByName["rsaes-oaep-sha256"] = rsaPaddingModes[0] | |
| rsaPaddingModeByName["oaep-sha1"] = rsaPaddingModes[1] | |
| rsaPaddingModeByName["rsaes-oaep-sha1"] = rsaPaddingModes[1] | |
| rsaPaddingModeByName["pkcs1v15"] = rsaPaddingModes[2] | |
| rsaPaddingModeByName["pkcs1-v1_5"] = rsaPaddingModes[2] | |
| rsaPaddingModeByName["pkcs1-v1.5"] = rsaPaddingModes[2] | |
| rsaPaddingModeByName["rsa-pkcs1-v1_5"] = rsaPaddingModes[2] | |
| rsaPaddingModeByName["rsaes-pkcs1-v1_5"] = rsaPaddingModes[2] | |
| } | |
| func main() { | |
| opts := parseFlags() | |
| if err := run(opts); err != nil { | |
| log.Fatal(err) | |
| } | |
| } | |
| func parseFlags() options { | |
| opts := options{} | |
| flag.StringVar(&opts.keyName, "key-name", defaultKeyName, "Persistent key name for this PoC") | |
| flag.StringVar(&opts.useContext, "context", "Unlock keyring secret for winhello PoC", "Use context shown by Windows Hello") | |
| flag.StringVar(&opts.stateFile, "state-file", filepath.Join("_local", "winhello-poc-state.json"), "Path to persisted wrapped-state file for create-only/unwrap-only tests") | |
| flag.StringVar(&opts.padding, "padding", "auto", "Padding mode: auto|oaep-sha256|oaep-sha1|pkcs1v15") | |
| flag.BoolVar(&opts.cleanupOnly, "cleanup", false, "Delete the PoC key and exit") | |
| flag.BoolVar(&opts.deleteAfter, "delete-after", false, "Delete the PoC key after a successful roundtrip") | |
| flag.BoolVar(&opts.recreate, "recreate", false, "Delete and recreate the PoC key before running") | |
| flag.BoolVar(&opts.createOnly, "create-only", false, "Create/reuse the key, wrap a CEK, persist wrapped state, and exit without unwrap") | |
| flag.BoolVar(&opts.unwrapOnly, "unwrap-only", false, "Read persisted wrapped state and only perform unwrap") | |
| flag.BoolVar(&opts.silentUnwrap, "silent-unwrap", false, "Request silent unwrap with NCRYPT_SILENT_FLAG; should fail if UI is required") | |
| flag.BoolVar(&opts.requireGesture, "require-gesture", true, "Set PinCacheIsGestureRequired and Use Context before unwrap") | |
| flag.Parse() | |
| if opts.createOnly && opts.unwrapOnly { | |
| log.Fatal("--create-only and --unwrap-only are mutually exclusive") | |
| } | |
| return opts | |
| } | |
| func run(opts options) error { | |
| provider, err := openStorageProvider(passportKSPName) | |
| if err != nil { | |
| return fmt.Errorf("open Passport KSP %q: %w", passportKSPName, err) | |
| } | |
| defer freeHandle(provider) | |
| if err := setBestEffortWindowHandle(provider); err != nil { | |
| log.Printf("warning: unable to set NCrypt parent window handle on provider: %v", err) | |
| } | |
| passportKeyName, err := passportPersistentKeyName(opts.keyName) | |
| if err != nil { | |
| return err | |
| } | |
| fmt.Printf("logical key name: %s\n", opts.keyName) | |
| fmt.Printf("passport key name: %s\n", passportKeyName) | |
| if opts.cleanupOnly { | |
| deleted, err := deleteKeyByName(provider, passportKeyName) | |
| if err != nil { | |
| return err | |
| } | |
| if deleted { | |
| fmt.Printf("deleted key %q\n", passportKeyName) | |
| } else { | |
| fmt.Printf("key %q not found\n", passportKeyName) | |
| } | |
| if err := removeStateFile(opts.stateFile); err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| if opts.unwrapOnly { | |
| return runUnwrapOnly(provider, passportKeyName, opts) | |
| } | |
| if opts.recreate { | |
| deleted, err := deleteKeyByName(provider, passportKeyName) | |
| if err != nil { | |
| return err | |
| } | |
| if deleted { | |
| fmt.Printf("deleted existing key %q\n", passportKeyName) | |
| } | |
| } | |
| key, created, err := openOrCreateKey(provider, passportKeyName) | |
| if err != nil { | |
| return err | |
| } | |
| if created { | |
| fmt.Printf("created persistent key %q\n", passportKeyName) | |
| } else { | |
| fmt.Printf("reused persistent key %q\n", passportKeyName) | |
| } | |
| cek := make([]byte, 32) | |
| if _, err := rand.Read(cek); err != nil { | |
| freeHandle(key) | |
| return fmt.Errorf("generate random 32-byte CEK: %w", err) | |
| } | |
| var ( | |
| wrapped []byte | |
| padding rsaPaddingMode | |
| ) | |
| if opts.padding == "auto" { | |
| wrapped, padding, err = rsaEncryptWithFallback(key, cek) | |
| } else { | |
| wrapped, padding, err = rsaEncryptWithSelectedMode(key, cek, opts.padding) | |
| } | |
| freeHandle(key) | |
| if err != nil { | |
| return fmt.Errorf("wrap CEK using public-key operation: %w", err) | |
| } | |
| fmt.Printf("wrap mode: %s\n", padding.name) | |
| if opts.createOnly { | |
| state := persistedState{ | |
| LogicalKeyName: opts.keyName, | |
| PassportKeyName: passportKeyName, | |
| WrapAlg: padding.name, | |
| WrappedCEKBase64: base64.StdEncoding.EncodeToString(wrapped), | |
| } | |
| if err := writePersistedState(opts.stateFile, state); err != nil { | |
| return err | |
| } | |
| fmt.Printf("persisted wrapped state to %s\n", opts.stateFile) | |
| return nil | |
| } | |
| reopened, err := openKey(provider, passportKeyName) | |
| if err != nil { | |
| return fmt.Errorf("reopen key %q: %w", passportKeyName, err) | |
| } | |
| defer freeHandle(reopened) | |
| if err := prepareDecrypt(reopened, opts.useContext, opts.requireGesture); err != nil { | |
| return fmt.Errorf("prepare Windows Hello decrypt policy: %w", err) | |
| } | |
| unwrapped, err := rsaDecryptWithModeAndFlags(reopened, wrapped, padding, decryptFlags(opts.silentUnwrap)) | |
| if err != nil { | |
| return fmt.Errorf("unwrap CEK using private-key operation: %w", err) | |
| } | |
| if !bytes.Equal(cek, unwrapped) { | |
| return fmt.Errorf("roundtrip mismatch: original and unwrapped CEKs differ") | |
| } | |
| fmt.Printf("roundtrip ok (wrapped=%d bytes)\n", len(wrapped)) | |
| fmt.Println("If your policy requires a gesture, Windows Hello should appear during unwrap.") | |
| if opts.deleteAfter { | |
| freeHandle(reopened) | |
| reopened = 0 | |
| deleted, err := deleteKeyByName(provider, passportKeyName) | |
| if err != nil { | |
| return err | |
| } | |
| if deleted { | |
| fmt.Printf("deleted key %q\n", passportKeyName) | |
| } | |
| } | |
| return nil | |
| } | |
| func runUnwrapOnly(provider ncryptHandle, passportKeyName string, opts options) error { | |
| state, err := readPersistedState(opts.stateFile) | |
| if err != nil { | |
| return err | |
| } | |
| if state.PassportKeyName != "" && state.PassportKeyName != passportKeyName { | |
| log.Printf("warning: state file passport key name %q differs from current derived name %q", state.PassportKeyName, passportKeyName) | |
| } | |
| padding, err := lookupPaddingModeByName(state.WrapAlg) | |
| if err != nil { | |
| return fmt.Errorf("state file wrap_alg %q: %w", state.WrapAlg, err) | |
| } | |
| wrapped, err := base64.StdEncoding.DecodeString(state.WrappedCEKBase64) | |
| if err != nil { | |
| return fmt.Errorf("decode wrapped CEK from %s: %w", opts.stateFile, err) | |
| } | |
| key, err := openKey(provider, passportKeyName) | |
| if err != nil { | |
| return fmt.Errorf("open key %q for unwrap-only: %w", passportKeyName, err) | |
| } | |
| defer freeHandle(key) | |
| if err := prepareDecrypt(key, opts.useContext, opts.requireGesture); err != nil { | |
| return fmt.Errorf("prepare unwrap-only decrypt policy: %w", err) | |
| } | |
| plaintext, err := rsaDecryptWithModeAndFlags(key, wrapped, padding, decryptFlags(opts.silentUnwrap)) | |
| if err != nil { | |
| return fmt.Errorf("unwrap-only decrypt using %s: %w", padding.name, err) | |
| } | |
| if len(plaintext) != 32 { | |
| return fmt.Errorf("unwrap-only plaintext length = %d, want 32", len(plaintext)) | |
| } | |
| fmt.Printf("unwrap-only ok (mode=%s, silent=%t, require_gesture=%t, plaintext_len=%d)\n", padding.name, opts.silentUnwrap, opts.requireGesture, len(plaintext)) | |
| return nil | |
| } | |
| func openStorageProvider(providerName string) (ncryptHandle, error) { | |
| providerNameUTF16, err := windows.UTF16PtrFromString(providerName) | |
| if err != nil { | |
| return 0, fmt.Errorf("encode provider name: %w", err) | |
| } | |
| var provider ncryptHandle | |
| if err := callNCrypt( | |
| procNCryptOpenStorageProvider, | |
| uintptr(unsafe.Pointer(&provider)), | |
| uintptr(unsafe.Pointer(providerNameUTF16)), | |
| 0, | |
| ); err != nil { | |
| return 0, err | |
| } | |
| return provider, nil | |
| } | |
| func openOrCreateKey(provider ncryptHandle, keyName string) (ncryptHandle, bool, error) { | |
| key, err := openKey(provider, keyName) | |
| if err == nil { | |
| return key, false, nil | |
| } | |
| if isInvalidParameterError(err) { | |
| return 0, false, fmt.Errorf("open key %q: Passport KSP rejected the key identity or parameters: %w", keyName, err) | |
| } | |
| if !isKeyNotFoundError(err) { | |
| return 0, false, fmt.Errorf("open key %q: %w", keyName, err) | |
| } | |
| key, err = createPersistedRSAKey(provider, keyName) | |
| if err != nil { | |
| return 0, false, err | |
| } | |
| return key, true, nil | |
| } | |
| func createPersistedRSAKey(provider ncryptHandle, keyName string) (ncryptHandle, error) { | |
| algorithmUTF16, err := windows.UTF16PtrFromString(ncryptRSAAlgorithm) | |
| if err != nil { | |
| return 0, fmt.Errorf("encode algorithm %q: %w", ncryptRSAAlgorithm, err) | |
| } | |
| keyNameUTF16, err := windows.UTF16PtrFromString(keyName) | |
| if err != nil { | |
| return 0, fmt.Errorf("encode key name: %w", err) | |
| } | |
| key, err := createPersistedKeyWithSpec(provider, algorithmUTF16, keyNameUTF16, 0) | |
| if err != nil { | |
| return 0, fmt.Errorf("create persisted key %q: %w", keyName, err) | |
| } | |
| if err := setUint32Property(key, ncryptLengthProperty, 2048); err != nil { | |
| freeHandle(key) | |
| return 0, fmt.Errorf("set key length property: %w", err) | |
| } | |
| if err := setKeyUsage(key); err != nil { | |
| freeHandle(key) | |
| return 0, fmt.Errorf("set key usage property: %w", err) | |
| } | |
| if err := setNgcCacheType(key); err != nil { | |
| freeHandle(key) | |
| return 0, fmt.Errorf("set NGC cache type property: %w", err) | |
| } | |
| if err := setUseContext(key, createKeyUseContext); err != nil { | |
| freeHandle(key) | |
| return 0, fmt.Errorf("set use context before finalize: %w", err) | |
| } | |
| if err := setBestEffortWindowHandle(key); err != nil { | |
| log.Printf("warning: unable to set NCrypt parent window handle on key creation handle: %v", err) | |
| } | |
| if err := callNCrypt(procNCryptFinalizeKey, uintptr(key), 0); err != nil { | |
| freeHandle(key) | |
| return 0, fmt.Errorf("finalize key %q: %w", keyName, err) | |
| } | |
| return key, nil | |
| } | |
| func createPersistedKeyWithSpec(provider ncryptHandle, algorithmUTF16 *uint16, keyNameUTF16 *uint16, legacyKeySpec uint32) (ncryptHandle, error) { | |
| var key ncryptHandle | |
| if err := callNCrypt( | |
| procNCryptCreatePersistedKey, | |
| uintptr(provider), | |
| uintptr(unsafe.Pointer(&key)), | |
| uintptr(unsafe.Pointer(algorithmUTF16)), | |
| uintptr(unsafe.Pointer(keyNameUTF16)), | |
| uintptr(legacyKeySpec), | |
| 0, | |
| ); err != nil { | |
| return 0, err | |
| } | |
| return key, nil | |
| } | |
| func openKey(provider ncryptHandle, keyName string) (ncryptHandle, error) { | |
| keyNameUTF16, err := windows.UTF16PtrFromString(keyName) | |
| if err != nil { | |
| return 0, fmt.Errorf("encode key name: %w", err) | |
| } | |
| var key ncryptHandle | |
| if err := callNCrypt( | |
| procNCryptOpenKey, | |
| uintptr(provider), | |
| uintptr(unsafe.Pointer(&key)), | |
| uintptr(unsafe.Pointer(keyNameUTF16)), | |
| 0, | |
| 0, | |
| ); err != nil { | |
| return 0, err | |
| } | |
| return key, nil | |
| } | |
| func deleteKeyByName(provider ncryptHandle, keyName string) (bool, error) { | |
| key, err := openKey(provider, keyName) | |
| if err != nil { | |
| if isKeyNotFoundError(err) { | |
| return false, nil | |
| } | |
| return false, fmt.Errorf("open key %q for deletion: %w", keyName, err) | |
| } | |
| if err := callNCrypt(procNCryptDeleteKey, uintptr(key), 0); err != nil { | |
| freeHandle(key) | |
| return false, fmt.Errorf("delete key %q: %w", keyName, err) | |
| } | |
| return true, nil | |
| } | |
| func setUseContext(key ncryptHandle, context string) error { | |
| contextUTF16, err := windows.UTF16FromString(context) | |
| if err != nil { | |
| return fmt.Errorf("encode use context: %w", err) | |
| } | |
| contextBytes := unsafe.Slice((*byte)(unsafe.Pointer(&contextUTF16[0])), len(contextUTF16)*2) | |
| return setRawProperty(key, ncryptUseContextProperty, contextBytes, 0) | |
| } | |
| func setKeyUsage(key ncryptHandle) error { | |
| return setUint32Property(key, ncryptKeyUsageProperty, ncryptAllowDecryptFlag|ncryptAllowSigningFlag) | |
| } | |
| func setNgcCacheType(key ncryptHandle) error { | |
| err := setUint32Property(key, ncryptNgcCacheTypeProperty, ngcCacheTypeAuthMandatory) | |
| if err == nil { | |
| return nil | |
| } | |
| legacyErr := setUint32Property(key, ncryptNgcCacheTypeLegacyProperty, ngcCacheTypeAuthMandatory) | |
| if legacyErr == nil { | |
| return nil | |
| } | |
| return fmt.Errorf("%s: %w; %s fallback: %v", ncryptNgcCacheTypeProperty, err, ncryptNgcCacheTypeLegacyProperty, legacyErr) | |
| } | |
| func requireGestureForDecrypt(key ncryptHandle, context string) error { | |
| if err := setBestEffortWindowHandle(key); err != nil { | |
| log.Printf("warning: unable to set NCrypt parent window handle on key decrypt handle: %v", err) | |
| } | |
| if err := setUseContext(key, context); err != nil { | |
| return err | |
| } | |
| return setUint32Property(key, ncryptPinCacheIsGestureRequiredProperty, 1) | |
| } | |
| func prepareDecrypt(key ncryptHandle, context string, requireGesture bool) error { | |
| if requireGesture { | |
| return requireGestureForDecrypt(key, context) | |
| } | |
| if err := setBestEffortWindowHandle(key); err != nil { | |
| log.Printf("warning: unable to set NCrypt parent window handle on key decrypt handle: %v", err) | |
| } | |
| return nil | |
| } | |
| func setUint32Property(key ncryptHandle, property string, value uint32) error { | |
| valueBytes := unsafe.Slice((*byte)(unsafe.Pointer(&value)), int(unsafe.Sizeof(value))) | |
| return setRawProperty(key, property, valueBytes, 0) | |
| } | |
| func setBestEffortWindowHandle(handle ncryptHandle) error { | |
| hwnd := bestEffortWindowHandle() | |
| if hwnd == 0 { | |
| return nil | |
| } | |
| hwndBytes := unsafe.Slice((*byte)(unsafe.Pointer(&hwnd)), int(unsafe.Sizeof(hwnd))) | |
| return setRawProperty(handle, ncryptWindowHandleProperty, hwndBytes, 0) | |
| } | |
| func bestEffortWindowHandle() uintptr { | |
| if hwnd, _, _ := procGetConsoleWindow.Call(); hwnd != 0 && isWindowVisible(hwnd) { | |
| return hwnd | |
| } | |
| hwnd, _, _ := procGetForegroundWindow.Call() | |
| return hwnd | |
| } | |
| func isWindowVisible(hwnd uintptr) bool { | |
| visible, _, _ := procIsWindowVisible.Call(hwnd) | |
| return visible != 0 | |
| } | |
| func setRawProperty(key ncryptHandle, property string, value []byte, flags uint32) error { | |
| propertyUTF16, err := windows.UTF16PtrFromString(property) | |
| if err != nil { | |
| return fmt.Errorf("encode property %q: %w", property, err) | |
| } | |
| var valuePtr uintptr | |
| if len(value) > 0 { | |
| valuePtr = uintptr(unsafe.Pointer(&value[0])) | |
| } | |
| if err := callNCrypt( | |
| procNCryptSetProperty, | |
| uintptr(key), | |
| uintptr(unsafe.Pointer(propertyUTF16)), | |
| valuePtr, | |
| uintptr(uint32(len(value))), | |
| uintptr(flags), | |
| ); err != nil { | |
| return fmt.Errorf("set property %q: %w", property, err) | |
| } | |
| return nil | |
| } | |
| func rsaEncryptWithFallback(key ncryptHandle, plaintext []byte) ([]byte, rsaPaddingMode, error) { | |
| var lastErr error | |
| for _, mode := range rsaPaddingModes { | |
| wrapped, err := rsaEncryptWithMode(key, plaintext, mode) | |
| if err == nil { | |
| return wrapped, mode, nil | |
| } | |
| lastErr = err | |
| log.Printf("warning: encrypt with %s failed: %v", mode.name, err) | |
| } | |
| return nil, rsaPaddingMode{}, fmt.Errorf("all RSA wrap modes failed; last error: %w", lastErr) | |
| } | |
| func rsaEncryptWithSelectedMode(key ncryptHandle, plaintext []byte, modeName string) ([]byte, rsaPaddingMode, error) { | |
| mode, err := lookupPaddingModeByName(modeName) | |
| if err != nil { | |
| return nil, rsaPaddingMode{}, err | |
| } | |
| wrapped, err := rsaEncryptWithMode(key, plaintext, mode) | |
| if err != nil { | |
| return nil, rsaPaddingMode{}, err | |
| } | |
| return wrapped, mode, nil | |
| } | |
| func rsaEncryptWithMode(key ncryptHandle, plaintext []byte, mode rsaPaddingMode) ([]byte, error) { | |
| paddingInfo, err := mode.paddingInfo() | |
| if err != nil { | |
| return nil, err | |
| } | |
| paddingInfoPtr, err := ncryptPaddingInfoPtr(mode.flag, paddingInfo) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var wrappedLen uint32 | |
| if err := callNCrypt( | |
| procNCryptEncrypt, | |
| uintptr(key), | |
| uintptr(unsafe.Pointer(slicePtr(plaintext))), | |
| uintptr(uint32(len(plaintext))), | |
| paddingInfoPtr, | |
| 0, | |
| 0, | |
| uintptr(unsafe.Pointer(&wrappedLen)), | |
| uintptr(mode.flag), | |
| ); err != nil { | |
| return nil, err | |
| } | |
| wrapped := make([]byte, wrappedLen) | |
| if err := callNCrypt( | |
| procNCryptEncrypt, | |
| uintptr(key), | |
| uintptr(unsafe.Pointer(slicePtr(plaintext))), | |
| uintptr(uint32(len(plaintext))), | |
| paddingInfoPtr, | |
| uintptr(unsafe.Pointer(slicePtr(wrapped))), | |
| uintptr(uint32(len(wrapped))), | |
| uintptr(unsafe.Pointer(&wrappedLen)), | |
| uintptr(mode.flag), | |
| ); err != nil { | |
| return nil, err | |
| } | |
| return wrapped[:wrappedLen], nil | |
| } | |
| func rsaDecryptWithMode(key ncryptHandle, ciphertext []byte, mode rsaPaddingMode) ([]byte, error) { | |
| return rsaDecryptWithModeAndFlags(key, ciphertext, mode, 0) | |
| } | |
| func rsaDecryptWithModeAndFlags(key ncryptHandle, ciphertext []byte, mode rsaPaddingMode, extraFlags uint32) ([]byte, error) { | |
| paddingInfo, err := mode.paddingInfo() | |
| if err != nil { | |
| return nil, err | |
| } | |
| paddingInfoPtr, err := ncryptPaddingInfoPtr(mode.flag, paddingInfo) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var plaintextLen uint32 | |
| if err := callNCrypt( | |
| procNCryptDecrypt, | |
| uintptr(key), | |
| uintptr(unsafe.Pointer(slicePtr(ciphertext))), | |
| uintptr(uint32(len(ciphertext))), | |
| paddingInfoPtr, | |
| 0, | |
| 0, | |
| uintptr(unsafe.Pointer(&plaintextLen)), | |
| uintptr(mode.flag | extraFlags), | |
| ); err != nil { | |
| return nil, err | |
| } | |
| plaintext := make([]byte, plaintextLen) | |
| if err := callNCrypt( | |
| procNCryptDecrypt, | |
| uintptr(key), | |
| uintptr(unsafe.Pointer(slicePtr(ciphertext))), | |
| uintptr(uint32(len(ciphertext))), | |
| paddingInfoPtr, | |
| uintptr(unsafe.Pointer(slicePtr(plaintext))), | |
| uintptr(uint32(len(plaintext))), | |
| uintptr(unsafe.Pointer(&plaintextLen)), | |
| uintptr(mode.flag | extraFlags), | |
| ); err != nil { | |
| return nil, err | |
| } | |
| return plaintext[:plaintextLen], nil | |
| } | |
| func decryptFlags(silent bool) uint32 { | |
| if silent { | |
| return ncryptSilentFlag | |
| } | |
| return 0 | |
| } | |
| func lookupPaddingModeByName(name string) (rsaPaddingMode, error) { | |
| mode, ok := rsaPaddingModeByName[normalizePaddingName(name)] | |
| if !ok { | |
| return rsaPaddingMode{}, fmt.Errorf("unsupported padding mode %q", name) | |
| } | |
| return mode, nil | |
| } | |
| func normalizePaddingName(name string) string { | |
| name = strings.TrimSpace(strings.ToLower(name)) | |
| name = strings.ReplaceAll(name, "_", "-") | |
| return name | |
| } | |
| func writePersistedState(path string, state persistedState) error { | |
| if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { | |
| return fmt.Errorf("create state file directory for %s: %w", path, err) | |
| } | |
| data, err := json.MarshalIndent(state, "", " ") | |
| if err != nil { | |
| return fmt.Errorf("marshal persisted state: %w", err) | |
| } | |
| if err := os.WriteFile(path, append(data, '\n'), 0o600); err != nil { | |
| return fmt.Errorf("write state file %s: %w", path, err) | |
| } | |
| return nil | |
| } | |
| func readPersistedState(path string) (persistedState, error) { | |
| data, err := os.ReadFile(path) | |
| if err != nil { | |
| return persistedState{}, fmt.Errorf("read state file %s: %w", path, err) | |
| } | |
| var state persistedState | |
| if err := json.Unmarshal(data, &state); err != nil { | |
| return persistedState{}, fmt.Errorf("parse state file %s: %w", path, err) | |
| } | |
| if state.WrappedCEKBase64 == "" { | |
| return persistedState{}, fmt.Errorf("state file %s missing wrapped_cek", path) | |
| } | |
| if state.WrapAlg == "" { | |
| return persistedState{}, fmt.Errorf("state file %s missing wrap_alg", path) | |
| } | |
| return state, nil | |
| } | |
| func removeStateFile(path string) error { | |
| err := os.Remove(path) | |
| if err == nil || errors.Is(err, os.ErrNotExist) { | |
| return nil | |
| } | |
| return fmt.Errorf("remove state file %s: %w", path, err) | |
| } | |
| func ncryptPaddingInfoPtr(flag uint32, paddingInfo unsafe.Pointer) (uintptr, error) { | |
| switch flag { | |
| case ncryptPadOAEPFlag: | |
| if paddingInfo == nil { | |
| return 0, fmt.Errorf("OAEP padding requires non-nil padding info") | |
| } | |
| return uintptr(paddingInfo), nil | |
| case ncryptPadPKCS1Flag: | |
| return 0, nil | |
| default: | |
| return 0, fmt.Errorf("unsupported padding flag: 0x%08x", flag) | |
| } | |
| } | |
| func slicePtr(data []byte) *byte { | |
| if len(data) == 0 { | |
| return nil | |
| } | |
| return &data[0] | |
| } | |
| func freeHandle(handle ncryptHandle) { | |
| if handle == 0 { | |
| return | |
| } | |
| if err := callNCrypt(procNCryptFreeObject, uintptr(handle)); err != nil { | |
| log.Printf("warning: NCryptFreeObject failed: %v", err) | |
| } | |
| } | |
| func callNCrypt(proc *windows.LazyProc, args ...uintptr) error { | |
| status, _, _ := proc.Call(args...) | |
| if status == 0 { | |
| return nil | |
| } | |
| return windows.Errno(status) | |
| } | |
| func isInvalidParameterError(err error) bool { | |
| return errors.Is(err, nteInvalidParameter) || errors.Is(err, win32InvalidParameter) | |
| } | |
| func isKeyNotFoundError(err error) bool { | |
| return errors.Is(err, nteBadKeyset) || | |
| errors.Is(err, nteNoKey) || | |
| errors.Is(err, nteNotFound) | |
| } | |
| func passportPersistentKeyName(name string) (string, error) { | |
| currentUser, err := user.Current() | |
| if err != nil { | |
| return "", fmt.Errorf("get current user: %w", err) | |
| } | |
| if currentUser.Uid == "" { | |
| return "", fmt.Errorf("current user SID is empty") | |
| } | |
| return fmt.Sprintf("%s//%s/%s/%s", currentUser.Uid, passportKeyNameDomain, passportKeyNameSubdomain, name), nil | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment