1 package couchdb
2
3 import (
4 "bytes"
5 "context"
6 "crypto/rand"
7 "crypto/sha1"
8 "encoding/hex"
9 "errors"
10 "fmt"
11 "math/big"
12 "regexp"
13
14 "golang.org/x/crypto/pbkdf2"
15
16 corev1 "k8s.io/api/core/v1"
17 k8errors "k8s.io/apimachinery/pkg/api/errors"
18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19
20 "sigs.k8s.io/controller-runtime/pkg/client"
21 )
22
23 const (
24
25 SecretUsername = "username"
26 SecretPassword = "password"
27 SecretAdminsIni = "admins.ini"
28 SecretURI = "uri"
29 SecretDBName = "dbname"
30 SecretCookieName = "cookieAuthSecret"
31
32
33 AdminSecretName = "couchdb-admin-creds"
34 CookieSecretName = "couchdb-cookie"
35 ReplicationSMgrSecretName = "store-couchdb-repl"
36 StoreReplicationSecretName = "couchdb-master-creds"
37 StoreSecretName = "couchdb-local-creds"
38 ReplicationSecretName = "couchdb-replication-creds"
39 )
40
41 var (
42 ErrInvalidCredentialsSecret = errors.New("the credentials secret has missing values")
43 ErrInvalidCookieSecret = errors.New("the cookie secret has missing values")
44 ErrCouchDBURIMissing = errors.New("the secret is missing couchdb URI")
45 ErrDBNameMissing = errors.New("the secret is missing DB Name")
46 ErrSecretDataMissing = errors.New("field missing from secret")
47
48
49 adminRegexStr = `^\[admins\]\n.* = -pbkdf2-[a-z0-9]{40},[a-z0-9]{16},4096\n$`
50 adminRegex = regexp.MustCompile(adminRegexStr)
51 )
52
53
54 type CredentialsManager interface {
55 FromSecret(ctx context.Context, cl client.Client, nn client.ObjectKey) (*corev1.Secret, error)
56 ToSecret(ctx context.Context, cl client.Client, nn client.ObjectKey, ownerRefs ...metav1.OwnerReference) (*corev1.Secret, error)
57 }
58
59 type UsernamePassword struct {
60 Username []byte
61 Password []byte
62 }
63
64
65 type AdminCredentials struct {
66 UsernamePassword
67 CookieAuthSecret []byte
68 PrehashedAdmins []byte
69 }
70
71
72 type UserCredentials struct {
73 UsernamePassword
74 URI []byte
75 }
76
77 type ReplicationCredentials struct {
78 UserCredentials
79 DBName []byte
80 }
81
82
83
84
85 type SecretManagerSecret struct {
86 Username string `json:"username"`
87 Password string `json:"password"`
88 URI string `json:"uri"`
89 DBName string `json:"dbname"`
90 }
91
92
93 func (l *UsernamePassword) GenerateUsernamePassword() {
94 if len(l.Username) == 0 {
95 l.Username = randStr(8)
96 }
97 if len(l.Password) == 0 {
98 l.Password = randStr(16)
99 }
100 }
101
102
103 func (l *UsernamePassword) Parse(secret *corev1.Secret) error {
104
105 e := fmt.Errorf("%w", ErrInvalidCredentialsSecret)
106 username, ok := secret.Data[SecretUsername]
107 if !ok || len(username) == 0 {
108 return fmt.Errorf("%w. missing %s", e, SecretUsername)
109 }
110 l.Username = username
111
112 password, ok := secret.Data[SecretPassword]
113 if !ok || len(password) == 0 {
114 return fmt.Errorf("%w. missing %s", e, SecretPassword)
115 }
116 l.Password = password
117
118 return nil
119 }
120
121
122 func (l *UsernamePassword) FromSecret(ctx context.Context, cl client.Client, nn client.ObjectKey) (*corev1.Secret, error) {
123 secret := &corev1.Secret{}
124 err := cl.Get(ctx, nn, secret)
125 if err != nil {
126 return nil, err
127 }
128 return secret, l.Parse(secret)
129 }
130
131
132 func (l *UserCredentials) Parse(secret *corev1.Secret) error {
133 if err := l.UsernamePassword.Parse(secret); err != nil {
134 return err
135 }
136 uri, ok := secret.Data[SecretURI]
137 if !ok || len(uri) == 0 {
138 return fmt.Errorf("%w. missing %s", ErrCouchDBURIMissing, SecretURI)
139 }
140 if len(l.URI) != 0 && !bytes.Equal(l.URI, uri) {
141 return fmt.Errorf("%s: %w", SecretURI, ErrSecretDataMissing)
142 }
143 l.URI = uri
144 return nil
145 }
146
147 func (c *AdminCredentials) FromSecret(ctx context.Context, cl client.Client, nn client.ObjectKey) (*corev1.Secret, error) {
148
149 secret := &corev1.Secret{}
150 err := cl.Get(ctx, nn, secret)
151 if err != nil {
152 return nil, err
153 }
154
155 err = c.UsernamePassword.Parse(secret)
156 if err != nil {
157 return nil, err
158 }
159
160 phaData, ok := secret.Data[SecretAdminsIni]
161 if !ok || len(phaData) == 0 {
162 hashed, err := pbkdf2Hash(c.Password)
163 if err != nil {
164 return nil, fmt.Errorf("failed to hash admin creds from secret: %v. err: %w", nn, err)
165 }
166 phaData = toAdminIni(string(c.Username), hashed)
167 }
168 match := adminRegex.Match(phaData)
169 if !match {
170 return nil, fmt.Errorf("%w. invalid %s, must match %s", ErrInvalidCredentialsSecret, SecretAdminsIni, adminRegexStr)
171 }
172 c.PrehashedAdmins = phaData
173
174 e := fmt.Errorf("%w", ErrInvalidCookieSecret)
175 data, ok := secret.Data[SecretCookieName]
176 if !ok || len(data) == 0 {
177 return nil, e
178 }
179 c.CookieAuthSecret = data
180
181 return secret, nil
182 }
183
184
185 func (c *AdminCredentials) ToSecret(ctx context.Context, cl client.Client, nn client.ObjectKey, ownerRefs ...metav1.OwnerReference) (*corev1.Secret, error) {
186 c.GenerateUsernamePassword()
187
188 if len(c.PrehashedAdmins) < 1 {
189 hashed, err := pbkdf2Hash(c.Password)
190 if err != nil {
191 return nil, fmt.Errorf("failed to generate prehashed admin secret for credential: %v. err: %v", nn, err)
192 }
193 c.PrehashedAdmins = toAdminIni(string(c.Username), hashed)
194 }
195
196 if len(c.CookieAuthSecret) < 1 {
197 cookieData, err := getCookie(ctx, cl)
198 if err != nil {
199 return nil, fmt.Errorf("failed to get/generate cookie: err: %v", err)
200 }
201 c.CookieAuthSecret = cookieData
202 }
203
204 secret := &corev1.Secret{
205 TypeMeta: metav1.TypeMeta{
206 APIVersion: "v1",
207 Kind: "Secret",
208 },
209 ObjectMeta: metav1.ObjectMeta{
210 Name: nn.Name,
211 Namespace: nn.Namespace,
212 Labels: map[string]string{
213 IgnoreDeletionLabel: "true",
214 },
215 },
216 Data: map[string][]byte{
217 SecretUsername: c.Username,
218 SecretPassword: c.Password,
219 SecretAdminsIni: c.PrehashedAdmins,
220 SecretCookieName: c.CookieAuthSecret,
221 },
222 }
223 if len(ownerRefs) > 0 {
224 secret.OwnerReferences = ownerRefs
225 }
226 return secret, nil
227 }
228
229 func (c *ReplicationCredentials) FromSecret(ctx context.Context, client client.Client, nn client.ObjectKey) (*corev1.Secret, error) {
230
231 secret := &corev1.Secret{}
232 err := client.Get(ctx, nn, secret)
233 if err != nil {
234 return nil, err
235 }
236
237 if err = c.UserCredentials.Parse(secret); err != nil {
238 return nil, err
239 }
240
241 data, ok := secret.Data[SecretDBName]
242 if !ok || len(data) == 0 {
243
244 return nil, ErrDBNameMissing
245 }
246 c.DBName = data
247
248 return secret, nil
249 }
250
251
252 func (c *ReplicationCredentials) ToSecret(_ context.Context, _ client.Client, nn client.ObjectKey, ownerRefs ...metav1.OwnerReference) (*corev1.Secret, error) {
253 e := fmt.Errorf("%w", ErrInvalidCredentialsSecret)
254 if c.DBName == nil {
255 return nil, fmt.Errorf("%w. missing %s", e, SecretDBName)
256 }
257 if c.URI == nil {
258 return nil, fmt.Errorf("%w. missing %s", e, SecretURI)
259 }
260 c.GenerateUsernamePassword()
261 secret := &corev1.Secret{
262 TypeMeta: metav1.TypeMeta{
263 APIVersion: "v1",
264 Kind: "Secret",
265 },
266 ObjectMeta: metav1.ObjectMeta{
267 Name: nn.Name,
268 Namespace: nn.Namespace,
269 },
270 Data: map[string][]byte{
271 SecretUsername: c.Username,
272 SecretPassword: c.Password,
273 SecretURI: c.URI,
274 SecretDBName: c.DBName,
275 },
276 }
277 if len(ownerRefs) > 0 {
278 secret.OwnerReferences = ownerRefs
279 }
280
281 return secret, nil
282 }
283
284 func (l *UserCredentials) FromSecret(ctx context.Context, client client.Client, nn client.ObjectKey) (*corev1.Secret, error) {
285 secret := &corev1.Secret{}
286 if err := client.Get(ctx, nn, secret); err != nil {
287 return nil, err
288 }
289 if err := l.Parse(secret); err != nil {
290 return nil, err
291 }
292 return secret, nil
293 }
294
295 func (l *UserCredentials) ToSecret(_ context.Context, _ client.Client, nn client.ObjectKey, ownerRefs ...metav1.OwnerReference) (*corev1.Secret, error) {
296 l.GenerateUsernamePassword()
297 if len(l.URI) == 0 {
298 return nil, ErrCouchDBURIMissing
299 }
300 secret := &corev1.Secret{
301 TypeMeta: metav1.TypeMeta{
302 APIVersion: "v1",
303 Kind: "Secret",
304 },
305 ObjectMeta: metav1.ObjectMeta{
306 Name: nn.Name,
307 Namespace: nn.Namespace,
308 },
309 Data: map[string][]byte{
310 SecretUsername: l.Username,
311 SecretPassword: l.Password,
312 SecretURI: l.URI,
313 },
314 }
315 if len(ownerRefs) > 0 {
316 secret.OwnerReferences = ownerRefs
317 }
318
319 return secret, nil
320 }
321
322 func getCookie(ctx context.Context, cl client.Client) ([]byte, error) {
323 cookieSecret := &corev1.Secret{}
324 cookieKey := client.ObjectKey{Namespace: Namespace, Name: CookieSecretName}
325 var cookieData []byte
326 if err := cl.Get(ctx, cookieKey, cookieSecret); err != nil {
327 if !k8errors.IsNotFound(err) {
328 return nil, err
329 }
330
331 cookieData = randStr(32)
332 } else {
333 ok := false
334 cookieData, ok = cookieSecret.Data[SecretCookieName]
335 if !ok || len(cookieData) == 0 {
336
337 cookieData = randStr(32)
338 }
339 }
340 return cookieData, nil
341 }
342
343 func randStr(length int) []byte {
344 letterBytes := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
345 b := make([]byte, length)
346 l := int64(len(letterBytes))
347 for i := range b {
348 nBig, err := rand.Int(rand.Reader, big.NewInt(l))
349 if err != nil {
350 panic(err)
351 }
352 n := nBig.Int64()
353 b[i] = letterBytes[n]
354 }
355 return b
356 }
357
358 func pbkdf2Hash(password []byte) (string, error) {
359 salt := make([]byte, 8)
360 n, err := rand.Read(salt)
361 if err != nil {
362 return "", fmt.Errorf("failed to generate random salt: %v", err)
363 }
364 if n != 8 {
365 return "", fmt.Errorf("pbkdf2 err: expected 8 byte salt, was: %v", n)
366 }
367 salt8 := hex.EncodeToString(salt)
368 iter := 4096
369
370 key := pbkdf2.Key(password, []byte(salt8), iter, 20, sha1.New)
371 return fmt.Sprintf("-pbkdf2-%x,%s,%d", key, salt8, iter), nil
372 }
373
374 func toAdminIni(user, hashed string) []byte {
375 return []byte(fmt.Sprintf("[admins]\n%s = %s\n", user, hashed))
376 }
377
View as plain text