1// Package secrets implements the secret management use case.
2package secrets
3
4import (
5 "context"
6 "errors"
7 "strings"
8
9 "github.com/bnema/zerowrap"
10
11 "gordon/internal/boundaries/out"
12)
13
14// Domain validation errors.
15var (
16 ErrDomainEmpty = errors.New("domain cannot be empty")
17 ErrDomainTooLong = errors.New("domain exceeds maximum length of 253 characters")
18 ErrDomainPathTraversal = errors.New("domain contains path traversal sequence")
19 ErrDomainInvalidChars = errors.New("domain contains invalid characters")
20)
21
22// Service implements the SecretService interface.
23type Service struct {
24 store out.DomainSecretStore
25 log zerowrap.Logger
26}
27
28// NewService creates a new secrets service.
29func NewService(store out.DomainSecretStore, log zerowrap.Logger) *Service {
30 return &Service{
31 store: store,
32 log: log,
33 }
34}
35
36// ListKeys returns the list of secret keys for a domain (not values).
37func (s *Service) ListKeys(ctx context.Context, domain string) ([]string, error) {
38 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
39 zerowrap.FieldLayer: "usecase",
40 zerowrap.FieldUseCase: "ListKeys",
41 "domain": domain,
42 })
43 log := zerowrap.FromCtx(ctx)
44
45 if err := ValidateDomain(domain); err != nil {
46 log.Warn().Err(err).Msg("domain validation failed")
47 return nil, err
48 }
49
50 keys, err := s.store.ListKeys(domain)
51 if err != nil {
52 log.Error().Err(err).Msg("failed to list secret keys")
53 return nil, err
54 }
55
56 log.Debug().Int("count", len(keys)).Msg("listed secret keys")
57 return keys, nil
58}
59
60// GetAll returns all secrets for a domain as a key-value map.
61func (s *Service) GetAll(ctx context.Context, domain string) (map[string]string, error) {
62 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
63 zerowrap.FieldLayer: "usecase",
64 zerowrap.FieldUseCase: "GetAll",
65 "domain": domain,
66 })
67 log := zerowrap.FromCtx(ctx)
68
69 if err := ValidateDomain(domain); err != nil {
70 log.Warn().Err(err).Msg("domain validation failed")
71 return nil, err
72 }
73
74 secrets, err := s.store.GetAll(domain)
75 if err != nil {
76 log.Error().Err(err).Msg("failed to get secrets")
77 return nil, err
78 }
79
80 log.Debug().Int("count", len(secrets)).Msg("retrieved secrets")
81 return secrets, nil
82}
83
84// Set sets or updates multiple secrets for a domain, merging with existing.
85func (s *Service) Set(ctx context.Context, domain string, secrets map[string]string) error {
86 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
87 zerowrap.FieldLayer: "usecase",
88 zerowrap.FieldUseCase: "Set",
89 "domain": domain,
90 })
91 log := zerowrap.FromCtx(ctx)
92
93 if err := ValidateDomain(domain); err != nil {
94 log.Warn().Err(err).Msg("domain validation failed")
95 return err
96 }
97
98 if err := s.store.Set(domain, secrets); err != nil {
99 log.Error().Err(err).Msg("failed to set secrets")
100 return err
101 }
102
103 log.Info().Int("count", len(secrets)).Msg("secrets set")
104 return nil
105}
106
107// Delete removes a specific secret key from a domain.
108func (s *Service) Delete(ctx context.Context, domain, key string) error {
109 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
110 zerowrap.FieldLayer: "usecase",
111 zerowrap.FieldUseCase: "Delete",
112 "domain": domain,
113 "key": key,
114 })
115 log := zerowrap.FromCtx(ctx)
116
117 if err := ValidateDomain(domain); err != nil {
118 log.Warn().Err(err).Msg("domain validation failed")
119 return err
120 }
121
122 if err := s.store.Delete(domain, key); err != nil {
123 log.Error().Err(err).Msg("failed to delete secret")
124 return err
125 }
126
127 log.Info().Msg("secret deleted")
128 return nil
129}
130
131// ValidateDomain validates that a domain is safe to use for secret storage.
132// This prevents path traversal attacks and ensures the domain is valid.
133func ValidateDomain(domain string) error {
134 // Check for empty domain
135 if domain == "" {
136 return ErrDomainEmpty
137 }
138
139 // Check domain length (max DNS name length is 253)
140 if len(domain) > 253 {
141 return ErrDomainTooLong
142 }
143
144 // Check for path traversal attempts
145 if strings.Contains(domain, "..") {
146 return ErrDomainPathTraversal
147 }
148
149 // Check for null bytes (can cause issues in file paths)
150 if strings.ContainsRune(domain, '\x00') {
151 return ErrDomainInvalidChars
152 }
153
154 return nil
155}