From da53a787fe7ec7ced48c4861f8ccaaeabd05e9d0 Mon Sep 17 00:00:00 2001 From: Evan Hosinski Date: Tue, 7 Apr 2026 08:52:11 -0400 Subject: [PATCH] Altered the badger db to derive the HWID from more static sources and to have a fallback in the event of a failure --- cmd/whois.go | 4 +- internal/badger/badger.go | 247 +++++++++++++++++++++++++++++++-- internal/badger/badger_test.go | 73 ++++++++++ 3 files changed, 310 insertions(+), 14 deletions(-) create mode 100644 internal/badger/badger_test.go diff --git a/cmd/whois.go b/cmd/whois.go index a30ecce..9fda540 100644 --- a/cmd/whois.go +++ b/cmd/whois.go @@ -195,8 +195,8 @@ var ( // Write history records to file if any if len(historyRecords) > 0 { - fmt.Println("[*] Records Found: %d\n", len(historyRecords)) - fmt.Println("[*] WHOIS History being written to file: %s%s\n", whoisOutputFile, fType.Extension()) + fmt.Printf("[*] Records Found: %d\n", len(historyRecords)) + fmt.Printf("[*] WHOIS History being written to file: %s%s\n", whoisOutputFile, fType.Extension()) writeErr := export.WriteWhoIsHistoryToFile(historyRecords, filename, fType) if writeErr != nil { if debugGlobal { diff --git a/internal/badger/badger.go b/internal/badger/badger.go index 543a72c..7da2015 100644 --- a/internal/badger/badger.go +++ b/internal/badger/badger.go @@ -3,15 +3,19 @@ package badger import ( "crypto/sha256" "errors" - "github.com/dgraph-io/badger/v4" - "go.uber.org/zap" + "fmt" "log" "os" + "os/exec" "os/user" "path/filepath" "runtime" "strings" "sync" + "time" + + "github.com/dgraph-io/badger/v4" + "go.uber.org/zap" ) var ( @@ -21,13 +25,35 @@ var ( once sync.Once ) -func GetHardwareEntropy() []byte { +const fingerprintSalt = "CrowsNest-static-salt-value" + +func GetHardwareEntropy() ([]byte, error) { + source, machineID, err := getMachineID() + if err != nil { + return nil, err + } + + fingerprint := strings.Join([]string{ + "v2", + runtime.GOOS, + source, + machineID, + fingerprintSalt, + }, ":") + + return hashFingerprint(fingerprint), nil +} + +func GetLegacyHardwareEntropy() []byte { // Get hostname hostname, err := os.Hostname() if err != nil { hostname = "unknown-host" log.Printf("Error getting hostname: %v", err) } + if legacyHostname := strings.TrimSpace(os.Getenv("CROWSNEST_LEGACY_HOSTNAME")); legacyHostname != "" { + hostname = legacyHostname + } // Get username currentUser, err := user.Current() @@ -35,9 +61,15 @@ func GetHardwareEntropy() []byte { if err == nil && currentUser != nil { username = currentUser.Username } + if legacyUsername := strings.TrimSpace(os.Getenv("CROWSNEST_LEGACY_USERNAME")); legacyUsername != "" { + username = legacyUsername + } // Get OS and architecture info osInfo := runtime.GOOS + "-" + runtime.GOARCH + if legacyOSInfo := strings.TrimSpace(os.Getenv("CROWSNEST_LEGACY_OSINFO")); legacyOSInfo != "" { + osInfo = legacyOSInfo + } // Combine all information for a unique but consistent fingerprint fingerprint := strings.Join([]string{ @@ -45,14 +77,93 @@ func GetHardwareEntropy() []byte { username, osInfo, // You could add a static salt here for additional security - "CrowsNest-static-salt-value", + fingerprintSalt, }, ":") // Hash the fingerprint to get a 32-byte key + return hashFingerprint(fingerprint) +} + +func hashFingerprint(fingerprint string) []byte { sum := sha256.Sum256([]byte(fingerprint)) return sum[:] } +func getMachineID() (string, string, error) { + switch runtime.GOOS { + case "darwin": + return getDarwinMachineID() + case "linux": + return getLinuxMachineID() + case "windows": + return getWindowsMachineID() + default: + return "", "", fmt.Errorf("stable machine id is not implemented for %s", runtime.GOOS) + } +} + +func getDarwinMachineID() (string, string, error) { + out, err := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice").Output() + if err != nil { + return "", "", fmt.Errorf("run ioreg: %w", err) + } + + for _, line := range strings.Split(string(out), "\n") { + if !strings.Contains(line, "IOPlatformUUID") { + continue + } + if id := normalizeMachineID(lastQuotedValue(line)); id != "" { + return "darwin-ioplatformuuid", id, nil + } + } + + return "", "", errors.New("IOPlatformUUID not found") +} + +func getLinuxMachineID() (string, string, error) { + for _, path := range []string{"/etc/machine-id", "/var/lib/dbus/machine-id"} { + out, err := os.ReadFile(path) + if err != nil { + continue + } + if id := normalizeMachineID(string(out)); id != "" { + return "linux-machine-id", id, nil + } + } + + return "", "", errors.New("machine-id not found") +} + +func getWindowsMachineID() (string, string, error) { + out, err := exec.Command("reg", "query", `HKLM\SOFTWARE\Microsoft\Cryptography`, "/v", "MachineGuid").Output() + if err != nil { + return "", "", fmt.Errorf("query MachineGuid: %w", err) + } + + for _, line := range strings.Split(string(out), "\n") { + fields := strings.Fields(line) + if len(fields) >= 3 && strings.EqualFold(fields[0], "MachineGuid") { + if id := normalizeMachineID(fields[len(fields)-1]); id != "" { + return "windows-machineguid", id, nil + } + } + } + + return "", "", errors.New("MachineGuid not found") +} + +func lastQuotedValue(line string) string { + values := strings.Split(line, "\"") + if len(values) < 4 { + return "" + } + return values[len(values)-2] +} + +func normalizeMachineID(id string) string { + return strings.ToLower(strings.TrimSpace(id)) +} + func Start(dirPath string) *badger.DB { var err error @@ -65,7 +176,7 @@ func Start(dirPath string) *badger.DB { } rootDir = dirPath - encryptionKey = GetHardwareEntropy() + encryptionKey, err = GetHardwareEntropy() if err != nil { zap.L().Fatal("get_encryption_key", zap.String("message", "failed to get encryption key"), @@ -74,22 +185,134 @@ func Start(dirPath string) *badger.DB { } badgerDB := filepath.Join(rootDir, "badger.db") - opts := badger.DefaultOptions(badgerDB). - WithEncryptionKey(encryptionKey). - WithIndexCacheSize(10 << 20). // 10MB - WithLoggingLevel(badger.ERROR) - db, err = badger.Open(opts) + db, err = openBadger(badgerDB, encryptionKey) if err != nil { - zap.L().Fatal("new_badger_db", - zap.String("message", "failed to open badger database"), + zap.L().Warn("open_badger_db", + zap.String("message", "failed to open badger database with stable machine key; trying legacy key"), zap.Error(err), ) + db, err = openBadgerWithLegacyMigration(badgerDB, encryptionKey) + if err != nil { + zap.L().Fatal("new_badger_db", + zap.String("message", "failed to open badger database"), + zap.Error(err), + ) + } } }) return db } +func openBadger(dbPath string, key []byte) (*badger.DB, error) { + opts := badger.DefaultOptions(dbPath). + WithEncryptionKey(key). + WithIndexCacheSize(10 << 20). // 10MB + WithLoggingLevel(badger.ERROR) + return badger.Open(opts) +} + +func openBadgerWithLegacyMigration(dbPath string, stableKey []byte) (*badger.DB, error) { + legacyKey := GetLegacyHardwareEntropy() + legacyDB, err := openBadger(dbPath, legacyKey) + if err != nil { + return nil, fmt.Errorf("stable key failed and legacy key failed: %w", err) + } + + migratedDB, err := migrateBadgerEncryption(dbPath, legacyDB, stableKey) + if err != nil { + if closeErr := legacyDB.Close(); closeErr != nil { + zap.L().Error("close_legacy_badger_db", zap.Error(closeErr)) + } + return nil, err + } + + return migratedDB, nil +} + +func migrateBadgerEncryption(dbPath string, legacyDB *badger.DB, stableKey []byte) (*badger.DB, error) { + parentDir := filepath.Dir(dbPath) + timestamp := time.Now().Format("20060102-150405") + migrationPath := filepath.Join(parentDir, fmt.Sprintf(".%s.migrating-%s", filepath.Base(dbPath), timestamp)) + backupPath := filepath.Join(parentDir, fmt.Sprintf("%s.legacy-backup-%s", filepath.Base(dbPath), timestamp)) + + newDB, err := openBadger(migrationPath, stableKey) + if err != nil { + return nil, fmt.Errorf("open migration badger db: %w", err) + } + + if err := copyBadgerData(legacyDB, newDB); err != nil { + _ = newDB.Close() + _ = os.RemoveAll(migrationPath) + return nil, fmt.Errorf("copy legacy badger data: %w", err) + } + + if err := legacyDB.Close(); err != nil { + _ = newDB.Close() + _ = os.RemoveAll(migrationPath) + return nil, fmt.Errorf("close legacy badger db: %w", err) + } + + if err := newDB.Close(); err != nil { + _ = os.RemoveAll(migrationPath) + return nil, fmt.Errorf("close migration badger db: %w", err) + } + + if err := os.Rename(dbPath, backupPath); err != nil { + _ = os.RemoveAll(migrationPath) + return nil, fmt.Errorf("backup legacy badger db: %w", err) + } + + if err := os.Rename(migrationPath, dbPath); err != nil { + if restoreErr := os.Rename(backupPath, dbPath); restoreErr != nil { + return nil, fmt.Errorf("promote migrated badger db: %w; restore legacy backup: %v", err, restoreErr) + } + return nil, fmt.Errorf("promote migrated badger db: %w", err) + } + + db, err := openBadger(dbPath, stableKey) + if err != nil { + return nil, fmt.Errorf("open migrated badger db: %w", err) + } + + zap.L().Info("migrated_badger_encryption", + zap.String("backup", backupPath), + zap.String("path", dbPath), + ) + return db, nil +} + +func copyBadgerData(src *badger.DB, dst *badger.DB) error { + return src.View(func(srcTxn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = true + iter := srcTxn.NewIterator(opts) + defer iter.Close() + + return dst.Update(func(dstTxn *badger.Txn) error { + for iter.Rewind(); iter.Valid(); iter.Next() { + item := iter.Item() + if item.IsDeletedOrExpired() { + continue + } + + key := item.KeyCopy(nil) + value, err := item.ValueCopy(nil) + if err != nil { + return err + } + + entry := badger.NewEntry(key, value).WithMeta(item.UserMeta()) + entry.ExpiresAt = item.ExpiresAt() + if err := dstTxn.SetEntry(entry); err != nil { + return err + } + } + return nil + }) + }) +} + func Close() { err := db.Close() if err != nil { diff --git a/internal/badger/badger_test.go b/internal/badger/badger_test.go new file mode 100644 index 0000000..fc4cd2b --- /dev/null +++ b/internal/badger/badger_test.go @@ -0,0 +1,73 @@ +package badger + +import ( + "crypto/sha256" + "os" + "path/filepath" + "strings" + "testing" + + badgerapi "github.com/dgraph-io/badger/v4" +) + +func TestMigrateBadgerEncryptionCopiesDataToStableKey(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "badger.db") + legacyKey := testKey("legacy-key") + stableKey := testKey("stable-key") + + legacyDB, err := openBadger(dbPath, legacyKey) + if err != nil { + t.Fatalf("open legacy db: %v", err) + } + + if err := legacyDB.Update(func(txn *badgerapi.Txn) error { + return txn.Set([]byte("cfg:api_key"), []byte("secret")) + }); err != nil { + t.Fatalf("seed legacy db: %v", err) + } + + migratedDB, err := migrateBadgerEncryption(dbPath, legacyDB, stableKey) + if err != nil { + t.Fatalf("migrate db: %v", err) + } + defer migratedDB.Close() + + var got string + if err := migratedDB.View(func(txn *badgerapi.Txn) error { + item, err := txn.Get([]byte("cfg:api_key")) + if err != nil { + return err + } + return item.Value(func(value []byte) error { + got = string(value) + return nil + }) + }); err != nil { + t.Fatalf("read migrated db: %v", err) + } + + if got != "secret" { + t.Fatalf("migrated value = %q, want %q", got, "secret") + } + + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("read temp dir: %v", err) + } + foundBackup := false + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), "badger.db.legacy-backup-") { + foundBackup = true + break + } + } + if !foundBackup { + t.Fatal("legacy backup directory was not created") + } +} + +func testKey(value string) []byte { + sum := sha256.Sum256([]byte(value)) + return sum[:] +}