github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/bitwarden/settings/settings.go (about)

     1  package settings
     2  
     3  import (
     4  	"encoding/base64"
     5  	"errors"
     6  
     7  	"github.com/cozy/cozy-stack/model/account"
     8  	"github.com/cozy/cozy-stack/model/instance"
     9  	"github.com/cozy/cozy-stack/model/job"
    10  	"github.com/cozy/cozy-stack/pkg/consts"
    11  	"github.com/cozy/cozy-stack/pkg/couchdb"
    12  	"github.com/cozy/cozy-stack/pkg/crypto"
    13  	"github.com/cozy/cozy-stack/pkg/metadata"
    14  	"github.com/gofrs/uuid/v5"
    15  )
    16  
    17  // DocTypeVersion represents the doctype version. Each time this document
    18  // structure is modified, update this value
    19  const DocTypeVersion = "1"
    20  
    21  // ErrMissingOrgKey is used when the organization key does not exist
    22  var ErrMissingOrgKey = errors.New("No organization key")
    23  
    24  // Settings is the struct that holds the birwarden settings
    25  type Settings struct {
    26  	CouchID                 string                 `json:"_id,omitempty"`
    27  	CouchRev                string                 `json:"_rev,omitempty"`
    28  	PassphraseKdf           int                    `json:"passphrase_kdf,omitempty"`
    29  	PassphraseKdfIterations int                    `json:"passphrase_kdf_iterations,omitempty"`
    30  	PassphraseHint          string                 `json:"passphrase_hint,omitempty"`
    31  	SecurityStamp           string                 `json:"security_stamp,omitempty"`
    32  	Key                     string                 `json:"key,omitempty"`
    33  	PublicKey               string                 `json:"public_key,omitempty"`
    34  	PrivateKey              string                 `json:"private_key,omitempty"`
    35  	EncryptedOrgKey         string                 `json:"encrypted_organization_key,omitempty"`
    36  	OrganizationID          string                 `json:"organization_id,omitempty"`
    37  	CollectionID            string                 `json:"collection_id,omitempty"`
    38  	EquivalentDomains       [][]string             `json:"equivalent_domains,omitempty"`
    39  	GlobalEquivalentDomains []int                  `json:"global_equivalent_domains,omitempty"`
    40  	Metadata                *metadata.CozyMetadata `json:"cozyMetadata,omitempty"`
    41  	ExtensionInstalled      bool                   `json:"extension_installed,omitempty"`
    42  }
    43  
    44  // ID returns the settings qualified identifier
    45  func (s *Settings) ID() string { return s.CouchID }
    46  
    47  // Rev returns the settings revision
    48  func (s *Settings) Rev() string { return s.CouchRev }
    49  
    50  // DocType returns the settings document type
    51  func (s *Settings) DocType() string { return consts.Settings }
    52  
    53  // Clone implements couchdb.Doc
    54  func (s *Settings) Clone() couchdb.Doc {
    55  	cloned := *s
    56  	if s.Metadata != nil {
    57  		cloned.Metadata = s.Metadata.Clone()
    58  	}
    59  	return &cloned
    60  }
    61  
    62  // SetID changes the settings qualified identifier
    63  func (s *Settings) SetID(id string) { s.CouchID = id }
    64  
    65  // SetRev changes the settings revision
    66  func (s *Settings) SetRev(rev string) { s.CouchRev = rev }
    67  
    68  // Save persists the settings document for bitwarden in CouchDB.
    69  func (s *Settings) Save(inst *instance.Instance) error {
    70  	s.CouchID = consts.BitwardenSettingsID
    71  	if s.Metadata == nil {
    72  		md := metadata.New()
    73  		md.DocTypeVersion = DocTypeVersion
    74  		s.Metadata = md
    75  	} else {
    76  		s.Metadata.ChangeUpdatedAt()
    77  	}
    78  	if s.CouchRev == "" {
    79  		return couchdb.CreateNamedDocWithDB(inst, s)
    80  	}
    81  	return couchdb.UpdateDoc(inst, s)
    82  }
    83  
    84  // SetKeyPair is used to save the key pair of the user, that will be used to
    85  // share passwords with the cozy organization.
    86  func (s *Settings) SetKeyPair(inst *instance.Instance, pub, priv string) error {
    87  	s.PublicKey = pub
    88  	s.PrivateKey = priv
    89  	if err := s.EnsureCozyOrganization(inst); err != nil {
    90  		return err
    91  	}
    92  	return couchdb.UpdateDoc(inst, s)
    93  }
    94  
    95  // EnsureCozyOrganization make sure that the settings for the Cozy organization
    96  // are set.
    97  func (s *Settings) EnsureCozyOrganization(inst *instance.Instance) error {
    98  	var err error
    99  	if len(s.EncryptedOrgKey) == 0 {
   100  		orgKey := crypto.GenerateRandomBytes(64)
   101  		b64 := base64.StdEncoding.EncodeToString(orgKey)
   102  		s.EncryptedOrgKey, err = account.EncryptCredentialsData(b64)
   103  		if err != nil {
   104  			return err
   105  		}
   106  	}
   107  	if s.OrganizationID == "" {
   108  		uid, err := uuid.NewV7()
   109  		if err != nil {
   110  			return err
   111  		}
   112  		s.OrganizationID = uid.String()
   113  	}
   114  	if s.CollectionID == "" {
   115  		uid, err := uuid.NewV7()
   116  		if err != nil {
   117  			return err
   118  		}
   119  		s.CollectionID = uid.String()
   120  	}
   121  	return nil
   122  }
   123  
   124  // OrganizationKey returns the organization key (in clear, not encrypted).
   125  func (s *Settings) OrganizationKey() ([]byte, error) {
   126  	if len(s.EncryptedOrgKey) == 0 {
   127  		return nil, ErrMissingOrgKey
   128  	}
   129  	decrypted, err := account.DecryptCredentialsData(s.EncryptedOrgKey)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	b64, ok := decrypted.(string)
   134  	if !ok {
   135  		return nil, errors.New("Invalid key")
   136  	}
   137  	return base64.StdEncoding.DecodeString(b64)
   138  }
   139  
   140  // Get returns the settings document for bitwarden.
   141  func Get(inst *instance.Instance) (*Settings, error) {
   142  	settings := &Settings{}
   143  	err := couchdb.GetDoc(inst, consts.Settings, consts.BitwardenSettingsID, settings)
   144  	if err != nil && !couchdb.IsNotFoundError(err) {
   145  		return nil, err
   146  	}
   147  	return settings, nil
   148  }
   149  
   150  // HasVault returns true if a pass/bitwarden has been used on this instance.
   151  func HasVault(inst *instance.Instance) bool {
   152  	bitwardenSettings, err := Get(inst)
   153  	if err != nil {
   154  		return false
   155  	}
   156  	return bitwardenSettings.ExtensionInstalled
   157  }
   158  
   159  // UpdateRevisionDate updates the updatedAt field of the bitwarden settings
   160  // document. This field is used to know by some clients to know the date of the
   161  // last change on the server before doing a full sync.
   162  func UpdateRevisionDate(inst *instance.Instance, settings *Settings) error {
   163  	var err error
   164  	if settings == nil {
   165  		settings, err = Get(inst)
   166  	}
   167  	if err == nil {
   168  		err = settings.Save(inst)
   169  	}
   170  	if err != nil {
   171  		inst.Logger().WithNamespace("bitwarden").
   172  			Infof("Cannot update revision date: %s", err)
   173  	}
   174  	return err
   175  }
   176  
   177  // MigrateAccountsToCiphers creates a job to copy the konnectors accounts
   178  // inside the bitwarden vault (and set the extension_installed flag).
   179  func MigrateAccountsToCiphers(inst *instance.Instance) error {
   180  	msg, err := job.NewMessage(map[string]interface{}{
   181  		"type": "accounts-to-organization",
   182  	})
   183  	if err != nil {
   184  		return err
   185  	}
   186  	_, err = job.System().PushJob(inst, &job.JobRequest{
   187  		WorkerType: "migrations",
   188  		Message:    msg,
   189  	})
   190  	return err
   191  }
   192  
   193  var _ couchdb.Doc = &Settings{}