...

Source file src/edge-infra.dev/pkg/edge/datasync/couchdb/secrets.go

Documentation: edge-infra.dev/pkg/edge/datasync/couchdb

     1  package couchdb
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/rand"
     7  	"crypto/sha1" // nolint:gosec // SHA1 PRF for PBKDF2-HMAC-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  	// secret data keys
    25  	SecretUsername   = "username"
    26  	SecretPassword   = "password"
    27  	SecretAdminsIni  = "admins.ini"
    28  	SecretURI        = "uri"
    29  	SecretDBName     = "dbname"
    30  	SecretCookieName = "cookieAuthSecret"
    31  
    32  	// secret names
    33  	AdminSecretName            = "couchdb-admin-creds"       //nolint:gosec // default admin secret name
    34  	CookieSecretName           = "couchdb-cookie"            //nolint:gosec // default cookie secret name
    35  	ReplicationSMgrSecretName  = "store-couchdb-repl"        //nolint:gosec // secret name only
    36  	StoreReplicationSecretName = "couchdb-master-creds"      //nolint:gosec // secret name only
    37  	StoreSecretName            = "couchdb-local-creds"       //nolint:gosec // secret name only
    38  	ReplicationSecretName      = "couchdb-replication-creds" //nolint:gosec // secret name only
    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  	// PBKDF2-HMAC-SHA1 with 20-byte keys, output size is deterministic, lazily verify username part
    49  	adminRegexStr = `^\[admins\]\n.* = -pbkdf2-[a-z0-9]{40},[a-z0-9]{16},4096\n$`
    50  	adminRegex    = regexp.MustCompile(adminRegexStr)
    51  )
    52  
    53  // CredentialsManager generate and retrieves couchdb related secret
    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  // AdminCredentials admin credentials for couchdb server
    65  type AdminCredentials struct {
    66  	UsernamePassword
    67  	CookieAuthSecret []byte
    68  	PrehashedAdmins  []byte
    69  }
    70  
    71  // UserCredentials a user with limited permission to be used by workload team, etc...
    72  type UserCredentials struct {
    73  	UsernamePassword
    74  	URI []byte // URL for couchdb server
    75  }
    76  
    77  type ReplicationCredentials struct {
    78  	UserCredentials
    79  	DBName []byte // replication database i.e `repl-hash`
    80  }
    81  
    82  // SecretManagerSecret is the string encoded version of SecretManagerSecret.
    83  // json.Marshall encodes []bytes as base64 encoded strings, so a string representation is required
    84  // to interoperate.
    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  // GenerateUsernamePassword random username and password
    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  // Parse common secret fields parsing for UsernamePassword
   103  func (l *UsernamePassword) Parse(secret *corev1.Secret) error {
   104  	// validate the secret data
   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  // FromSecret based for all couchdb secrets:
   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  // Parse common secret fields parsing for UserCredentials
   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  	// try to get the secret
   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  // ToSecret attempts to create a new credentials secret with generated strings
   185  func (c *AdminCredentials) ToSecret(ctx context.Context, cl client.Client, nn client.ObjectKey, ownerRefs ...metav1.OwnerReference) (*corev1.Secret, error) {
   186  	c.GenerateUsernamePassword()
   187  	// generate hash if missing
   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  	// try to get the secret
   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  		// TODO handle backward compatibility
   244  		return nil, ErrDBNameMissing
   245  	}
   246  	c.DBName = data
   247  
   248  	return secret, nil
   249  }
   250  
   251  // ToSecret attempts to create a new credentials secret with generated strings
   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  		// generate new cookie secret if secret not found
   331  		cookieData = randStr(32)
   332  	} else {
   333  		ok := false
   334  		cookieData, ok = cookieSecret.Data[SecretCookieName]
   335  		if !ok || len(cookieData) == 0 {
   336  			// generate new cookie secret if secret is empty
   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  	// couchdb uses PBKDF2-HMAC-SHA1 with 20-byte keys
   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