| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- package repository
- import (
- "context"
- "crypto/rand"
- "database/sql"
- "encoding/hex"
- "errors"
- "fmt"
- "log"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/ent/securitysecret"
- "github.com/Wei-Shaw/sub2api/internal/config"
- )
- const (
- securitySecretKeyJWT = "jwt_secret"
- securitySecretReadRetryMax = 5
- securitySecretReadRetryWait = 10 * time.Millisecond
- )
- var readRandomBytes = rand.Read
- func ensureBootstrapSecrets(ctx context.Context, client *ent.Client, cfg *config.Config) error {
- if client == nil {
- return fmt.Errorf("nil ent client")
- }
- if cfg == nil {
- return fmt.Errorf("nil config")
- }
- cfg.JWT.Secret = strings.TrimSpace(cfg.JWT.Secret)
- if cfg.JWT.Secret != "" {
- storedSecret, err := createSecuritySecretIfAbsent(ctx, client, securitySecretKeyJWT, cfg.JWT.Secret)
- if err != nil {
- return fmt.Errorf("persist jwt secret: %w", err)
- }
- if storedSecret != cfg.JWT.Secret {
- log.Println("Warning: configured JWT secret mismatches persisted value; using persisted secret for cross-instance consistency.")
- }
- cfg.JWT.Secret = storedSecret
- return nil
- }
- secret, created, err := getOrCreateGeneratedSecuritySecret(ctx, client, securitySecretKeyJWT, 32)
- if err != nil {
- return fmt.Errorf("ensure jwt secret: %w", err)
- }
- cfg.JWT.Secret = secret
- if created {
- log.Println("Warning: JWT secret auto-generated and persisted to database. Consider rotating to a managed secret for production.")
- }
- return nil
- }
- func getOrCreateGeneratedSecuritySecret(ctx context.Context, client *ent.Client, key string, byteLength int) (string, bool, error) {
- existing, err := client.SecuritySecret.Query().Where(securitysecret.KeyEQ(key)).Only(ctx)
- if err == nil {
- value := strings.TrimSpace(existing.Value)
- if len([]byte(value)) < 32 {
- return "", false, fmt.Errorf("stored secret %q must be at least 32 bytes", key)
- }
- return value, false, nil
- }
- if !ent.IsNotFound(err) {
- return "", false, err
- }
- generated, err := generateHexSecret(byteLength)
- if err != nil {
- return "", false, err
- }
- if err := client.SecuritySecret.Create().
- SetKey(key).
- SetValue(generated).
- OnConflictColumns(securitysecret.FieldKey).
- DoNothing().
- Exec(ctx); err != nil {
- if !isSQLNoRowsError(err) {
- return "", false, err
- }
- }
- stored, err := querySecuritySecretWithRetry(ctx, client, key)
- if err != nil {
- return "", false, err
- }
- value := strings.TrimSpace(stored.Value)
- if len([]byte(value)) < 32 {
- return "", false, fmt.Errorf("stored secret %q must be at least 32 bytes", key)
- }
- return value, value == generated, nil
- }
- func createSecuritySecretIfAbsent(ctx context.Context, client *ent.Client, key, value string) (string, error) {
- value = strings.TrimSpace(value)
- if len([]byte(value)) < 32 {
- return "", fmt.Errorf("secret %q must be at least 32 bytes", key)
- }
- if err := client.SecuritySecret.Create().
- SetKey(key).
- SetValue(value).
- OnConflictColumns(securitysecret.FieldKey).
- DoNothing().
- Exec(ctx); err != nil {
- if !isSQLNoRowsError(err) {
- return "", err
- }
- }
- stored, err := querySecuritySecretWithRetry(ctx, client, key)
- if err != nil {
- return "", err
- }
- storedValue := strings.TrimSpace(stored.Value)
- if len([]byte(storedValue)) < 32 {
- return "", fmt.Errorf("stored secret %q must be at least 32 bytes", key)
- }
- return storedValue, nil
- }
- func querySecuritySecretWithRetry(ctx context.Context, client *ent.Client, key string) (*ent.SecuritySecret, error) {
- var lastErr error
- for attempt := 0; attempt <= securitySecretReadRetryMax; attempt++ {
- stored, err := client.SecuritySecret.Query().Where(securitysecret.KeyEQ(key)).Only(ctx)
- if err == nil {
- return stored, nil
- }
- if !isSecretNotFoundError(err) {
- return nil, err
- }
- lastErr = err
- if attempt == securitySecretReadRetryMax {
- break
- }
- timer := time.NewTimer(securitySecretReadRetryWait)
- select {
- case <-ctx.Done():
- timer.Stop()
- return nil, ctx.Err()
- case <-timer.C:
- }
- }
- return nil, lastErr
- }
- func isSecretNotFoundError(err error) bool {
- if err == nil {
- return false
- }
- return ent.IsNotFound(err) || isSQLNoRowsError(err)
- }
- func isSQLNoRowsError(err error) bool {
- if err == nil {
- return false
- }
- return errors.Is(err, sql.ErrNoRows) || strings.Contains(err.Error(), "no rows in result set")
- }
- func generateHexSecret(byteLength int) (string, error) {
- if byteLength <= 0 {
- byteLength = 32
- }
- buf := make([]byte, byteLength)
- if _, err := readRandomBytes(buf); err != nil {
- return "", fmt.Errorf("generate random secret: %w", err)
- }
- return hex.EncodeToString(buf), nil
- }
|