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 }