security_secret_bootstrap.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. package repository
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "database/sql"
  6. "encoding/hex"
  7. "errors"
  8. "fmt"
  9. "log"
  10. "strings"
  11. "time"
  12. "github.com/Wei-Shaw/sub2api/ent"
  13. "github.com/Wei-Shaw/sub2api/ent/securitysecret"
  14. "github.com/Wei-Shaw/sub2api/internal/config"
  15. )
  16. const (
  17. securitySecretKeyJWT = "jwt_secret"
  18. securitySecretReadRetryMax = 5
  19. securitySecretReadRetryWait = 10 * time.Millisecond
  20. )
  21. var readRandomBytes = rand.Read
  22. func ensureBootstrapSecrets(ctx context.Context, client *ent.Client, cfg *config.Config) error {
  23. if client == nil {
  24. return fmt.Errorf("nil ent client")
  25. }
  26. if cfg == nil {
  27. return fmt.Errorf("nil config")
  28. }
  29. cfg.JWT.Secret = strings.TrimSpace(cfg.JWT.Secret)
  30. if cfg.JWT.Secret != "" {
  31. storedSecret, err := createSecuritySecretIfAbsent(ctx, client, securitySecretKeyJWT, cfg.JWT.Secret)
  32. if err != nil {
  33. return fmt.Errorf("persist jwt secret: %w", err)
  34. }
  35. if storedSecret != cfg.JWT.Secret {
  36. log.Println("Warning: configured JWT secret mismatches persisted value; using persisted secret for cross-instance consistency.")
  37. }
  38. cfg.JWT.Secret = storedSecret
  39. return nil
  40. }
  41. secret, created, err := getOrCreateGeneratedSecuritySecret(ctx, client, securitySecretKeyJWT, 32)
  42. if err != nil {
  43. return fmt.Errorf("ensure jwt secret: %w", err)
  44. }
  45. cfg.JWT.Secret = secret
  46. if created {
  47. log.Println("Warning: JWT secret auto-generated and persisted to database. Consider rotating to a managed secret for production.")
  48. }
  49. return nil
  50. }
  51. func getOrCreateGeneratedSecuritySecret(ctx context.Context, client *ent.Client, key string, byteLength int) (string, bool, error) {
  52. existing, err := client.SecuritySecret.Query().Where(securitysecret.KeyEQ(key)).Only(ctx)
  53. if err == nil {
  54. value := strings.TrimSpace(existing.Value)
  55. if len([]byte(value)) < 32 {
  56. return "", false, fmt.Errorf("stored secret %q must be at least 32 bytes", key)
  57. }
  58. return value, false, nil
  59. }
  60. if !ent.IsNotFound(err) {
  61. return "", false, err
  62. }
  63. generated, err := generateHexSecret(byteLength)
  64. if err != nil {
  65. return "", false, err
  66. }
  67. if err := client.SecuritySecret.Create().
  68. SetKey(key).
  69. SetValue(generated).
  70. OnConflictColumns(securitysecret.FieldKey).
  71. DoNothing().
  72. Exec(ctx); err != nil {
  73. if !isSQLNoRowsError(err) {
  74. return "", false, err
  75. }
  76. }
  77. stored, err := querySecuritySecretWithRetry(ctx, client, key)
  78. if err != nil {
  79. return "", false, err
  80. }
  81. value := strings.TrimSpace(stored.Value)
  82. if len([]byte(value)) < 32 {
  83. return "", false, fmt.Errorf("stored secret %q must be at least 32 bytes", key)
  84. }
  85. return value, value == generated, nil
  86. }
  87. func createSecuritySecretIfAbsent(ctx context.Context, client *ent.Client, key, value string) (string, error) {
  88. value = strings.TrimSpace(value)
  89. if len([]byte(value)) < 32 {
  90. return "", fmt.Errorf("secret %q must be at least 32 bytes", key)
  91. }
  92. if err := client.SecuritySecret.Create().
  93. SetKey(key).
  94. SetValue(value).
  95. OnConflictColumns(securitysecret.FieldKey).
  96. DoNothing().
  97. Exec(ctx); err != nil {
  98. if !isSQLNoRowsError(err) {
  99. return "", err
  100. }
  101. }
  102. stored, err := querySecuritySecretWithRetry(ctx, client, key)
  103. if err != nil {
  104. return "", err
  105. }
  106. storedValue := strings.TrimSpace(stored.Value)
  107. if len([]byte(storedValue)) < 32 {
  108. return "", fmt.Errorf("stored secret %q must be at least 32 bytes", key)
  109. }
  110. return storedValue, nil
  111. }
  112. func querySecuritySecretWithRetry(ctx context.Context, client *ent.Client, key string) (*ent.SecuritySecret, error) {
  113. var lastErr error
  114. for attempt := 0; attempt <= securitySecretReadRetryMax; attempt++ {
  115. stored, err := client.SecuritySecret.Query().Where(securitysecret.KeyEQ(key)).Only(ctx)
  116. if err == nil {
  117. return stored, nil
  118. }
  119. if !isSecretNotFoundError(err) {
  120. return nil, err
  121. }
  122. lastErr = err
  123. if attempt == securitySecretReadRetryMax {
  124. break
  125. }
  126. timer := time.NewTimer(securitySecretReadRetryWait)
  127. select {
  128. case <-ctx.Done():
  129. timer.Stop()
  130. return nil, ctx.Err()
  131. case <-timer.C:
  132. }
  133. }
  134. return nil, lastErr
  135. }
  136. func isSecretNotFoundError(err error) bool {
  137. if err == nil {
  138. return false
  139. }
  140. return ent.IsNotFound(err) || isSQLNoRowsError(err)
  141. }
  142. func isSQLNoRowsError(err error) bool {
  143. if err == nil {
  144. return false
  145. }
  146. return errors.Is(err, sql.ErrNoRows) || strings.Contains(err.Error(), "no rows in result set")
  147. }
  148. func generateHexSecret(byteLength int) (string, error) {
  149. if byteLength <= 0 {
  150. byteLength = 32
  151. }
  152. buf := make([]byte, byteLength)
  153. if _, err := readRandomBytes(buf); err != nil {
  154. return "", fmt.Errorf("generate random secret: %w", err)
  155. }
  156. return hex.EncodeToString(buf), nil
  157. }