mirror of
https://github.com/amnezia-vpn/amneziawg-windows-client.git
synced 2026-07-03 14:05:10 +02:00
global: move certain win32 APIs to x/sys/windows
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
+20
-39
@@ -9,78 +9,59 @@ import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
dpCRYPTPROTECT_UI_FORBIDDEN uint32 = 0x1
|
||||
dpCRYPTPROTECT_LOCAL_MACHINE uint32 = 0x4
|
||||
dpCRYPTPROTECT_CRED_SYNC uint32 = 0x8
|
||||
dpCRYPTPROTECT_AUDIT uint32 = 0x10
|
||||
dpCRYPTPROTECT_NO_RECOVERY uint32 = 0x20
|
||||
dpCRYPTPROTECT_VERIFY_PROTECTION uint32 = 0x40
|
||||
dpCRYPTPROTECT_CRED_REGENERATE uint32 = 0x80
|
||||
)
|
||||
|
||||
type dpBlob struct {
|
||||
len uint32
|
||||
data uintptr
|
||||
func bytesToBlob(bytes []byte) *windows.DataBlob {
|
||||
blob := &windows.DataBlob{Size: uint32(len(bytes))}
|
||||
if len(bytes) > 0 {
|
||||
blob.Data = &bytes[0]
|
||||
}
|
||||
return blob
|
||||
}
|
||||
|
||||
func bytesToBlob(bytes []byte) *dpBlob {
|
||||
blob := &dpBlob{}
|
||||
blob.len = uint32(len(bytes))
|
||||
if len(bytes) > 0 {
|
||||
blob.data = uintptr(unsafe.Pointer(&bytes[0]))
|
||||
}
|
||||
return blob
|
||||
}
|
||||
|
||||
//sys cryptProtectData(dataIn *dpBlob, name *uint16, optionalEntropy *dpBlob, reserved uintptr, promptStruct uintptr, flags uint32, dataOut *dpBlob) (err error) = crypt32.CryptProtectData
|
||||
|
||||
func Encrypt(data []byte, name string) ([]byte, error) {
|
||||
out := dpBlob{}
|
||||
err := cryptProtectData(bytesToBlob(data), windows.StringToUTF16Ptr(name), nil, 0, 0, dpCRYPTPROTECT_UI_FORBIDDEN, &out)
|
||||
out := windows.DataBlob{}
|
||||
err := windows.CryptProtectData(bytesToBlob(data), windows.StringToUTF16Ptr(name), nil, 0, nil, windows.CRYPTPROTECT_UI_FORBIDDEN, &out)
|
||||
if err != nil {
|
||||
return nil, errors.New("Unable to encrypt DPAPI protected data: " + err.Error())
|
||||
return nil, fmt.Errorf("unable to encrypt DPAPI protected data: %w", err)
|
||||
}
|
||||
|
||||
outSlice := *(*[]byte)(unsafe.Pointer(&(struct {
|
||||
addr uintptr
|
||||
addr *byte
|
||||
len int
|
||||
cap int
|
||||
}{out.data, int(out.len), int(out.len)})))
|
||||
}{out.Data, int(out.Size), int(out.Size)})))
|
||||
ret := make([]byte, len(outSlice))
|
||||
copy(ret, outSlice)
|
||||
windows.LocalFree(windows.Handle(out.data))
|
||||
windows.LocalFree(windows.Handle(unsafe.Pointer(out.Data)))
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
//sys cryptUnprotectData(dataIn *dpBlob, name **uint16, optionalEntropy *dpBlob, reserved uintptr, promptStruct uintptr, flags uint32, dataOut *dpBlob) (err error) = crypt32.CryptUnprotectData
|
||||
|
||||
func Decrypt(data []byte, name string) ([]byte, error) {
|
||||
out := dpBlob{}
|
||||
out := windows.DataBlob{}
|
||||
var outName *uint16
|
||||
utf16Name, err := windows.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cryptUnprotectData(bytesToBlob(data), &outName, nil, 0, 0, dpCRYPTPROTECT_UI_FORBIDDEN, &out)
|
||||
err = windows.CryptUnprotectData(bytesToBlob(data), &outName, nil, 0, nil, windows.CRYPTPROTECT_UI_FORBIDDEN, &out)
|
||||
if err != nil {
|
||||
return nil, errors.New("Unable to decrypt DPAPI protected data: " + err.Error())
|
||||
return nil, fmt.Errorf("unable to decrypt DPAPI protected data: %w", err)
|
||||
}
|
||||
|
||||
outSlice := *(*[]byte)(unsafe.Pointer(&(struct {
|
||||
addr uintptr
|
||||
addr *byte
|
||||
len int
|
||||
cap int
|
||||
}{out.data, int(out.len), int(out.len)})))
|
||||
}{out.Data, int(out.Size), int(out.Size)})))
|
||||
ret := make([]byte, len(outSlice))
|
||||
copy(ret, outSlice)
|
||||
windows.LocalFree(windows.Handle(out.data))
|
||||
windows.LocalFree(windows.Handle(unsafe.Pointer(out.Data)))
|
||||
|
||||
// Note: this ridiculous open-coded strcmp is not constant time.
|
||||
different := false
|
||||
@@ -101,7 +82,7 @@ func Decrypt(data []byte, name string) ([]byte, error) {
|
||||
windows.LocalFree(windows.Handle(unsafe.Pointer(outName)))
|
||||
|
||||
if different {
|
||||
return nil, errors.New("The input name does not match the stored name")
|
||||
return nil, errors.New("input name does not match the stored name")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2020 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package dpapi
|
||||
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zdpapi_windows.go dpapi_windows.go
|
||||
Executable
BIN
Binary file not shown.
@@ -1,61 +0,0 @@
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
package dpapi
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modcrypt32 = windows.NewLazySystemDLL("crypt32.dll")
|
||||
|
||||
procCryptProtectData = modcrypt32.NewProc("CryptProtectData")
|
||||
procCryptUnprotectData = modcrypt32.NewProc("CryptUnprotectData")
|
||||
)
|
||||
|
||||
func cryptProtectData(dataIn *dpBlob, name *uint16, optionalEntropy *dpBlob, reserved uintptr, promptStruct uintptr, flags uint32, dataOut *dpBlob) (err error) {
|
||||
r1, _, e1 := syscall.Syscall9(procCryptProtectData.Addr(), 7, uintptr(unsafe.Pointer(dataIn)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(optionalEntropy)), uintptr(reserved), uintptr(promptStruct), uintptr(flags), uintptr(unsafe.Pointer(dataOut)), 0, 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func cryptUnprotectData(dataIn *dpBlob, name **uint16, optionalEntropy *dpBlob, reserved uintptr, promptStruct uintptr, flags uint32, dataOut *dpBlob) (err error) {
|
||||
r1, _, e1 := syscall.Syscall9(procCryptUnprotectData.Addr(), 7, uintptr(unsafe.Pointer(dataIn)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(optionalEntropy)), uintptr(reserved), uintptr(promptStruct), uintptr(flags), uintptr(unsafe.Pointer(dataOut)), 0, 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -11,20 +11,6 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
fncFILE_NAME uint32 = 0x00000001
|
||||
fncDIR_NAME uint32 = 0x00000002
|
||||
fncATTRIBUTES uint32 = 0x00000004
|
||||
fncSIZE uint32 = 0x00000008
|
||||
fncLAST_WRITE uint32 = 0x00000010
|
||||
fncLAST_ACCESS uint32 = 0x00000020
|
||||
fncCREATION uint32 = 0x00000040
|
||||
fncSECURITY uint32 = 0x00000100
|
||||
)
|
||||
|
||||
//sys findFirstChangeNotification(path *uint16, watchSubtree bool, filter uint32) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = kernel32.FindFirstChangeNotificationW
|
||||
//sys findNextChangeNotification(handle windows.Handle) (err error) = kernel32.FindNextChangeNotification
|
||||
|
||||
var haveStartedWatchingConfigDir bool
|
||||
|
||||
func startWatchingConfigDir() {
|
||||
@@ -36,7 +22,7 @@ func startWatchingConfigDir() {
|
||||
h := windows.InvalidHandle
|
||||
defer func() {
|
||||
if h != windows.InvalidHandle {
|
||||
windows.CloseHandle(h)
|
||||
windows.FindCloseChangeNotification(h)
|
||||
}
|
||||
haveStartedWatchingConfigDir = false
|
||||
}()
|
||||
@@ -45,7 +31,7 @@ func startWatchingConfigDir() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h, err = findFirstChangeNotification(windows.StringToUTF16Ptr(configFileDir), true, fncFILE_NAME|fncDIR_NAME|fncATTRIBUTES|fncSIZE|fncLAST_WRITE|fncLAST_ACCESS|fncCREATION|fncSECURITY)
|
||||
h, err = windows.FindFirstChangeNotification(configFileDir, true, windows.FILE_NOTIFY_CHANGE_FILE_NAME|windows.FILE_NOTIFY_CHANGE_DIR_NAME|windows.FILE_NOTIFY_CHANGE_ATTRIBUTES|windows.FILE_NOTIFY_CHANGE_SIZE|windows.FILE_NOTIFY_CHANGE_LAST_WRITE|windows.FILE_NOTIFY_CHANGE_LAST_ACCESS|windows.FILE_NOTIFY_CHANGE_CREATION|windows.FILE_NOTIFY_CHANGE_SECURITY)
|
||||
if err != nil {
|
||||
log.Printf("Unable to monitor config directory: %v", err)
|
||||
return
|
||||
@@ -54,7 +40,7 @@ func startWatchingConfigDir() {
|
||||
s, err := windows.WaitForSingleObject(h, windows.INFINITE)
|
||||
if err != nil || s == windows.WAIT_FAILED {
|
||||
log.Printf("Unable to wait on config directory watcher: %v", err)
|
||||
windows.CloseHandle(h)
|
||||
windows.FindCloseChangeNotification(h)
|
||||
h = windows.InvalidHandle
|
||||
goto startover
|
||||
}
|
||||
@@ -63,10 +49,10 @@ func startWatchingConfigDir() {
|
||||
cb.cb()
|
||||
}
|
||||
|
||||
err = findNextChangeNotification(h)
|
||||
err = windows.FindNextChangeNotification(h)
|
||||
if err != nil {
|
||||
log.Printf("Unable to monitor config directory again: %v", err)
|
||||
windows.CloseHandle(h)
|
||||
windows.FindCloseChangeNotification(h)
|
||||
h = windows.InvalidHandle
|
||||
goto startover
|
||||
}
|
||||
|
||||
@@ -38,35 +38,11 @@ func errnoErr(e syscall.Errno) error {
|
||||
}
|
||||
|
||||
var (
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
modwininet = windows.NewLazySystemDLL("wininet.dll")
|
||||
modwininet = windows.NewLazySystemDLL("wininet.dll")
|
||||
|
||||
procFindFirstChangeNotificationW = modkernel32.NewProc("FindFirstChangeNotificationW")
|
||||
procFindNextChangeNotification = modkernel32.NewProc("FindNextChangeNotification")
|
||||
procInternetGetConnectedState = modwininet.NewProc("InternetGetConnectedState")
|
||||
procInternetGetConnectedState = modwininet.NewProc("InternetGetConnectedState")
|
||||
)
|
||||
|
||||
func findFirstChangeNotification(path *uint16, watchSubtree bool, filter uint32) (handle windows.Handle, err error) {
|
||||
var _p0 uint32
|
||||
if watchSubtree {
|
||||
_p0 = 1
|
||||
}
|
||||
r0, _, e1 := syscall.Syscall(procFindFirstChangeNotificationW.Addr(), 3, uintptr(unsafe.Pointer(path)), uintptr(_p0), uintptr(filter))
|
||||
handle = windows.Handle(r0)
|
||||
if handle == windows.InvalidHandle {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func findNextChangeNotification(handle windows.Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procFindNextChangeNotification.Addr(), 1, uintptr(handle), 0, 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func internetGetConnectedState(flags *uint32, reserved uint32) (connected bool) {
|
||||
r0, _, _ := syscall.Syscall(procInternetGetConnectedState.Addr(), 2, uintptr(unsafe.Pointer(flags)), uintptr(reserved), 0)
|
||||
connected = r0 != 0
|
||||
|
||||
@@ -37,12 +37,12 @@ func TokenIsElevatedOrElevatable(token windows.Token) bool {
|
||||
}
|
||||
|
||||
func IsAdminDesktop() (bool, error) {
|
||||
hwnd := getShellWindow()
|
||||
hwnd := windows.GetShellWindow()
|
||||
if hwnd == 0 {
|
||||
return false, windows.ERROR_INVALID_WINDOW_HANDLE
|
||||
}
|
||||
var pid uint32
|
||||
_, err := getWindowThreadProcessId(hwnd, &pid)
|
||||
_, err := windows.GetWindowThreadProcessId(hwnd, &pid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -84,6 +84,3 @@ const (
|
||||
//sys coInitializeEx(reserved uintptr, coInit uint32) (ret error) = ole32.CoInitializeEx
|
||||
//sys coUninitialize() = ole32.CoUninitialize
|
||||
//sys coGetObject(name *uint16, bindOpts *cBIND_OPTS3, guid *windows.GUID, functionTable ***[0xffff]uintptr) (ret error) = ole32.CoGetObject
|
||||
|
||||
//sys getWindowThreadProcessId(hwnd uintptr, pid *uint32) (tid uint32, err error) = user32.GetWindowThreadProcessId
|
||||
//sys getShellWindow() (hwnd uintptr) = user32.GetShellWindow
|
||||
|
||||
@@ -38,17 +38,14 @@ func errnoErr(e syscall.Errno) error {
|
||||
}
|
||||
|
||||
var (
|
||||
modntdll = windows.NewLazySystemDLL("ntdll.dll")
|
||||
modole32 = windows.NewLazySystemDLL("ole32.dll")
|
||||
moduser32 = windows.NewLazySystemDLL("user32.dll")
|
||||
modntdll = windows.NewLazySystemDLL("ntdll.dll")
|
||||
modole32 = windows.NewLazySystemDLL("ole32.dll")
|
||||
|
||||
procRtlGetCurrentPeb = modntdll.NewProc("RtlGetCurrentPeb")
|
||||
procRtlInitUnicodeString = modntdll.NewProc("RtlInitUnicodeString")
|
||||
procCoGetObject = modole32.NewProc("CoGetObject")
|
||||
procCoInitializeEx = modole32.NewProc("CoInitializeEx")
|
||||
procCoUninitialize = modole32.NewProc("CoUninitialize")
|
||||
procGetShellWindow = moduser32.NewProc("GetShellWindow")
|
||||
procGetWindowThreadProcessId = moduser32.NewProc("GetWindowThreadProcessId")
|
||||
procRtlGetCurrentPeb = modntdll.NewProc("RtlGetCurrentPeb")
|
||||
procRtlInitUnicodeString = modntdll.NewProc("RtlInitUnicodeString")
|
||||
procCoGetObject = modole32.NewProc("CoGetObject")
|
||||
procCoInitializeEx = modole32.NewProc("CoInitializeEx")
|
||||
procCoUninitialize = modole32.NewProc("CoUninitialize")
|
||||
)
|
||||
|
||||
func rtlGetCurrentPeb() (peb *cPEB) {
|
||||
@@ -82,18 +79,3 @@ func coUninitialize() {
|
||||
syscall.Syscall(procCoUninitialize.Addr(), 0, 0, 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
func getShellWindow() (hwnd uintptr) {
|
||||
r0, _, _ := syscall.Syscall(procGetShellWindow.Addr(), 0, 0, 0, 0)
|
||||
hwnd = uintptr(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func getWindowThreadProcessId(hwnd uintptr, pid *uint32) (tid uint32, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procGetWindowThreadProcessId.Addr(), 2, uintptr(hwnd), uintptr(unsafe.Pointer(pid)), 0)
|
||||
tid = uint32(r0)
|
||||
if tid == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Copyright (C) 2019-2020 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package wintrust
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -18,7 +18,7 @@ func TestExtractCertificateNames(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
names, err := ExtractCertificateNames(filepath.Join(system32, "ntoskrnl.exe"))
|
||||
names, err := extractCertificateNames(filepath.Join(system32, "ntoskrnl.exe"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestExtractCertificateExtension(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
policies, err := ExtractCertificatePolicies(filepath.Join(system32, "ntoskrnl.exe"), "2.5.29.32")
|
||||
policies, err := extractCertificatePolicies(filepath.Join(system32, "ntoskrnl.exe"), "2.5.29.32")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2020 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func extractCertificateNames(path string) ([]string, error) {
|
||||
path16, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var certStore windows.Handle
|
||||
err = windows.CryptQueryObject(windows.CERT_QUERY_OBJECT_FILE, unsafe.Pointer(path16), windows.CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, windows.CERT_QUERY_FORMAT_FLAG_ALL, 0, nil, nil, nil, &certStore, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer windows.CertCloseStore(certStore, 0)
|
||||
var cert *windows.CertContext
|
||||
var names []string
|
||||
for {
|
||||
cert, err = windows.CertEnumCertificatesInStore(certStore, cert)
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok {
|
||||
if errno == syscall.Errno(windows.CRYPT_E_NOT_FOUND) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if cert == nil {
|
||||
break
|
||||
}
|
||||
nameLen := windows.CertGetNameString(cert, windows.CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nil, nil, 0)
|
||||
if nameLen == 0 {
|
||||
continue
|
||||
}
|
||||
name16 := make([]uint16, nameLen)
|
||||
if windows.CertGetNameString(cert, windows.CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nil, &name16[0], nameLen) != nameLen {
|
||||
continue
|
||||
}
|
||||
if name16[0] == 0 {
|
||||
continue
|
||||
}
|
||||
names = append(names, windows.UTF16ToString(name16))
|
||||
}
|
||||
if names == nil {
|
||||
return nil, syscall.Errno(windows.CRYPT_E_NOT_FOUND)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func extractCertificatePolicies(path string, oid string) ([]string, error) {
|
||||
path16, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oid8, err := windows.BytePtrFromString(oid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var certStore windows.Handle
|
||||
err = windows.CryptQueryObject(windows.CERT_QUERY_OBJECT_FILE, unsafe.Pointer(path16), windows.CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, windows.CERT_QUERY_FORMAT_FLAG_ALL, 0, nil, nil, nil, &certStore, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer windows.CertCloseStore(certStore, 0)
|
||||
var cert *windows.CertContext
|
||||
var policies []string
|
||||
for {
|
||||
cert, err = windows.CertEnumCertificatesInStore(certStore, cert)
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok {
|
||||
if errno == syscall.Errno(windows.CRYPT_E_NOT_FOUND) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if cert == nil {
|
||||
break
|
||||
}
|
||||
ci := (*windows.CertInfo)(unsafe.Pointer(cert.CertInfo))
|
||||
ext := windows.CertFindExtension(oid8, ci.CountExtensions, ci.Extensions)
|
||||
if ext == nil {
|
||||
continue
|
||||
}
|
||||
var decodedLen uint32
|
||||
err = windows.CryptDecodeObject(windows.X509_ASN_ENCODING|windows.PKCS_7_ASN_ENCODING, ext.ObjId, ext.Value.Data, ext.Value.Size, 0, nil, &decodedLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes := make([]byte, decodedLen)
|
||||
certPoliciesInfo := (*windows.CertPoliciesInfo)(unsafe.Pointer(&bytes[0]))
|
||||
err = windows.CryptDecodeObject(windows.X509_ASN_ENCODING|windows.PKCS_7_ASN_ENCODING, ext.ObjId, ext.Value.Data, ext.Value.Size, 0, unsafe.Pointer(&bytes[0]), &decodedLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := uintptr(0); i < uintptr(certPoliciesInfo.Count); i++ {
|
||||
cp := (*windows.CertPolicy)(unsafe.Pointer(uintptr(unsafe.Pointer(certPoliciesInfo.PolicyInfos)) + i*unsafe.Sizeof(*certPoliciesInfo.PolicyInfos)))
|
||||
policies = append(policies, windows.BytePtrToString(cp.Identifier))
|
||||
}
|
||||
}
|
||||
if policies == nil {
|
||||
return nil, syscall.Errno(windows.CRYPT_E_NOT_FOUND)
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
+13
-16
@@ -10,8 +10,6 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"golang.zx2c4.com/wireguard/windows/version/wintrust"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,19 +23,18 @@ func VerifyAuthenticode(path string) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
file := &wintrust.WinTrustFileInfo{
|
||||
CbStruct: uint32(unsafe.Sizeof(wintrust.WinTrustFileInfo{})),
|
||||
FilePath: path16,
|
||||
data := &windows.WinTrustData{
|
||||
Size: uint32(unsafe.Sizeof(windows.WinTrustData{})),
|
||||
UIChoice: windows.WTD_UI_NONE,
|
||||
RevocationChecks: windows.WTD_REVOKE_WHOLECHAIN, // Full revocation checking, as this is called with network connectivity.
|
||||
UnionChoice: windows.WTD_CHOICE_FILE,
|
||||
StateAction: windows.WTD_STATEACTION_VERIFY,
|
||||
FileOrCatalogOrBlobOrSgnrOrCert: unsafe.Pointer(&windows.WinTrustFileInfo{
|
||||
Size: uint32(unsafe.Sizeof(windows.WinTrustFileInfo{})),
|
||||
FilePath: path16,
|
||||
}),
|
||||
}
|
||||
data := &wintrust.WinTrustData{
|
||||
CbStruct: uint32(unsafe.Sizeof(wintrust.WinTrustData{})),
|
||||
UIChoice: wintrust.WTD_UI_NONE,
|
||||
RevocationChecks: wintrust.WTD_REVOKE_WHOLECHAIN, // Full revocation checking, as this is called with network connectivity.
|
||||
UnionChoice: wintrust.WTD_CHOICE_FILE,
|
||||
StateAction: wintrust.WTD_STATEACTION_VERIFY,
|
||||
FileOrCatalogOrBlobOrSgnrOrCert: uintptr(unsafe.Pointer(file)),
|
||||
}
|
||||
return wintrust.WinVerifyTrust(windows.InvalidHandle, &wintrust.WINTRUST_ACTION_GENERIC_VERIFY_V2, data) == nil
|
||||
return windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data) == nil
|
||||
}
|
||||
|
||||
// These are easily by-passable checks, which do not serve serve security purposes. Do not place security-sensitive
|
||||
@@ -49,7 +46,7 @@ func IsRunningOfficialVersion() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
names, err := wintrust.ExtractCertificateNames(path)
|
||||
names, err := extractCertificateNames(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -67,7 +64,7 @@ func IsRunningEVSigned() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
policies, err := wintrust.ExtractCertificatePolicies(path, policyExtensionOid)
|
||||
policies, err := extractCertificatePolicies(path, policyExtensionOid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2020 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package wintrust
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
_CERT_QUERY_OBJECT_FILE = 1
|
||||
_CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED = 1024
|
||||
_CERT_QUERY_FORMAT_FLAG_ALL = 14
|
||||
_CERT_NAME_SIMPLE_DISPLAY_TYPE = 4
|
||||
)
|
||||
|
||||
type blob struct {
|
||||
len uint32
|
||||
data *byte
|
||||
}
|
||||
|
||||
type bitBlob struct {
|
||||
len uint32
|
||||
data *byte
|
||||
unusedBits uint32
|
||||
}
|
||||
|
||||
type algoIdentifier struct {
|
||||
objId uintptr
|
||||
params blob
|
||||
}
|
||||
|
||||
type pubkeyInfo struct {
|
||||
algo algoIdentifier
|
||||
publicKey bitBlob
|
||||
}
|
||||
|
||||
type certExtension struct {
|
||||
objId *byte
|
||||
critical uint32
|
||||
value blob
|
||||
}
|
||||
|
||||
type certInfo struct {
|
||||
version uint32
|
||||
serialNumber blob /* CRYPT_INTEGER_BLOB */
|
||||
signatureAlgorithm algoIdentifier /* CRYPT_ALGORITHM_IDENTIFIER */
|
||||
issuer blob /* CERT_NAME_BLOB */
|
||||
notbefore windows.Filetime
|
||||
notafter windows.Filetime
|
||||
subject blob /* CERT_NAME_BLOB */
|
||||
subjectPublicKeyInfo pubkeyInfo /* CERT_PUBLIC_KEY_INFO */
|
||||
issuerUniqueId bitBlob /* CRYPT_BIT_BLOB */
|
||||
subjectUniqueId bitBlob /* CRYPT_BIT_BLOB */
|
||||
countExtensions uint32
|
||||
extensions *certExtension /* *CERT_EXTENSION */
|
||||
}
|
||||
|
||||
type certPolicy struct {
|
||||
identifier *byte
|
||||
countQualifiers uint32
|
||||
qualifiers uintptr /* CERT_POLICY_QUALIFIER_INFO */
|
||||
}
|
||||
|
||||
type certPoliciesInfo struct {
|
||||
countPolicyInfos uint32
|
||||
policyInfos *certPolicy
|
||||
}
|
||||
|
||||
//sys cryptQueryObject(objectType uint32, object uintptr, expectedContentTypeFlags uint32, expectedFormatTypeFlags uint32, flags uint32, msgAndCertEncodingType *uint32, contentType *uint32, formatType *uint32, certStore *windows.Handle, msg *windows.Handle, context *uintptr) (err error) = crypt32.CryptQueryObject
|
||||
//sys certGetNameString(certContext *windows.CertContext, nameType uint32, flags uint32, typePara unsafe.Pointer, name *uint16, size uint32) (chars uint32) = crypt32.CertGetNameStringW
|
||||
//sys certFindExtension(objId *byte, countExtensions uint32, extensions *certExtension) (ret *certExtension) = crypt32.CertFindExtension
|
||||
//sys cryptDecodeObject(encodingType uint32, structType *byte, encodedBytes *byte, lenEncodedBytes uint32, flags uint32, decoded unsafe.Pointer, decodedLen *uint32) (err error) = crypt32.CryptDecodeObject
|
||||
|
||||
func ExtractCertificateNames(path string) ([]string, error) {
|
||||
path16, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var certStore windows.Handle
|
||||
err = cryptQueryObject(_CERT_QUERY_OBJECT_FILE, uintptr(unsafe.Pointer(path16)), _CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, _CERT_QUERY_FORMAT_FLAG_ALL, 0, nil, nil, nil, &certStore, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer windows.CertCloseStore(certStore, 0)
|
||||
var cert *windows.CertContext
|
||||
var names []string
|
||||
for {
|
||||
cert, err = windows.CertEnumCertificatesInStore(certStore, cert)
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok {
|
||||
if errno == syscall.Errno(windows.CRYPT_E_NOT_FOUND) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if cert == nil {
|
||||
break
|
||||
}
|
||||
nameLen := certGetNameString(cert, _CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nil, nil, 0)
|
||||
if nameLen == 0 {
|
||||
continue
|
||||
}
|
||||
name16 := make([]uint16, nameLen)
|
||||
if certGetNameString(cert, _CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nil, &name16[0], nameLen) != nameLen {
|
||||
continue
|
||||
}
|
||||
if name16[0] == 0 {
|
||||
continue
|
||||
}
|
||||
names = append(names, windows.UTF16ToString(name16))
|
||||
}
|
||||
if names == nil {
|
||||
return nil, syscall.Errno(windows.CRYPT_E_NOT_FOUND)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func ExtractCertificatePolicies(path string, oid string) ([]string, error) {
|
||||
path16, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oid8, err := windows.BytePtrFromString(oid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var certStore windows.Handle
|
||||
err = cryptQueryObject(_CERT_QUERY_OBJECT_FILE, uintptr(unsafe.Pointer(path16)), _CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, _CERT_QUERY_FORMAT_FLAG_ALL, 0, nil, nil, nil, &certStore, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer windows.CertCloseStore(certStore, 0)
|
||||
var cert *windows.CertContext
|
||||
var policies []string
|
||||
for {
|
||||
cert, err = windows.CertEnumCertificatesInStore(certStore, cert)
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok {
|
||||
if errno == syscall.Errno(windows.CRYPT_E_NOT_FOUND) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if cert == nil {
|
||||
break
|
||||
}
|
||||
ci := (*certInfo)(unsafe.Pointer(cert.CertInfo))
|
||||
ext := certFindExtension(oid8, ci.countExtensions, ci.extensions)
|
||||
if ext == nil {
|
||||
continue
|
||||
}
|
||||
var decodedLen uint32
|
||||
err = cryptDecodeObject(windows.X509_ASN_ENCODING|windows.PKCS_7_ASN_ENCODING, ext.objId, ext.value.data, ext.value.len, 0, nil, &decodedLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes := make([]byte, decodedLen)
|
||||
certPoliciesInfo := (*certPoliciesInfo)(unsafe.Pointer(&bytes[0]))
|
||||
err = cryptDecodeObject(windows.X509_ASN_ENCODING|windows.PKCS_7_ASN_ENCODING, ext.objId, ext.value.data, ext.value.len, 0, unsafe.Pointer(&bytes[0]), &decodedLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := uintptr(0); i < uintptr(certPoliciesInfo.countPolicyInfos); i++ {
|
||||
cp := (*certPolicy)(unsafe.Pointer(uintptr(unsafe.Pointer(certPoliciesInfo.policyInfos)) + i*unsafe.Sizeof(*certPoliciesInfo.policyInfos)))
|
||||
policies = append(policies, windows.BytePtrToString(cp.identifier))
|
||||
}
|
||||
}
|
||||
if policies == nil {
|
||||
return nil, syscall.Errno(windows.CRYPT_E_NOT_FOUND)
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2020 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package wintrust
|
||||
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go wintrust_windows.go certificate_windows.go
|
||||
@@ -1,116 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2020 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package wintrust
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type WinTrustData struct {
|
||||
CbStruct uint32
|
||||
PolicyCallbackData uintptr
|
||||
SIPClientData uintptr
|
||||
UIChoice uint32
|
||||
RevocationChecks uint32
|
||||
UnionChoice uint32
|
||||
FileOrCatalogOrBlobOrSgnrOrCert uintptr
|
||||
StateAction uint32
|
||||
StateData syscall.Handle
|
||||
URLReference *uint16
|
||||
ProvFlags uint32
|
||||
UIContext uint32
|
||||
SignatureSettings *WintrustSignatureSettings
|
||||
}
|
||||
|
||||
const (
|
||||
WTD_UI_ALL = 1
|
||||
WTD_UI_NONE = 2
|
||||
WTD_UI_NOBAD = 3
|
||||
WTD_UI_NOGOOD = 4
|
||||
)
|
||||
|
||||
const (
|
||||
WTD_REVOKE_NONE = 0
|
||||
WTD_REVOKE_WHOLECHAIN = 1
|
||||
)
|
||||
|
||||
const (
|
||||
WTD_CHOICE_FILE = 1
|
||||
WTD_CHOICE_CATALOG = 2
|
||||
WTD_CHOICE_BLOB = 3
|
||||
WTD_CHOICE_SIGNER = 4
|
||||
WTD_CHOICE_CERT = 5
|
||||
)
|
||||
|
||||
const (
|
||||
WTD_STATEACTION_IGNORE = 0x00000000
|
||||
WTD_STATEACTION_VERIFY = 0x00000010
|
||||
WTD_STATEACTION_CLOSE = 0x00000002
|
||||
WTD_STATEACTION_AUTO_CACHE = 0x00000003
|
||||
WTD_STATEACTION_AUTO_CACHE_FLUSH = 0x00000004
|
||||
)
|
||||
|
||||
const (
|
||||
WTD_USE_IE4_TRUST_FLAG = 0x1
|
||||
WTD_NO_IE4_CHAIN_FLAG = 0x2
|
||||
WTD_NO_POLICY_USAGE_FLAG = 0x4
|
||||
WTD_REVOCATION_CHECK_NONE = 0x10
|
||||
WTD_REVOCATION_CHECK_END_CERT = 0x20
|
||||
WTD_REVOCATION_CHECK_CHAIN = 0x40
|
||||
WTD_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT = 0x80
|
||||
WTD_SAFER_FLAG = 0x100
|
||||
WTD_HASH_ONLY_FLAG = 0x200
|
||||
WTD_USE_DEFAULT_OSVER_CHECK = 0x400
|
||||
WTD_LIFETIME_SIGNING_FLAG = 0x800
|
||||
WTD_CACHE_ONLY_URL_RETRIEVAL = 0x1000
|
||||
WTD_DISABLE_MD2_MD4 = 0x2000
|
||||
WTD_MOTW = 0x4000
|
||||
)
|
||||
|
||||
const (
|
||||
TRUST_E_NOSIGNATURE = 0x800B0100
|
||||
TRUST_E_EXPLICIT_DISTRUST = 0x800B0111
|
||||
TRUST_E_SUBJECT_NOT_TRUSTED = 0x800B0004
|
||||
CRYPT_E_SECURITY_SETTINGS = 0x80092026
|
||||
)
|
||||
|
||||
const (
|
||||
WTD_UICONTEXT_EXECUTE = 0
|
||||
WTD_UICONTEXT_INSTALL = 1
|
||||
)
|
||||
|
||||
var WINTRUST_ACTION_GENERIC_VERIFY_V2 = windows.GUID{
|
||||
Data1: 0xaac56b,
|
||||
Data2: 0xcd44,
|
||||
Data3: 0x11d0,
|
||||
Data4: [8]byte{0x8c, 0xc2, 0x0, 0xc0, 0x4f, 0xc2, 0x95, 0xee},
|
||||
}
|
||||
|
||||
type WinTrustFileInfo struct {
|
||||
CbStruct uint32
|
||||
FilePath *uint16
|
||||
File windows.Handle
|
||||
KnownSubject *windows.GUID
|
||||
}
|
||||
|
||||
type WintrustSignatureSettings struct {
|
||||
CbStruct uint32
|
||||
Index uint32
|
||||
Flags uint32
|
||||
SecondarySigs uint32
|
||||
VerifiedSigIndex uint32
|
||||
CryptoPolicy *CertStrongSignPara
|
||||
}
|
||||
|
||||
type CertStrongSignPara struct {
|
||||
CbStruct uint32
|
||||
InfoChoice uint32
|
||||
InfoOrSerializedInfoOrOID uintptr
|
||||
}
|
||||
|
||||
//sys WinVerifyTrust(hWnd windows.Handle, actionId *windows.GUID, data *WinTrustData) (err error) [r1 != 0] = wintrust.WinVerifyTrust
|
||||
@@ -1,85 +0,0 @@
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
package wintrust
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modcrypt32 = windows.NewLazySystemDLL("crypt32.dll")
|
||||
modwintrust = windows.NewLazySystemDLL("wintrust.dll")
|
||||
|
||||
procCertFindExtension = modcrypt32.NewProc("CertFindExtension")
|
||||
procCertGetNameStringW = modcrypt32.NewProc("CertGetNameStringW")
|
||||
procCryptDecodeObject = modcrypt32.NewProc("CryptDecodeObject")
|
||||
procCryptQueryObject = modcrypt32.NewProc("CryptQueryObject")
|
||||
procWinVerifyTrust = modwintrust.NewProc("WinVerifyTrust")
|
||||
)
|
||||
|
||||
func certFindExtension(objId *byte, countExtensions uint32, extensions *certExtension) (ret *certExtension) {
|
||||
r0, _, _ := syscall.Syscall(procCertFindExtension.Addr(), 3, uintptr(unsafe.Pointer(objId)), uintptr(countExtensions), uintptr(unsafe.Pointer(extensions)))
|
||||
ret = (*certExtension)(unsafe.Pointer(r0))
|
||||
return
|
||||
}
|
||||
|
||||
func certGetNameString(certContext *windows.CertContext, nameType uint32, flags uint32, typePara unsafe.Pointer, name *uint16, size uint32) (chars uint32) {
|
||||
r0, _, _ := syscall.Syscall6(procCertGetNameStringW.Addr(), 6, uintptr(unsafe.Pointer(certContext)), uintptr(nameType), uintptr(flags), uintptr(typePara), uintptr(unsafe.Pointer(name)), uintptr(size))
|
||||
chars = uint32(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func cryptDecodeObject(encodingType uint32, structType *byte, encodedBytes *byte, lenEncodedBytes uint32, flags uint32, decoded unsafe.Pointer, decodedLen *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall9(procCryptDecodeObject.Addr(), 7, uintptr(encodingType), uintptr(unsafe.Pointer(structType)), uintptr(unsafe.Pointer(encodedBytes)), uintptr(lenEncodedBytes), uintptr(flags), uintptr(decoded), uintptr(unsafe.Pointer(decodedLen)), 0, 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func cryptQueryObject(objectType uint32, object uintptr, expectedContentTypeFlags uint32, expectedFormatTypeFlags uint32, flags uint32, msgAndCertEncodingType *uint32, contentType *uint32, formatType *uint32, certStore *windows.Handle, msg *windows.Handle, context *uintptr) (err error) {
|
||||
r1, _, e1 := syscall.Syscall12(procCryptQueryObject.Addr(), 11, uintptr(objectType), uintptr(object), uintptr(expectedContentTypeFlags), uintptr(expectedFormatTypeFlags), uintptr(flags), uintptr(unsafe.Pointer(msgAndCertEncodingType)), uintptr(unsafe.Pointer(contentType)), uintptr(unsafe.Pointer(formatType)), uintptr(unsafe.Pointer(certStore)), uintptr(unsafe.Pointer(msg)), uintptr(unsafe.Pointer(context)), 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func WinVerifyTrust(hWnd windows.Handle, actionId *windows.GUID, data *WinTrustData) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procWinVerifyTrust.Addr(), 3, uintptr(hWnd), uintptr(unsafe.Pointer(actionId)), uintptr(unsafe.Pointer(data)))
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user