From 10a3f0156b9a887af6a0c70547c5acdb9695a5c7 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Mon, 23 Mar 2026 10:45:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20v1=E2=86=92v2=20wallet=20key=20migratio?= =?UTF-8?q?n=20on=20server=20startup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- internal/chain/repository.go | 19 ++++++++++++++++++ internal/chain/service.go | 38 ++++++++++++++++++++++++++++++++++++ main.go | 5 +++++ 3 files changed, 62 insertions(+) diff --git a/internal/chain/repository.go b/internal/chain/repository.go index b6b4a1e..8dd9aec 100644 --- a/internal/chain/repository.go +++ b/internal/chain/repository.go @@ -29,3 +29,22 @@ func (r *Repository) FindByPubKeyHex(pubKeyHex string) (*UserWallet, error) { } return &w, nil } + +// FindAllByKeyVersion returns all wallets with the given key version. +func (r *Repository) FindAllByKeyVersion(version int) ([]UserWallet, error) { + var wallets []UserWallet + if err := r.db.Where("key_version = ?", version).Find(&wallets).Error; err != nil { + return nil, err + } + return wallets, nil +} + +// UpdateEncryption updates the encryption fields of a wallet. +func (r *Repository) UpdateEncryption(id uint, encPrivKey, encNonce, hkdfSalt string, keyVersion int) error { + return r.db.Model(&UserWallet{}).Where("id = ?", id).Updates(map[string]any{ + "encrypted_priv_key": encPrivKey, + "enc_nonce": encNonce, + "hkdf_salt": hkdfSalt, + "key_version": keyVersion, + }).Error +} diff --git a/internal/chain/service.go b/internal/chain/service.go index cefb060..da652fd 100644 --- a/internal/chain/service.go +++ b/internal/chain/service.go @@ -204,6 +204,44 @@ func (s *Service) decryptPrivKeyV2(cipherHex, nonceHex, saltHex string, userID u return tocrypto.PrivateKey(plaintext), nil } +// ---- Wallet Migration ---- + +// MigrateWalletKeys re-encrypts all v1 wallets using HKDF per-wallet keys. +// Each wallet is migrated individually; failures are logged and skipped. +func (s *Service) MigrateWalletKeys() error { + wallets, err := s.repo.FindAllByKeyVersion(1) + if err != nil { + return fmt.Errorf("query v1 wallets: %w", err) + } + if len(wallets) == 0 { + return nil + } + log.Printf("INFO: migrating %d v1 wallets to v2 (HKDF)", len(wallets)) + var migrated, failed int + for _, uw := range wallets { + privKey, err := s.decryptPrivKey(uw.EncryptedPrivKey, uw.EncNonce) + if err != nil { + log.Printf("ERROR: v1 decrypt failed for walletID=%d userID=%d: %v", uw.ID, uw.UserID, err) + failed++ + continue + } + cipherHex, nonceHex, saltHex, err := s.encryptPrivKeyV2(privKey, uw.UserID) + if err != nil { + log.Printf("ERROR: v2 encrypt failed for walletID=%d userID=%d: %v", uw.ID, uw.UserID, err) + failed++ + continue + } + if err := s.repo.UpdateEncryption(uw.ID, cipherHex, nonceHex, saltHex, 2); err != nil { + log.Printf("ERROR: DB update failed for walletID=%d userID=%d: %v", uw.ID, uw.UserID, err) + failed++ + continue + } + migrated++ + } + log.Printf("INFO: wallet migration complete: %d migrated, %d failed", migrated, failed) + return nil +} + // ---- Wallet Management ---- // CreateWallet generates a new keypair, encrypts it, and stores in DB. diff --git a/main.go b/main.go index f76ddc1..90b4dbb 100644 --- a/main.go +++ b/main.go @@ -75,6 +75,11 @@ func main() { } chainHandler := chain.NewHandler(chainSvc) + // Migrate v1 wallets to v2 (HKDF per-wallet keys) + if err := chainSvc.MigrateWalletKeys(); err != nil { + log.Fatalf("wallet key migration failed: %v", err) + } + userResolver := func(username string) (uint, error) { user, err := authRepo.FindByUsername(username) if err != nil {