diff --git a/.env.include.example b/.env.include.example index 8ce00b8..0133b1a 100644 --- a/.env.include.example +++ b/.env.include.example @@ -8,6 +8,11 @@ PORT=9000 DB_DRIVER=sqlite DB_CONNECTION="./data/local.db?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)" +#Generate all secret values by running go run ./cmd/generate_secrets +ID_ENCODING_ALPHABET= +ID_ENCODING_PRIME= +ID_ENCODING_INVERSE= +ID_ENCODINDG_XOR_KEY= JWT_SECRET= # Go duration format JWT_EXPIRY=168h @@ -19,6 +24,3 @@ MAILER_IMAP_PORT= MAILER_USERNAME= MAILER_PASSWORD= MAILER_EMAIL_FROM= -MAILER_ENVELOPE_FROM= -MAILER_SUPPORT_EMAIL= -MAILER_SUPPORT_ENVELOPE_FROM= diff --git a/cmd/generate_secrets/main.go b/cmd/generate_secrets/main.go new file mode 100644 index 0000000..e92a8cb --- /dev/null +++ b/cmd/generate_secrets/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "math/big" + "strings" +) + +func main() { + // 1. Generate Shuffled Alphabet + // We start with standard base62 + chars := []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") + + // Fisher-Yates Shuffle + // We use crypto/rand for the swap index to ensure high entropy + for i := len(chars) - 1; i > 0; i-- { + nBig, _ := rand.Int(rand.Reader, big.NewInt(int64(i+1))) + j := int(nBig.Int64()) + chars[i], chars[j] = chars[j], chars[i] + } + alphabet := string(chars) + + // 2. Generate Random 64-bit Prime + // crypto/rand.Prime automatically generates a number of the given bit length + // that is prime with high probability. + primeBig, err := rand.Prime(rand.Reader, 64) + if err != nil { + panic(fmt.Errorf("failed to generate prime: %v", err)) + } + + // 3. Calculate Modular Inverse + // (Prime * Inverse) % 2^64 == 1 + // We use 1 << 64 as the modulus + modulus := new(big.Int).Lsh(big.NewInt(1), 64) + inverseBig := new(big.Int).ModInverse(primeBig, modulus) + + // 4. Generate Random 64-bit XOR Key + // Just a random 64-bit integer + xorKeyBig, _ := rand.Int(rand.Reader, modulus) + + // 5. Generate Hex-encoded JWT Secret (32 bytes / 256 bits) + jwtBytes := make([]byte, 32) + if _, err := rand.Read(jwtBytes); err != nil { + panic(fmt.Errorf("failed to generate jwt secret: %v", err)) + } + jwtSecret := hex.EncodeToString(jwtBytes) + + // --- OUTPUT --- + fmt.Println("Here are your generated secrets. Copy these into your .env or config file.") + fmt.Println(strings.Repeat("-", 60)) + + fmt.Printf("ALPHABET = \"%s\"\n", alphabet) + fmt.Printf("PRIME = %s\n", primeBig.String()) + fmt.Printf("INVERSE = %s\n", inverseBig.String()) + fmt.Printf("XOR_KEY = %s\n", xorKeyBig.String()) + fmt.Printf("JWT_SECRET = \"%s\"\n", jwtSecret) + + fmt.Println(strings.Repeat("-", 60)) +} diff --git a/internal/app/app.go b/internal/app/app.go index 3c1abcb..df10f49 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -29,13 +29,26 @@ func New(cfg *config.Config) (*App, error) { return nil, fmt.Errorf("failed to run migrations: %w", err) } - emailClient := service.NewEmailClient(cfg.MailerSMTPHost, cfg.MailerSMTPPort, cfg.MailerIMAPHost, cfg.MailerIMAPPort, cfg.MailerUsername, cfg.MailerPassword) + emailClient := service.NewEmailClient( + cfg.MailerSMTPHost, + cfg.MailerSMTPPort, + cfg.MailerIMAPHost, + cfg.MailerIMAPPort, + cfg.MailerUsername, + cfg.MailerPassword, + ) userRepository := repository.NewUserRepository(database) userService := service.NewUserService(userRepository) authService := service.NewAuthService(userRepository) - emailService := service.NewEmailService(emailClient, cfg.MailerEmailFrom, cfg.MailerEnvelopeFrom, cfg.MailerSupportFrom, cfg.MailerSupportEnvelopeFrom, cfg.AppURL, cfg.AppName, cfg.AppEnv == "development") + emailService := service.NewEmailService( + emailClient, + cfg.MailerEmailFrom, + cfg.AppURL, + cfg.AppName, + cfg.AppEnv == "development", + ) return &App{ Cfg: cfg, diff --git a/internal/config/config.go b/internal/config/config.go index 09f9f07..f121dac 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,19 +20,21 @@ type Config struct { DBDriver string DBConnection string + IDEncodingAlphabet string + IDEncodingPrime uint64 + IDEncodingInverse uint64 + IDEncodingXorKey uint64 + JWTSecret string JWTExpiry time.Duration - MailerSMTPHost string - MailerSMTPPort int - MailerIMAPHost string - MailerIMAPPort int - MailerUsername string - MailerPassword string - MailerEmailFrom string - MailerEnvelopeFrom string - MailerSupportFrom string - MailerSupportEnvelopeFrom string + MailerSMTPHost string + MailerSMTPPort int + MailerIMAPHost string + MailerIMAPPort int + MailerUsername string + MailerPassword string + MailerEmailFrom string } func Load() *Config { @@ -52,19 +54,21 @@ func Load() *Config { DBDriver: envString("DB_DRIVER", "sqlite"), DBConnection: envString("DB_CONNECTION", "./data/local.db?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)"), + IDEncodingAlphabet: envRequired("ID_ENCODING_ALPHABET"), + IDEncodingPrime: envRequiredUint64("ID_ENCODING_PRIME"), + IDEncodingInverse: envRequiredUint64("ID_ENCODING_INVERSE"), + IDEncodingXorKey: envRequiredUint64("ID_ENCODING_XOR_KEY"), + JWTSecret: envRequired("JWT_SECRET"), JWTExpiry: envDuration("JWT_EXPIRY", 168*time.Hour), // 7 days default - MailerSMTPHost: envString("MAILER_SMTP_HOST", ""), - MailerSMTPPort: envInt("MAILER_SMTP_PORT", 587), - MailerIMAPHost: envString("MAILER_IMAP_HOST", ""), - MailerIMAPPort: envInt("MAILER_IMAP_PORT", 993), - MailerUsername: envString("MAILER_USERNAME", ""), - MailerPassword: envString("MAILER_PASSWORD", ""), - MailerEmailFrom: envString("MAILER_EMAIL_FROM", ""), - MailerEnvelopeFrom: envString("MAILER_ENVELOPE_FROM", ""), - MailerSupportFrom: envString("MAILER_SUPPORT_EMAIL_FROM", ""), - MailerSupportEnvelopeFrom: envString("MAILER_SUPPORT_ENVELOPE_FROM", ""), + MailerSMTPHost: envString("MAILER_SMTP_HOST", ""), + MailerSMTPPort: envInt("MAILER_SMTP_PORT", 587), + MailerIMAPHost: envString("MAILER_IMAP_HOST", ""), + MailerIMAPPort: envInt("MAILER_IMAP_PORT", 993), + MailerUsername: envString("MAILER_USERNAME", ""), + MailerPassword: envString("MAILER_PASSWORD", ""), + MailerEmailFrom: envString("MAILER_EMAIL_FROM", ""), } return cfg @@ -85,8 +89,7 @@ func (c *Config) Sanitized() *Config { Port: c.Port, AppTagline: c.AppTagline, - MailerEmailFrom: c.MailerEmailFrom, - MailerEnvelopeFrom: c.MailerEnvelopeFrom, + MailerEmailFrom: c.MailerEmailFrom, } } @@ -111,6 +114,20 @@ func envInt(key string, def int) int { return int(i) } +func envRequiredUint64(key string) uint64 { + if value := os.Getenv(key); value != "" { + i64, err := parseUint64(value) + if err != nil { + slog.Error("config invalid required uint64", "key", key, "value", value, "error", err) + os.Exit(1) + } + return i64 + } + slog.Error("config required uint64 env var missing", "key", key) + os.Exit(1) + return 0 +} + func envDuration(key string, def time.Duration) time.Duration { value, ok := os.LookupEnv(key) if !ok || value == "" { @@ -132,3 +149,7 @@ func envRequired(key string) string { os.Exit(1) return "" } + +func parseUint64(s string) (uint64, error) { + return strconv.ParseUint(s, 10, 64) +} diff --git a/internal/service/email.go b/internal/service/email.go index 73add75..b4280a9 100644 --- a/internal/service/email.go +++ b/internal/service/email.go @@ -126,26 +126,20 @@ func (nc *EmailClient) connectToIMAP() (*client.Client, error) { } type EmailService struct { - client *EmailClient - fromEmail string - fromEnvelope string - supportEmail string - supportEnvelope string - isDev bool - appURL string - appName string + client *EmailClient + fromEmail string + isDev bool + appURL string + appName string } -func NewEmailService(client *EmailClient, fromEmail, fromEnvelope, supportEmail, supportEnvelope, appURL, appName string, isDev bool) *EmailService { +func NewEmailService(client *EmailClient, fromEmail, appURL, appName string, isDev bool) *EmailService { return &EmailService{ - client: client, - fromEmail: fromEmail, - fromEnvelope: fromEnvelope, - supportEmail: supportEmail, - supportEnvelope: supportEnvelope, - isDev: isDev, - appURL: appURL, - appName: appName, + client: client, + fromEmail: fromEmail, + isDev: isDev, + appURL: appURL, + appName: appName, } } @@ -159,10 +153,11 @@ func (s *EmailService) SendMagicLinkEmail(email, token, name string) error { } params := &EmailParams{ - From: s.fromEmail, - To: []string{email}, - Subject: subject, - Text: body, + From: s.fromString(), + EnvelopeFrom: s.fromEmail, + To: []string{email}, + Subject: subject, + Text: body, } _, err := s.client.SendWithContext(context.Background(), params) @@ -172,6 +167,10 @@ func (s *EmailService) SendMagicLinkEmail(email, token, name string) error { return err } +func (s *EmailService) fromString() string { + return fmt.Sprintf("%s <%s>", s.appName, s.fromEmail) +} + func magicLinkEmailTemplate(magicURL, appName string) (string, string) { subject := fmt.Sprintf("Sign in to %s", appName) body := fmt.Sprintf(`Click this link to sign in to your account: