feat: v1→v2 wallet key migration on server startup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,3 +29,22 @@ func (r *Repository) FindByPubKeyHex(pubKeyHex string) (*UserWallet, error) {
|
|||||||
}
|
}
|
||||||
return &w, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -204,6 +204,44 @@ func (s *Service) decryptPrivKeyV2(cipherHex, nonceHex, saltHex string, userID u
|
|||||||
return tocrypto.PrivateKey(plaintext), nil
|
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 ----
|
// ---- Wallet Management ----
|
||||||
|
|
||||||
// CreateWallet generates a new keypair, encrypts it, and stores in DB.
|
// CreateWallet generates a new keypair, encrypts it, and stores in DB.
|
||||||
|
|||||||
5
main.go
5
main.go
@@ -75,6 +75,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
chainHandler := chain.NewHandler(chainSvc)
|
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) {
|
userResolver := func(username string) (uint, error) {
|
||||||
user, err := authRepo.FindByUsername(username)
|
user, err := authRepo.FindByUsername(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user