From 0cd0d2a4023d5530946fed95c74cd808c7401b40 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Mon, 23 Mar 2026 10:52:27 +0900 Subject: [PATCH] feat: wallet private key export API with password verification --- internal/auth/service.go | 12 ++++++++++++ internal/chain/handler.go | 34 ++++++++++++++++++++++++++++++++++ internal/chain/service.go | 35 +++++++++++++++++++++++++++-------- main.go | 1 + routes/routes.go | 1 + 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/internal/auth/service.go b/internal/auth/service.go index e3e048c..eb8c349 100644 --- a/internal/auth/service.go +++ b/internal/auth/service.go @@ -535,6 +535,18 @@ func sanitizeForUsername(s string) string { // If these fail, the admin user exists without a wallet/profile. // This is acceptable because EnsureAdmin runs once at startup and failures // are logged as warnings. A restart will skip user creation (already exists). +// VerifyPassword checks if the password matches the user's stored hash. +func (s *Service) VerifyPassword(userID uint, password string) error { + user, err := s.repo.FindByID(userID) + if err != nil { + return fmt.Errorf("user not found") + } + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { + return fmt.Errorf("invalid password") + } + return nil +} + func (s *Service) EnsureAdmin(username, password string) error { if _, err := s.repo.FindByUsername(username); err == nil { return nil diff --git a/internal/chain/handler.go b/internal/chain/handler.go index 900d326..083302a 100644 --- a/internal/chain/handler.go +++ b/internal/chain/handler.go @@ -3,6 +3,7 @@ package chain import ( "errors" "log" + "log/slog" "strconv" "strings" @@ -620,6 +621,39 @@ func (h *Handler) RegisterTemplate(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(result) } +// ExportWallet godoc +// @Summary 개인키 내보내기 +// @Description 비밀번호 확인 후 현재 유저의 지갑 개인키를 반환합니다 +// @Tags Chain +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param body body exportRequest true "비밀번호" +// @Success 200 {object} map[string]string +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Router /api/chain/wallet/export [post] +type exportRequest struct { + Password string `json:"password"` +} + +func (h *Handler) ExportWallet(c *fiber.Ctx) error { + userID, err := getUserID(c) + if err != nil { + return err + } + var req exportRequest + if err := c.BodyParser(&req); err != nil || req.Password == "" { + return c.Status(400).JSON(fiber.Map{"error": "password is required"}) + } + slog.Warn("wallet export requested", "userID", userID, "ip", c.IP()) + privKeyHex, err := h.svc.ExportPrivKey(userID, req.Password) + if err != nil { + return c.Status(401).JSON(fiber.Map{"error": "invalid password"}) + } + return c.JSON(fiber.Map{"privateKey": privKeyHex}) +} + // ---- Internal Handlers (game server, username-based) ---- // InternalGrantReward godoc diff --git a/internal/chain/service.go b/internal/chain/service.go index da652fd..609fb1f 100644 --- a/internal/chain/service.go +++ b/internal/chain/service.go @@ -21,14 +21,15 @@ import ( ) type Service struct { - repo *Repository - client *Client - chainID string - operatorWallet *wallet.Wallet - encKeyBytes []byte // 32-byte AES-256 key - userResolver func(username string) (uint, error) - operatorMu sync.Mutex // serialises operator-nonce transactions - userMu sync.Map // per-user mutex (keyed by userID uint) + repo *Repository + client *Client + chainID string + operatorWallet *wallet.Wallet + encKeyBytes []byte // 32-byte AES-256 key + userResolver func(username string) (uint, error) + passwordVerifier func(userID uint, password string) error + operatorMu sync.Mutex // serialises operator-nonce transactions + userMu sync.Map // per-user mutex (keyed by userID uint) } // SetUserResolver sets the callback that resolves username → userID. @@ -36,6 +37,24 @@ func (s *Service) SetUserResolver(fn func(username string) (uint, error)) { s.userResolver = fn } +func (s *Service) SetPasswordVerifier(fn func(userID uint, password string) error) { + s.passwordVerifier = fn +} + +func (s *Service) ExportPrivKey(userID uint, password string) (string, error) { + if s.passwordVerifier == nil { + return "", fmt.Errorf("password verifier not configured") + } + if err := s.passwordVerifier(userID, password); err != nil { + return "", err + } + w, _, err := s.loadUserWallet(userID) + if err != nil { + return "", err + } + return w.PrivKey().Hex(), nil +} + // resolveUsername converts a username to the user's on-chain pubKeyHex. // If the user exists but has no wallet (e.g. legacy user or failed creation), // a wallet is auto-created on the fly. diff --git a/main.go b/main.go index 90b4dbb..b4f2c35 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,7 @@ func main() { _, err := chainSvc.CreateWallet(userID) return err }) + chainSvc.SetPasswordVerifier(authSvc.VerifyPassword) playerRepo := player.NewRepository(db) playerSvc := player.NewService(playerRepo) diff --git a/routes/routes.go b/routes/routes.go index 03896eb..e37f694 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -113,6 +113,7 @@ func Register( // Chain - Queries (authenticated) ch := api.Group("/chain", authMw) ch.Get("/wallet", chainH.GetWalletInfo) + ch.Post("/wallet/export", chainH.ExportWallet) ch.Get("/balance", chainH.GetBalance) ch.Get("/assets", chainH.GetAssets) ch.Get("/asset/:id", chainH.GetAsset)