github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/worker/migrations/migrate-accounts-to-organisation.go (about)

     1  package migrations
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/cozy/cozy-stack/model/account"
     7  	"github.com/cozy/cozy-stack/model/app"
     8  	"github.com/cozy/cozy-stack/model/bitwarden"
     9  	"github.com/cozy/cozy-stack/model/bitwarden/settings"
    10  	"github.com/cozy/cozy-stack/model/instance"
    11  	"github.com/cozy/cozy-stack/model/job"
    12  	"github.com/cozy/cozy-stack/pkg/config/config"
    13  	"github.com/cozy/cozy-stack/pkg/consts"
    14  	"github.com/cozy/cozy-stack/pkg/couchdb"
    15  	"github.com/cozy/cozy-stack/pkg/crypto"
    16  	"github.com/cozy/cozy-stack/pkg/logger"
    17  	"github.com/cozy/cozy-stack/pkg/metadata"
    18  
    19  	multierror "github.com/hashicorp/go-multierror"
    20  )
    21  
    22  type vaultReference struct {
    23  	ID       string `json:"_id"`
    24  	Type     string `json:"_type"`
    25  	Protocol string `json:"_protocol"`
    26  }
    27  
    28  func isAdditionalField(fieldName string) bool {
    29  	return !(fieldName == "login" ||
    30  		fieldName == "password" ||
    31  		fieldName == "advancedFields")
    32  }
    33  
    34  // Builds a cipher from an io.cozy.account
    35  //
    36  // A raw JSONDoc is used to be able to access auth.fields
    37  func buildCipher(orgKey []byte, manifest *app.KonnManifest, account couchdb.JSONDoc, url string, log *logger.Entry) (*bitwarden.Cipher, error) {
    38  	log.Infof("Building ciphers...")
    39  
    40  	auth, _ := account.M["auth"].(map[string]interface{})
    41  
    42  	username, _ := auth["login"].(string)
    43  	password, _ := auth["password"].(string)
    44  	email, _ := auth["email"].(string)
    45  
    46  	// Special case if the email field is used instead of login
    47  	if username == "" && email != "" {
    48  		username = email
    49  	}
    50  
    51  	key := orgKey[:32]
    52  	hmac := orgKey[32:]
    53  
    54  	ivURL := crypto.GenerateRandomBytes(16)
    55  	encURL, err := crypto.EncryptWithAES256HMAC(key, hmac, []byte(url), ivURL)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	u := bitwarden.LoginURI{URI: encURL, Match: nil}
    60  	uris := []bitwarden.LoginURI{u}
    61  
    62  	ivName := crypto.GenerateRandomBytes(16)
    63  	encName, err := crypto.EncryptWithAES256HMAC(key, hmac, []byte(manifest.Name()), ivName)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	ivUsername := crypto.GenerateRandomBytes(16)
    69  	encUsername, err := crypto.EncryptWithAES256HMAC(key, hmac, []byte(username), ivUsername)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	ivPassword := crypto.GenerateRandomBytes(16)
    75  	encPassword, err := crypto.EncryptWithAES256HMAC(key, hmac, []byte(password), ivPassword)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	login := &bitwarden.LoginData{
    81  		Username: encUsername,
    82  		Password: encPassword,
    83  		URIs:     uris,
    84  	}
    85  
    86  	md := metadata.New()
    87  	md.DocTypeVersion = bitwarden.DocTypeVersion
    88  
    89  	bitwardenFields := make([]bitwarden.Field, 0)
    90  
    91  	for name, rawValue := range auth {
    92  		value, ok := rawValue.(string)
    93  		if !ok {
    94  			continue
    95  		}
    96  		if !isAdditionalField(name) {
    97  			continue
    98  		}
    99  
   100  		ivName := crypto.GenerateRandomBytes(16)
   101  		encName, err := crypto.EncryptWithAES256HMAC(key, hmac, []byte(name), ivName)
   102  		if err != nil {
   103  			return nil, err
   104  		}
   105  
   106  		ivValue := crypto.GenerateRandomBytes(16)
   107  		encValue, err := crypto.EncryptWithAES256HMAC(key, hmac, []byte(value), ivValue)
   108  		if err != nil {
   109  			return nil, err
   110  		}
   111  
   112  		field := bitwarden.Field{
   113  			Name:  encName,
   114  			Value: encValue,
   115  			Type:  bitwarden.FieldTypeText,
   116  		}
   117  		bitwardenFields = append(bitwardenFields, field)
   118  	}
   119  
   120  	c := bitwarden.Cipher{
   121  		Type:           bitwarden.LoginType,
   122  		Name:           encName,
   123  		Login:          login,
   124  		SharedWithCozy: true,
   125  		Metadata:       md,
   126  		Fields:         bitwardenFields,
   127  	}
   128  	return &c, nil
   129  }
   130  
   131  func getCipherLinkFromManifest(manifest *app.KonnManifest) (string, error) {
   132  	link, ok := manifest.VendorLink().(string)
   133  	if !ok {
   134  		return "", nil
   135  	}
   136  	link = strings.Trim(link, "'")
   137  	return link, nil
   138  }
   139  
   140  func updateSettings(inst *instance.Instance, attempt int, log *logger.Entry) error {
   141  	log.Infof("Updating bitwarden settings after migration...")
   142  	// Reload the setting in case the revision changed
   143  	setting, err := settings.Get(inst)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	// This flag is checked at the extension pre-login to run the migration or not
   148  	setting.ExtensionInstalled = true
   149  	err = settings.UpdateRevisionDate(inst, setting)
   150  	if err != nil {
   151  		if couchdb.IsConflictError(err) && attempt < 2 {
   152  			return updateSettings(inst, attempt+1, log)
   153  		}
   154  	}
   155  	return nil
   156  }
   157  
   158  func addCipherRelationshipToAccount(acc couchdb.JSONDoc, cipher *bitwarden.Cipher) {
   159  	vRef := vaultReference{
   160  		ID:       cipher.ID(),
   161  		Type:     consts.BitwardenCiphers,
   162  		Protocol: consts.BitwardenProtocol,
   163  	}
   164  
   165  	relationships, ok := acc.M["relationships"].(map[string]interface{})
   166  	if !ok {
   167  		relationships = make(map[string]interface{})
   168  	}
   169  
   170  	rel := map[string]vaultReference{"data": vRef}
   171  
   172  	relationships[consts.BitwardenCipherRelationship] = rel
   173  
   174  	acc.M["relationships"] = relationships
   175  }
   176  
   177  // Migrates all the encrypted accounts to Bitwarden ciphers.
   178  // It decrypts each account, reencrypt the fields with the organization key,
   179  // and save it in the ciphers database.
   180  func migrateAccountsToOrganization(domain string) error {
   181  	inst, err := instance.Get(domain)
   182  	if err != nil {
   183  		return err
   184  	}
   185  	mu := config.Lock().ReadWrite(inst, "migrate-accounts")
   186  	if err := mu.Lock(); err != nil {
   187  		return err
   188  	}
   189  	defer mu.Unlock()
   190  	log := inst.Logger().WithNamespace("migration")
   191  
   192  	setting, err := settings.Get(inst)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	if setting.ExtensionInstalled {
   197  		// The migration has already been run
   198  		return nil
   199  	}
   200  
   201  	// Get org key
   202  	if err := setting.EnsureCozyOrganization(inst); err != nil {
   203  		return err
   204  	}
   205  	orgKey, err := setting.OrganizationKey()
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	// Iterate over all triggers to get the konnectors with the associated account
   211  	jobsSystem := job.System()
   212  	triggers, err := jobsSystem.GetAllTriggers(inst)
   213  	if err != nil {
   214  		return err
   215  	}
   216  	var msg struct {
   217  		Account string `json:"account"`
   218  		Slug    string `json:"konnector"`
   219  	}
   220  
   221  	var errm error
   222  	for _, t := range triggers {
   223  		if t.Infos().WorkerType != "konnector" {
   224  			continue
   225  		}
   226  		err := t.Infos().Message.Unmarshal(&msg)
   227  		if err != nil || msg.Account == "" || msg.Slug == "" {
   228  			continue
   229  		}
   230  
   231  		manifest, err := app.GetKonnectorBySlug(inst, msg.Slug)
   232  		if err != nil {
   233  			log.Warnf("Could not get manifest for %s", msg.Slug)
   234  			continue
   235  		}
   236  
   237  		link, err := getCipherLinkFromManifest(manifest)
   238  		if err != nil {
   239  			errm = multierror.Append(errm, err)
   240  			continue
   241  		}
   242  
   243  		if link == "" {
   244  			log.Warnf("No vendor_link in manifest for %s", msg.Slug)
   245  			continue
   246  		}
   247  
   248  		var accJSON couchdb.JSONDoc
   249  
   250  		if err := couchdb.GetDoc(inst, consts.Accounts, msg.Account, &accJSON); err != nil {
   251  			errm = multierror.Append(errm, err)
   252  			continue
   253  		}
   254  
   255  		accJSON.Type = consts.Accounts
   256  
   257  		account.Decrypt(accJSON)
   258  
   259  		cipher, err := buildCipher(orgKey, manifest, accJSON, link, log)
   260  		if err != nil {
   261  			errm = multierror.Append(errm, err)
   262  			continue
   263  		}
   264  		if err := couchdb.CreateDoc(inst, cipher); err != nil {
   265  			errm = multierror.Append(errm, err)
   266  			continue
   267  		}
   268  
   269  		addCipherRelationshipToAccount(accJSON, cipher)
   270  
   271  		account.Encrypt(accJSON)
   272  
   273  		log.Infof("Updating doc %s", accJSON)
   274  		if err := couchdb.UpdateDoc(inst, &accJSON); err != nil {
   275  			errm = multierror.Append(errm, err)
   276  			continue
   277  		}
   278  	}
   279  
   280  	err = updateSettings(inst, 0, log)
   281  	if err != nil {
   282  		errm = multierror.Append(errm, err)
   283  	}
   284  	return errm
   285  }