...

Source file src/edge-infra.dev/pkg/edge/iam/storage/database/database.go

Documentation: edge-infra.dev/pkg/edge/iam/storage/database

     1  package database
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  	"time"
    11  
    12  	"edge-infra.dev/pkg/edge/iam/config"
    13  	"edge-infra.dev/pkg/edge/iam/crypto"
    14  	iamErrors "edge-infra.dev/pkg/edge/iam/errors"
    15  	"edge-infra.dev/pkg/edge/iam/log"
    16  	"edge-infra.dev/pkg/edge/iam/storage"
    17  
    18  	"github.com/go-kivik/kivik/v4"
    19  	"github.com/go-kivik/kivik/v4/couchdb"
    20  	"github.com/go-logr/logr"
    21  	"github.com/go-redis/redis"
    22  	"github.com/ory/fosite"
    23  	"github.com/pkg/errors"
    24  )
    25  
    26  type (
    27  	KeyPrefix string
    28  	Store     struct {
    29  		Log          logr.Logger
    30  		CouchDB      *kivik.Client
    31  		RedisDB      *redis.Client
    32  		Sessions     *SessionStore
    33  		CouchDBLocal *kivik.Client
    34  		isOffline    bool
    35  	}
    36  	Doc struct {
    37  		ID         string          `json:"_id"`
    38  		Value      json.RawMessage `json:"value"`
    39  		Rev        string          `json:"_rev,omitempty"`
    40  		Expiration int64           `json:"expiration,omitempty"`
    41  	}
    42  	Options func(d *Doc)
    43  )
    44  
    45  const (
    46  	KeyPrefixAuthorizationCode KeyPrefix = "auth-code"
    47  	KeyPrefixAccessToken       KeyPrefix = "access-token"
    48  	KeyPrefixAccessTokenReq    KeyPrefix = "access-token-request"
    49  	KeyPrefixRefreshToken      KeyPrefix = "refresh-token"
    50  	KeyPrefixRefreshTokenReq   KeyPrefix = "refresh-token-request"
    51  	KeyPrefixOpenIDConnect     KeyPrefix = "oidc"
    52  	KeyPrefixPKCE              KeyPrefix = "pkce"
    53  	KeyPrefixClientCreds       KeyPrefix = "client-creds"
    54  	KeyPrefixClientProfile     KeyPrefix = "client-profile"
    55  	KeyPrefixClient            KeyPrefix = "client"
    56  	KeyPrefixPIN               KeyPrefix = "pin"
    57  	KeyPrefixDeviceAccount     KeyPrefix = "device-acct"
    58  	KeyPrefixProfile           KeyPrefix = "profile"
    59  	KeyPrefixAlias             KeyPrefix = "alias"
    60  	KeyPrefixDeviceLogin       KeyPrefix = "device-login"
    61  	KeyPrefixBarcode           KeyPrefix = "barcode"
    62  	KeyPrefixBarcodeCode       KeyPrefix = "barcode-code"
    63  	KeyPrefixBarcodeKey        KeyPrefix = "barcode-key"
    64  	KeyPrefixBarcodeUser       KeyPrefix = "barcode-user"
    65  	KeyPrefixLoginHint         KeyPrefix = "login-hint"
    66  )
    67  
    68  const AccountsDBName = "iam-accounts"
    69  
    70  // set the doc's expiration to the new time
    71  func WithExpiration(ttl time.Duration) Options {
    72  	return func(d *Doc) {
    73  		expiration := time.Now().Unix() + int64(ttl.Seconds())
    74  		d.Expiration = expiration
    75  	}
    76  }
    77  
    78  // NewOperatorStore Store for operator
    79  func NewOperatorStore(log logr.Logger) (*Store, error) {
    80  	couchdb, err := NewCouchDBClient(log)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	return &Store{
    85  		CouchDB: couchdb,
    86  		Log:     log,
    87  	}, err
    88  }
    89  
    90  // NewProviderStore Store for provider
    91  func NewProviderStore(log logr.Logger) (*Store, error) {
    92  	couchdb, err := NewCouchDBClient(log)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	redis, err := NewRedisClient()
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	sessionStore, err := NewRedisSessionStore(context.Background(), redis)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	store := &Store{
   105  		RedisDB:  redis,
   106  		Sessions: sessionStore,
   107  		CouchDB:  couchdb,
   108  		Log:      log,
   109  	}
   110  	if config.IsTouchpoint() {
   111  		var err error
   112  		store.CouchDBLocal, err = LocalCouchDBClient(log)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  		// periodically detect lan outage every 60s
   117  		go store.periodicDetection(time.Second * 60)
   118  	}
   119  	return store, nil
   120  }
   121  
   122  type redisSchema struct {
   123  	ID            string           `json:"id"`
   124  	RequestedAt   time.Time        `json:"requestedAt"`
   125  	ClientID      string           `json:"clientId"`
   126  	Scopes        fosite.Arguments `json:"scopes"`
   127  	GrantedScopes fosite.Arguments `json:"grantedScopes"`
   128  	Form          url.Values       `json:"formData"`
   129  	Session       json.RawMessage  `json:"sessionData"`
   130  	Active        bool             `json:"active"`
   131  }
   132  
   133  // type barcodeSchema struct {
   134  // 	redisSchema
   135  // 	// Credential is a unique generated credential for a barcode generation request.
   136  // 	Credential string `json:"credential"`
   137  // 	// CreatedAt refers to the timestamp when the barcode is generated as per user request.
   138  // 	CreatedAt time.Time `json:"createdAt"`
   139  // 	// ExpiresIn refers to the duration in seconds for which the barcode would be valid after creation.
   140  // 	ExpiresIn int64  `json:"expiresIn"`
   141  // 	Subject   string `json:"subject"`
   142  // }
   143  
   144  func NewCouchDBClient(log logr.Logger) (*kivik.Client, error) {
   145  	couchURI := config.CouchDBAddress()
   146  	couchClient, err := kivik.New("couch", couchURI, couchdb.BasicAuth(config.CouchDBUser(), config.CouchDBPassword()))
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	// do not check db and create repl_doc if it's touchpoint
   152  	if config.IsTouchpoint() {
   153  		return couchClient, err
   154  	}
   155  	exists, err := couchClient.DBExists(context.Background(), AccountsDBName)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	if !exists {
   160  		if err := couchClient.CreateDB(context.Background(), AccountsDBName); err != nil {
   161  			return nil, err
   162  		}
   163  	}
   164  	// check for replication doc existence, and create if needed
   165  	row := couchClient.DB(AccountsDBName).Get(context.Background(), "repl_doc")
   166  	if row.Err() != nil { // nolint
   167  		if kivik.HTTPStatus(row.Err()) == http.StatusNotFound { // nolint
   168  			err := putReplDoc(couchClient.DB(AccountsDBName))
   169  			if err != nil {
   170  				log.Error(err, "failed to PUT repl_doc into database")
   171  				return nil, err
   172  			}
   173  			log.Info("successfully PUT repl_doc into database")
   174  		} else {
   175  			log.Error(row.Err(), "failed to retrieve repl_doc status")
   176  			return nil, err
   177  		}
   178  	} else {
   179  		log.Info("repl_doc exists, no action required")
   180  	}
   181  
   182  	log.Info("connected to store couchdb", "address", couchURI)
   183  	return couchClient, nil
   184  }
   185  
   186  func LocalCouchDBClient(log logr.Logger) (*kivik.Client, error) {
   187  	couchURI := config.CouchDBAddressLocal()
   188  	// get couchdb lane username
   189  	username, err := config.CouchDBUserLocal()
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	// get couchdb lane password
   195  	password, err := config.CouchDBPasswordLocal()
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  	couchClient, err := kivik.New("couch", couchURI, couchdb.BasicAuth(username, password))
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	log.Info("connected to touchpoint couchdb", "address", couchURI)
   204  	return couchClient, nil
   205  }
   206  
   207  // add in the repl_doc to iam-accounts database
   208  func putReplDoc(db *kivik.DB) error {
   209  	datasets := []byte(`{
   210  		"datasets": [{
   211  			"config": {
   212  				"cancel": false,
   213  				"continuous": true,
   214  				"create_target": true,
   215  				"doc_ids": null,
   216  				"filter": "",
   217  				"interval": "60000",
   218  				"query_params": "",
   219  				"selector": "",
   220  				"since_seq": "",
   221  				"source_proxy": "",
   222  				"target_proxy": "",
   223  				"use_checkpoints": false
   224  			},
   225  			"name": "iam-accounts"
   226  		}]
   227  	}`)
   228  	_, err := db.Put(context.Background(), "repl_doc", datasets)
   229  	if err != nil {
   230  		return err
   231  	}
   232  	return nil
   233  }
   234  
   235  func NewRedisClient() (*redis.Client, error) {
   236  	logger := log.Logger()
   237  	redisAddress := config.RedisAddress()
   238  
   239  	db := redis.NewClient(&redis.Options{
   240  		Addr: redisAddress,
   241  	})
   242  
   243  	err := db.Ping().Err()
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	logger.Info("connected to redis", "address", redisAddress)
   248  	return db, nil
   249  }
   250  
   251  func (s *Store) set(key string, request storage.Request, ttl time.Duration) error {
   252  	payload, err := json.Marshal(request)
   253  	if err != nil {
   254  		return errors.WithStack(err)
   255  	}
   256  
   257  	if config.EncryptionEnabled() {
   258  		encryptedValue, err := crypto.EncryptRedis(payload, config.EncryptionKey())
   259  		if err != nil {
   260  			return errors.WithStack(err)
   261  		}
   262  		if err := s.RedisDB.Set(key, encryptedValue, ttl).Err(); err != nil {
   263  			return errors.WithStack(err)
   264  		}
   265  	} else {
   266  		if err := s.RedisDB.Set(key, payload, ttl).Err(); err != nil {
   267  			return errors.WithStack(err)
   268  		}
   269  	}
   270  
   271  	return nil
   272  }
   273  
   274  func (s *Store) get(key string) (*storage.Request, error) {
   275  	resp, err := s.RedisDB.Get(key).Bytes()
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  
   280  	var schema redisSchema
   281  	if config.EncryptionEnabled() { // nolint:nestif
   282  		// if redis data is not encrypted, encrypt it, update it in redis, and continue
   283  		if !isRedisDataEncrypted(resp) {
   284  			ttl := s.RedisDB.TTL(key)
   285  			encryptedVal, err := crypto.EncryptRedis(resp, config.EncryptionKey())
   286  			if err != nil {
   287  				return nil, err
   288  			}
   289  			// update with encrypted value
   290  			err = s.RedisDB.Set(key, encryptedVal, ttl.Val()).Err()
   291  			if err != nil {
   292  				return nil, err
   293  			}
   294  		} else {
   295  			decryptedValue, err := crypto.DecryptRedis(string(resp), config.EncryptionKey())
   296  			if err != nil {
   297  				return nil, err
   298  			}
   299  			resp = decryptedValue
   300  		}
   301  	}
   302  
   303  	if err := json.Unmarshal(resp, &schema); err != nil {
   304  		return nil, err
   305  	}
   306  
   307  	return &storage.Request{
   308  		ID:             schema.ID,
   309  		RequestedAt:    schema.RequestedAt,
   310  		ClientID:       schema.ClientID,
   311  		RequestedScope: schema.Scopes,
   312  		GrantedScope:   schema.GrantedScopes,
   313  		Form:           schema.Form,
   314  		Session:        schema.Session,
   315  		Active:         schema.Active,
   316  	}, nil
   317  }
   318  
   319  // findDoc finds doc with given query
   320  func (s *Store) findDoc(ctx context.Context, query interface{}) ([]*Doc, error) {
   321  	rows := s.CouchDB.DB(AccountsDBName).Find(ctx, query)
   322  	var docs []*Doc
   323  	for rows.Next() {
   324  		var doc *Doc
   325  		if err := rows.ScanDoc(&doc); err != nil {
   326  			return nil, err
   327  		}
   328  		docs = append(docs, doc)
   329  	}
   330  	if rows.Err() != nil {
   331  		return nil, rows.Err()
   332  	}
   333  	return docs, nil
   334  }
   335  
   336  func (s *Store) getRow(ctx context.Context, docID string) *kivik.Document {
   337  	if s.CouchDBLocal != nil && s.IsOffline() {
   338  		return s.CouchDBLocal.DB(AccountsDBName).Get(ctx, docID)
   339  	}
   340  	return s.CouchDB.DB(AccountsDBName).Get(ctx, docID)
   341  }
   342  
   343  // getDoc retrieves doc with given docID
   344  func (s *Store) getDoc(ctx context.Context, docID string) (*Doc, error) {
   345  	// proactively detect lan outage when online
   346  	// because periodicDetection only runs every 60s
   347  	// this allows it failover faster
   348  	if s.CouchDBLocal != nil && !s.IsOffline() {
   349  		s.offlineDetection()
   350  	}
   351  	var row *kivik.Document
   352  	var doc *Doc
   353  
   354  	row = s.getRow(ctx, docID)
   355  
   356  	if row.Err() != nil {
   357  		if kivik.HTTPStatus(row.Err()) == http.StatusNotFound {
   358  			return nil, nil
   359  		}
   360  		s.Log.Error(row.Err(), "failed to retrieve doc: ")
   361  		return nil, row.Err()
   362  	}
   363  
   364  	if err := row.ScanDoc(&doc); err != nil {
   365  		return nil, err
   366  	}
   367  
   368  	// if doc's expiration in unix is smaller the current time => expired => delete, and value is set (not 0)
   369  	if doc.Expiration < time.Now().Unix() && doc.Expiration != 0 {
   370  		if _, err := s.CouchDB.DB(AccountsDBName).Delete(ctx, docID, doc.Rev); err != nil {
   371  			return nil, err
   372  		}
   373  		return nil, nil
   374  	}
   375  
   376  	if config.EncryptionEnabled() && startsWithPrefix(docID) { //nolint
   377  		key := config.EncryptionKey()
   378  		msg := &map[string]string{}
   379  		// MOST of the time this returns a json.UnmarshalTypeError when the data is unencrypted.
   380  		// However, in cases like client-creds, it can unmarshal it without issue (no error)
   381  		// so we also check if (*msg)["EncryptedData"] = "" afterwords to double check and catch all cases
   382  		err := json.Unmarshal(doc.Value, msg)
   383  		if err != nil {
   384  			// if not a json.UnmarshalTypeError, it's an actual error, return
   385  			if _, ok := err.(*json.UnmarshalTypeError); !ok {
   386  				return nil, err
   387  			}
   388  		}
   389  
   390  		encryptedData := (*msg)["EncryptedData"]
   391  		if encryptedData == "" {
   392  			encryptedValue, err := crypto.EncryptJSON(doc.Value, key)
   393  			if err != nil {
   394  				return nil, err
   395  			}
   396  
   397  			doc.Value = encryptedValue
   398  
   399  			// update the doc with the encrypted doc.Value
   400  			if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, docID, doc); err != nil {
   401  				return nil, err
   402  			}
   403  		}
   404  
   405  		value, err := crypto.DecryptJSON(doc.Value, key)
   406  		if err != nil {
   407  			return nil, err
   408  		}
   409  		doc.Value = value
   410  	}
   411  
   412  	return doc, nil
   413  }
   414  
   415  // updateDoc update if already exists, create if not. Optional parameter to add in expiration time
   416  func (s *Store) updateDoc(ctx context.Context, docID string, value []byte, opts ...Options) error {
   417  	var doc *Doc
   418  	var err error
   419  	if doc, err = s.getDoc(ctx, docID); err != nil {
   420  		return err
   421  	}
   422  
   423  	if doc == nil {
   424  		doc = &Doc{}
   425  	}
   426  	doc.ID = docID
   427  
   428  	if config.EncryptionEnabled() && startsWithPrefix(docID) {
   429  		encryptedValue, err := crypto.EncryptJSON(value, config.EncryptionKey())
   430  		if err != nil {
   431  			return err
   432  		}
   433  
   434  		doc.Value = encryptedValue
   435  	} else {
   436  		doc.Value = value
   437  	}
   438  
   439  	// update expiration if added
   440  	for _, o := range opts {
   441  		o(doc)
   442  	}
   443  
   444  	// if we're a touchpoint (have local db) and we're offline, check again
   445  	// cause it takes up to 60 secs to get back online
   446  	if s.CouchDBLocal != nil && s.IsOffline() {
   447  		s.offlineDetection()
   448  
   449  		if s.IsOffline() {
   450  			// we can't update the doc
   451  			return iamErrors.ErrOffline
   452  		}
   453  	}
   454  
   455  	// we can update the doc
   456  	if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, docID, doc); err != nil {
   457  		return err
   458  	}
   459  
   460  	return nil
   461  }
   462  
   463  // copydoc is used when migrating from BSL format (acct@user) to okta (username).
   464  // Copies over contents of old doc to new doc. Mainly used to carry over expiration.
   465  func (s *Store) copyDoc(ctx context.Context, docID string, value []byte, expiration int64) error {
   466  	var doc *Doc
   467  	var err error
   468  	if doc, err = s.getDoc(ctx, docID); err != nil {
   469  		return err
   470  	}
   471  
   472  	if doc == nil {
   473  		doc = &Doc{}
   474  	}
   475  	doc.ID = docID
   476  	doc.Value = value
   477  	doc.Expiration = expiration
   478  
   479  	if !s.IsOffline() {
   480  		if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, docID, doc); err != nil {
   481  			return err
   482  		}
   483  	} else {
   484  		return iamErrors.ErrOffline
   485  	}
   486  
   487  	return nil
   488  }
   489  
   490  // createDoc only creates if not already exists
   491  func (s *Store) createDoc(ctx context.Context, docID string, value []byte, opts ...Options) error {
   492  	var doc *Doc
   493  	if config.EncryptionEnabled() {
   494  		encryptedValue, err := crypto.EncryptJSON(value, config.EncryptionKey())
   495  		if err != nil {
   496  			return err
   497  		}
   498  		doc = &Doc{
   499  			ID:    docID,
   500  			Value: encryptedValue,
   501  		}
   502  	} else {
   503  		doc = &Doc{
   504  			ID:    docID,
   505  			Value: value,
   506  		}
   507  	}
   508  
   509  	// update expiration if added
   510  	for _, o := range opts {
   511  		o(doc)
   512  	}
   513  
   514  	if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, docID, doc); err != nil &&
   515  		kivik.HTTPStatus(err) != http.StatusConflict {
   516  		return err
   517  	}
   518  
   519  	return nil
   520  }
   521  
   522  func (s *Store) deleteDoc(ctx context.Context, docID string) error {
   523  	var doc *Doc
   524  	var err error
   525  	if doc, err = s.getDoc(ctx, docID); err != nil {
   526  		return err
   527  	}
   528  
   529  	if doc == nil {
   530  		// not found, already deleted how?
   531  		return nil
   532  	}
   533  	if _, err := s.CouchDB.DB(AccountsDBName).Delete(ctx, docID, doc.Rev); err != nil {
   534  		return err
   535  	}
   536  
   537  	return nil
   538  }
   539  
   540  func (s *Store) periodicDetection(interval time.Duration) {
   541  	for {
   542  		s.offlineDetection()
   543  		time.Sleep(interval)
   544  	}
   545  }
   546  
   547  func (s *Store) offlineDetection() {
   548  	pingCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
   549  	defer cancel()
   550  	// get status of store db
   551  	online, err := s.CouchDB.Ping(pingCtx)
   552  	if err != nil && !s.IsOffline() {
   553  		s.Log.Error(err, "network outage detected, failing over to touchpoint")
   554  	}
   555  	if online && s.IsOffline() {
   556  		s.Log.Info("network outage recovered, switching back to store")
   557  	}
   558  	s.isOffline = !online
   559  }
   560  
   561  func (s *Store) IsOffline() bool {
   562  	return s.isOffline
   563  }
   564  
   565  func (s *Store) RunOfflineDetection() {
   566  	s.offlineDetection()
   567  }
   568  
   569  // func (s *Store) setBarcodeCode(key string, request storage.BarcodeCodeRequest) error {
   570  // 	payload, err := json.Marshal(request)
   571  // 	if err != nil {
   572  // 		return errors.WithStack(err)
   573  // 	}
   574  // 	if err := s.DB.SetNX(key, string(payload), 0).Err(); err != nil {
   575  // 		return errors.WithStack(err)
   576  // 	}
   577  
   578  // 	return nil
   579  // }
   580  
   581  // func (s *Store) setBarcode(key string, request storage.BarcodeRequest) error {
   582  // 	payload, err := json.Marshal(request)
   583  // 	if err != nil {
   584  // 		return errors.WithStack(err)
   585  // 	}
   586  // 	if err := s.DB.SetNX(key, string(payload), 0).Err(); err != nil {
   587  // 		return errors.WithStack(err)
   588  // 	}
   589  
   590  // 	return nil
   591  // }
   592  
   593  // func (s *Store) getBarcode(key string) (*storage.BarcodeRequest, error) {
   594  // 	resp, err := s.DB.Get(key).Bytes()
   595  // 	if err != nil {
   596  // 		return nil, err
   597  // 	}
   598  
   599  // 	var schema barcodeSchema
   600  // 	if err := json.Unmarshal(resp, &schema); err != nil {
   601  // 		return nil, err
   602  // 	}
   603  
   604  // 	return &storage.BarcodeRequest{
   605  // 		Subject:    schema.Subject,
   606  // 		Credential: schema.Credential,
   607  // 		CreatedAt:  schema.CreatedAt,
   608  // 		ExpiresIn:  schema.ExpiresIn,
   609  // 	}, nil
   610  // }
   611  
   612  // func (s *Store) getBarcodeCode(key string) (*storage.BarcodeCodeRequest, error) {
   613  // 	resp, err := s.DB.Get(key).Bytes()
   614  // 	if err != nil {
   615  // 		return nil, err
   616  // 	}
   617  
   618  // 	var schema barcodeSchema
   619  // 	if err := json.Unmarshal(resp, &schema); err != nil {
   620  // 		return nil, err
   621  // 	}
   622  
   623  // 	return &storage.BarcodeCodeRequest{
   624  // 		Subject:   schema.Subject,
   625  // 		CreatedAt: schema.CreatedAt,
   626  // 		ClientID:  schema.ClientID,
   627  // 	}, nil
   628  // }
   629  
   630  func keyFrom(kp KeyPrefix, v string) string {
   631  	key := fmt.Sprintf("%v:%v", kp, v)
   632  	return key
   633  }
   634  
   635  // updates the couchdb database to use a new key to store encrypted items
   636  func (s *Store) RotateCouchEncryptionKey(ctx context.Context, oldKey []byte, newKey []byte) error {
   637  	rows := s.CouchDB.DB(AccountsDBName).AllDocs(ctx)
   638  
   639  	for rows.Next() {
   640  		id, err := rows.ID()
   641  		if err != nil {
   642  			return err
   643  		}
   644  		// if it does not start with an edge-iam prefix, skip
   645  		if !startsWithPrefix(id) {
   646  			continue
   647  		}
   648  
   649  		// getting the doc, so it'll return unencrypted
   650  		doc, err := s.GetDocWithKey(ctx, id, oldKey)
   651  		if err != nil {
   652  			return err
   653  		}
   654  
   655  		// encrypt it with the new key
   656  		value, err := crypto.EncryptJSON(doc.Value, newKey)
   657  		if err != nil {
   658  			return err
   659  		}
   660  
   661  		doc.Value = value
   662  
   663  		// update the doc
   664  		if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, id, doc); err != nil {
   665  			return err
   666  		}
   667  	}
   668  
   669  	if rows.Err() != nil {
   670  		return rows.Err()
   671  	}
   672  
   673  	return nil
   674  }
   675  
   676  // getDocWithKey retrieves doc with given docID given a specific key
   677  // this function assumes we have encryption enabled and that all current values are in the correct format
   678  // of encryptedValue: [value]
   679  func (s *Store) GetDocWithKey(ctx context.Context, docID string, oldKey []byte) (*Doc, error) {
   680  	var row *kivik.Document
   681  	var doc *Doc
   682  
   683  	row = s.getRow(ctx, docID)
   684  
   685  	if row.Err() != nil {
   686  		if kivik.HTTPStatus(row.Err()) == http.StatusNotFound {
   687  			return nil, nil
   688  		}
   689  		s.Log.Error(row.Err(), "failed to retrieve doc: ")
   690  		return nil, row.Err()
   691  	}
   692  
   693  	if err := row.ScanDoc(&doc); err != nil {
   694  		return nil, err
   695  	}
   696  
   697  	// if doc's expiration in unix is smaller the current time => expired => delete, and value is set (not 0)
   698  	if doc.Expiration < time.Now().Unix() && doc.Expiration != 0 {
   699  		if _, err := s.CouchDB.DB(AccountsDBName).Delete(ctx, docID, doc.Rev); err != nil {
   700  			return nil, err
   701  		}
   702  		return nil, nil
   703  	}
   704  
   705  	value, err := crypto.DecryptJSON(doc.Value, oldKey)
   706  	if err != nil {
   707  		return nil, err
   708  	}
   709  	doc.Value = value
   710  
   711  	return doc, nil
   712  }
   713  
   714  func (s *Store) EncryptCouchDB(ctx context.Context, key []byte) error {
   715  	var doc *Doc
   716  
   717  	rows := s.CouchDB.DB(AccountsDBName).AllDocs(ctx)
   718  
   719  	for rows.Next() {
   720  		id, err := rows.ID()
   721  		if err != nil {
   722  			return err
   723  		}
   724  		// if it does not start with the specific edge-iam prefixes, skip
   725  		if !startsWithPrefix(id) {
   726  			continue
   727  		}
   728  
   729  		row := s.getRow(ctx, id)
   730  
   731  		if row.Err() != nil {
   732  			if kivik.HTTPStatus(row.Err()) == http.StatusNotFound {
   733  				return nil
   734  			}
   735  			s.Log.Error(row.Err(), "failed to retrieve doc: ")
   736  			return row.Err()
   737  		}
   738  
   739  		if err := row.ScanDoc(&doc); err != nil {
   740  			return err
   741  		}
   742  
   743  		// check if data is already encrypted
   744  		encrypted, err := isEncrypted(doc.Value)
   745  		if err != nil {
   746  			return err
   747  		}
   748  
   749  		if !encrypted {
   750  			encryptedValue, err := crypto.EncryptJSON(doc.Value, key)
   751  			if err != nil {
   752  				return err
   753  			}
   754  
   755  			doc.Value = encryptedValue
   756  
   757  			// update the doc with the encrypted doc.Value
   758  			if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, id, doc); err != nil {
   759  				return err
   760  			}
   761  		}
   762  	}
   763  
   764  	if rows.Err() != nil {
   765  		return rows.Err()
   766  	}
   767  
   768  	return nil
   769  }
   770  
   771  func (s *Store) DecryptCouchDB(ctx context.Context, key []byte) error {
   772  	var doc *Doc
   773  
   774  	rows := s.CouchDB.DB(AccountsDBName).AllDocs(ctx)
   775  
   776  	for rows.Next() {
   777  		id, err := rows.ID()
   778  
   779  		if err != nil {
   780  			return err
   781  		}
   782  
   783  		// if it does not start with the specific edge-iam prefixes, skip
   784  		if !startsWithPrefix(id) {
   785  			continue
   786  		}
   787  
   788  		row := s.getRow(ctx, id)
   789  
   790  		if row.Err() != nil {
   791  			if kivik.HTTPStatus(row.Err()) == http.StatusNotFound {
   792  				return nil
   793  			}
   794  			s.Log.Error(row.Err(), "failed to retrieve doc: ")
   795  			return row.Err()
   796  		}
   797  
   798  		if err := row.ScanDoc(&doc); err != nil {
   799  			return err
   800  		}
   801  
   802  		// check if data is encrypted
   803  		encrypted, err := isEncrypted(doc.Value)
   804  		if err != nil {
   805  			return err
   806  		}
   807  
   808  		if encrypted {
   809  			encryptedValue, err := crypto.DecryptJSON(doc.Value, key)
   810  			if err != nil {
   811  				return err
   812  			}
   813  
   814  			doc.Value = encryptedValue
   815  
   816  			// update the doc with the encrypted doc.Value
   817  			if _, err := s.CouchDB.DB(AccountsDBName).Put(ctx, id, doc); err != nil {
   818  				return err
   819  			}
   820  		}
   821  	}
   822  
   823  	if rows.Err() != nil {
   824  		return rows.Err()
   825  	}
   826  
   827  	return nil
   828  }
   829  
   830  // Checks if the doc.Value has already been encrypted
   831  // If we get a json.UnmarshalTypeError, this means it cannot be unmarshalled into a
   832  // &map[string]string{}, which is the format of the EncryptedData. This implies the data we are unmarshalling is unencrypted.
   833  // However some data can fit into this format, so we double check by grabbing the value of 'EncryptedData'.
   834  // If empty -> unencrypted. If it has a value -> encrypted
   835  func isEncrypted(value json.RawMessage) (bool, error) {
   836  	msg := &map[string]string{}
   837  	err := json.Unmarshal(value, msg)
   838  	if err != nil {
   839  		// if error is json.UnmarshalTypeError, this means the data is not encrypted.
   840  		if _, ok := err.(*json.UnmarshalTypeError); ok {
   841  			return false, nil
   842  		}
   843  		return true, err
   844  	}
   845  
   846  	// grab the value from EncryptedData: [value]
   847  	encryptedData := (*msg)["EncryptedData"]
   848  
   849  	// if empty, we are not encrypted as it couldn't find anything
   850  	if encryptedData != "" {
   851  		return true, nil
   852  	}
   853  	return false, nil
   854  }
   855  
   856  // checks if a given id contains a valid prefix for encryption
   857  // all edge-iam prefixes except ones for client-profile and client-creds
   858  func startsWithPrefix(id string) bool {
   859  	allowedPrefixes := []KeyPrefix{KeyPrefixAuthorizationCode, KeyPrefixAccessToken,
   860  		KeyPrefixAccessTokenReq, KeyPrefixRefreshToken, KeyPrefixRefreshTokenReq, KeyPrefixOpenIDConnect,
   861  		KeyPrefixPKCE, KeyPrefixPIN, KeyPrefixProfile, KeyPrefixAlias, KeyPrefixBarcode,
   862  		KeyPrefixBarcodeCode, KeyPrefixBarcodeKey, KeyPrefixBarcodeUser, KeyPrefixLoginHint}
   863  
   864  	for _, prefix := range allowedPrefixes {
   865  		if strings.HasPrefix(id, string(prefix)) {
   866  			return true
   867  		}
   868  	}
   869  	return false
   870  }
   871  

View as plain text