// SPDX-License-Identifier: Apache-2.0 // // Copyright The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build windows // Package fveapi reads the BitLocker status of a volume through fveapi.dll. // // fveapi.dll is the private user-mode BitLocker (Full Volume Encryption) API. // It is undocumented and Microsoft gives no ABI guarantee, so this package // only uses the oldest and smallest surface: FveGetStatusW with the // version 1 FVE_STATUS structure. The DLL is loaded lazily, and a missing // DLL or export results in an error instead of a crash. // // Why not the Shell property System.Volume.BitLockerProtection: the Shell // returns VT_EMPTY for built-in service accounts (LocalSystem, virtual // service accounts), so it does not work for windows_exporter running as a // service (https://github.com/prometheus-community/windows_exporter/issues/2290). // FveGetStatusW returns the same result for LocalSystem and for a // non-elevated user. // // Volume names must use the Win32 device namespace, for example // `\\.\C:`, `\\.\Volume{GUID}` or `\\.\GLOBALROOT\Device\HarddiskVolume1`. // A bare `C:` fails with E_ACCESSDENIED. // // The flag values come from the reverse-engineered header of KNSoft.NDK (MIT): // https://github.com/KNSoft/KNSoft.NDK/blob/main/Source/Include/KNSoft/NDK/Win32/FVE/FveApi.h // // Verified on Windows 11 (build 26300), as LocalSystem and as a non-elevated user, // against Win32_EncryptableVolume GetProtectionStatus/GetConversionStatus/GetLockStatus // and manage-bde -status. Flags per state: // // State Flags WMI Protection/Conversion/Lock // fully decrypted 0x00000004 0/0/0 // encryption paused 0x000401A1 0/4/0 // encryption running 0x00042121 0/2/0 // protection on 0x00041109 1/1/0 (OS volume with TPM: 0x00045309) // protection suspended 0x00040509 0/1/0 // locked 0x00041909 2/-/1 // decryption paused 0x00040191 0/5/0 // decryption running 0x00042111 0/3/0 // EFI and recovery 0x00000004 - // non-existent volume HRESULT 0x80070002 // // A suspended volume stays FULLY_ENCRYPTED; only PROTECTION_ACTIVE is cleared. // A locked volume still reports FULLY_ENCRYPTED and PROTECTION_ACTIVE, so LOCKED // has to be checked first. 0x80 marks a paused conversion and 0x2000 a running one; // both are not needed for the state. During decryption, ConvertedPercent is the // decrypted share of the volume. // // Not verified: waiting for activation (clear key without key protectors). package fveapi import ( "errors" "fmt" "unsafe" "golang.org/x/sys/windows" ) //nolint:gochecknoglobals var ( modFveapi = windows.NewLazySystemDLL("fveapi.dll") procFveGetStatusW = modFveapi.NewProc("FveGetStatusW") ) // FVE_STATUS_FLAG_* values of the Flags field. const ( StatusFlagInitialized uint32 = 0x00000001 StatusFlagFullyDecrypted uint32 = 0x00000004 StatusFlagFullyEncrypted uint32 = 0x00000008 StatusFlagDecryptionInProgress uint32 = 0x00000010 StatusFlagEncryptionInProgress uint32 = 0x00000020 StatusFlagConversionPausedMask uint32 = 0x000000C0 StatusFlagNonTPMProtector uint32 = 0x00000100 StatusFlagTPMProtector uint32 = 0x00000200 StatusFlagClearKey uint32 = 0x00000400 StatusFlagLocked uint32 = 0x00000800 StatusFlagProtectionActive uint32 = 0x00001000 StatusFlagOSVolume uint32 = 0x00004000 ) // statusV1 is the version 1 FVE_STATUS structure (32 bytes). // The padding fields keep the layout independent of the platform alignment of float64. type statusV1 struct { StructureSize uint32 StructureVersion uint32 Flags uint32 _ uint32 ConvertedPercent float64 LastConvertStatus int32 _ uint32 } // Status is the BitLocker status of a volume as reported by FveGetStatusW. type Status struct { Flags uint32 ConvertedPercent float64 } // State is the BitLocker state derived from Status.Flags. type State int const ( StateUnknown State = iota StateOff StateOn StateEncrypting StateDecrypting StateSuspended StateLocked StateWaitingForActivation ) var ErrUnavailable = errors.New("fveapi.dll or FveGetStatusW is not available") // GetStatus returns the BitLocker status of volumeName, e.g. `\\.\C:`. func GetStatus(volumeName string) (Status, error) { if err := procFveGetStatusW.Find(); err != nil { return Status{}, fmt.Errorf("%w: %w", ErrUnavailable, err) } volumeNamePtr, err := windows.UTF16PtrFromString(volumeName) if err != nil { return Status{}, fmt.Errorf("invalid volume name %q: %w", volumeName, err) } status := statusV1{ StructureSize: uint32(unsafe.Sizeof(statusV1{})), StructureVersion: 1, } hr, _, _ := procFveGetStatusW.Call( uintptr(unsafe.Pointer(volumeNamePtr)), uintptr(unsafe.Pointer(&status)), ) // FveGetStatusW returns an HRESULT. Only the sign bit indicates failure. if int32(hr) < 0 { // HRESULT_FROM_WIN32 (facility 7) wraps a Win32 error code, e.g. 0x80070002. if uint32(hr)&0xFFFF0000 == 0x80070000 { return Status{}, fmt.Errorf("FveGetStatusW(%s) failed: %w", volumeName, windows.Errno(uint32(hr)&0xFFFF)) } return Status{}, fmt.Errorf("FveGetStatusW(%s) failed: HRESULT 0x%08X", volumeName, uint32(hr)) } return Status{ Flags: status.Flags, ConvertedPercent: status.ConvertedPercent, }, nil } // State maps the status flags to a BitLocker state. // A fully encrypted volume is only reported as on if protection is active, // because a suspended volume stays fully encrypted with protection off. // Flag combinations that do not match a known state return StateUnknown // instead of being reported as off. func (s Status) State() State { flags := s.Flags switch { case flags&StatusFlagLocked != 0: return StateLocked case flags&StatusFlagFullyDecrypted != 0: return StateOff case flags&StatusFlagInitialized == 0: return StateUnknown case flags&StatusFlagEncryptionInProgress != 0: return StateEncrypting case flags&StatusFlagDecryptionInProgress != 0: return StateDecrypting case flags&StatusFlagFullyEncrypted == 0: return StateUnknown case flags&StatusFlagProtectionActive != 0: return StateOn case flags&StatusFlagClearKey != 0 && flags&(StatusFlagTPMProtector|StatusFlagNonTPMProtector) == 0: // Encrypted with a clear key, before any key protector was added. return StateWaitingForActivation default: return StateSuspended } }