Altered the badger db to derive the HWID from more static sources and to have a fallback in the event of a failure
This commit is contained in:
+235
-12
@@ -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 {
|
||||
|
||||
@@ -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[:]
|
||||
}
|
||||
Reference in New Issue
Block a user