github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/bitwarden/bitwarden.go (about) 1 // Package bitwarden exposes an API compatible with the Bitwarden Open-Soure apps. 2 package bitwarden 3 4 import ( 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 "strings" 10 "time" 11 12 "github.com/cozy/cozy-stack/model/app" 13 "github.com/cozy/cozy-stack/model/bitwarden" 14 "github.com/cozy/cozy-stack/model/bitwarden/settings" 15 "github.com/cozy/cozy-stack/model/instance" 16 "github.com/cozy/cozy-stack/model/instance/lifecycle" 17 "github.com/cozy/cozy-stack/model/oauth" 18 "github.com/cozy/cozy-stack/model/permission" 19 "github.com/cozy/cozy-stack/model/session" 20 "github.com/cozy/cozy-stack/pkg/config/config" 21 "github.com/cozy/cozy-stack/pkg/consts" 22 "github.com/cozy/cozy-stack/pkg/couchdb" 23 "github.com/cozy/cozy-stack/web/middlewares" 24 "github.com/labstack/echo/v4" 25 ) 26 27 // Prelogin tells to the client how many KDF iterations it must apply when 28 // hashing the master password. 29 func Prelogin(c echo.Context) error { 30 inst := middlewares.GetInstance(c) 31 setting, err := settings.Get(inst) 32 if err != nil { 33 return err 34 } 35 oidc := inst.HasForcedOIDC() 36 hasCiphers := true 37 if resp, err := couchdb.NormalDocs(inst, consts.BitwardenCiphers, 0, 1, "", false); err == nil { 38 hasCiphers = resp.Total > 0 39 } 40 flat := config.GetConfig().Subdomains == config.FlatSubdomains 41 return c.JSON(http.StatusOK, echo.Map{ 42 "Kdf": setting.PassphraseKdf, 43 "KdfIterations": setting.PassphraseKdfIterations, 44 "OIDC": oidc, 45 "HasCiphers": hasCiphers, 46 "FlatSubdomains": flat, 47 }) 48 } 49 50 // SendHint is the handler for sending the hint when the user has forgot their 51 // password. 52 func SendHint(c echo.Context) error { 53 i := middlewares.GetInstance(c) 54 return lifecycle.SendHint(i) 55 } 56 57 // GetProfile is the handler for the route to get profile information. 58 func GetProfile(c echo.Context) error { 59 inst := middlewares.GetInstance(c) 60 if err := middlewares.AllowWholeType(c, permission.GET, consts.BitwardenProfiles); err != nil { 61 return c.JSON(http.StatusUnauthorized, echo.Map{ 62 "error": "invalid token", 63 }) 64 } 65 setting, err := settings.Get(inst) 66 if err != nil { 67 return err 68 } 69 profile, err := newProfileResponse(inst, setting) 70 if err != nil { 71 return err 72 } 73 return c.JSON(http.StatusOK, profile) 74 } 75 76 // UpdateProfile is the handler for the route to update the profile. Currently, 77 // only the hint for the master password can be changed. 78 func UpdateProfile(c echo.Context) error { 79 inst := middlewares.GetInstance(c) 80 if err := middlewares.AllowWholeType(c, permission.PUT, consts.BitwardenProfiles); err != nil { 81 return c.JSON(http.StatusUnauthorized, echo.Map{ 82 "error": "invalid token", 83 }) 84 } 85 86 var data struct { 87 Hint string `json:"masterPasswordHint"` 88 } 89 if err := json.NewDecoder(c.Request().Body).Decode(&data); err != nil { 90 return c.JSON(http.StatusUnauthorized, echo.Map{ 91 "error": "invalid JSON payload", 92 }) 93 } 94 setting, err := settings.Get(inst) 95 if err != nil { 96 return err 97 } 98 setting.PassphraseHint = data.Hint 99 if err := setting.Save(inst); err != nil { 100 return err 101 } 102 profile, err := newProfileResponse(inst, setting) 103 if err != nil { 104 return err 105 } 106 return c.JSON(http.StatusOK, profile) 107 } 108 109 // SetKeyPair is the handler for setting the key pair: public and private keys. 110 func SetKeyPair(c echo.Context) error { 111 inst := middlewares.GetInstance(c) 112 log := inst.Logger().WithNamespace("bitwarden") 113 if err := middlewares.AllowWholeType(c, permission.POST, consts.BitwardenProfiles); err != nil { 114 return c.JSON(http.StatusUnauthorized, echo.Map{ 115 "error": "invalid token", 116 }) 117 } 118 119 var data struct { 120 Private string `json:"encryptedPrivateKey"` 121 Public string `json:"publicKey"` 122 } 123 if err := json.NewDecoder(c.Request().Body).Decode(&data); err != nil { 124 return c.JSON(http.StatusUnauthorized, echo.Map{ 125 "error": "invalid JSON payload", 126 }) 127 } 128 setting, err := settings.Get(inst) 129 if err != nil { 130 return err 131 } 132 if err := setting.SetKeyPair(inst, data.Public, data.Private); err != nil { 133 log.Errorf("Cannot set key pair: %s", err) 134 return err 135 } 136 profile, err := newProfileResponse(inst, setting) 137 if err != nil { 138 return err 139 } 140 return c.JSON(http.StatusOK, profile) 141 } 142 143 // ChangeSecurityStamp is used by the client to change the security stamp, 144 // which will deconnect all the clients. 145 func ChangeSecurityStamp(c echo.Context) error { 146 inst := middlewares.GetInstance(c) 147 var data struct { 148 Hashed string `json:"masterPasswordHash"` 149 } 150 if err := json.NewDecoder(c.Request().Body).Decode(&data); err != nil { 151 return c.JSON(http.StatusUnauthorized, echo.Map{ 152 "error": "invalid JSON payload", 153 }) 154 } 155 156 if err := instance.CheckPassphrase(inst, []byte(data.Hashed)); err != nil { 157 return c.JSON(http.StatusUnauthorized, echo.Map{ 158 "error": "invalid masterPasswordHash", 159 }) 160 } 161 162 setting, err := settings.Get(inst) 163 if err != nil { 164 return err 165 } 166 setting.SecurityStamp = lifecycle.NewSecurityStamp() 167 if err := setting.Save(inst); err != nil { 168 return err 169 } 170 return c.NoContent(http.StatusNoContent) 171 } 172 173 // GetRevisionDate returns the date of the last synchronization (as a number of 174 // milliseconds). 175 func GetRevisionDate(c echo.Context) error { 176 inst := middlewares.GetInstance(c) 177 if err := middlewares.AllowWholeType(c, permission.GET, consts.BitwardenProfiles); err != nil { 178 return c.JSON(http.StatusUnauthorized, echo.Map{ 179 "error": "invalid token", 180 }) 181 } 182 setting, err := settings.Get(inst) 183 if err != nil { 184 return err 185 } 186 187 at := setting.Metadata.UpdatedAt 188 milliseconds := fmt.Sprintf("%d", at.UnixNano()/1000000) 189 return c.Blob(http.StatusOK, "text/plain", []byte(milliseconds)) 190 } 191 192 // GetToken is used by the clients to get an access token. There are two 193 // supported grant types: password and refresh_token. Password is used the 194 // first time to register the client, and gets the initial credentials, by 195 // sending a hash of the user password. Refresh token is used later to get 196 // a new access token by sending the refresh token. 197 func GetToken(c echo.Context) error { 198 inst := middlewares.GetInstance(c) 199 copier := app.Copier(consts.WebappType, inst) 200 _, err := app.GetWebappBySlugAndUpdate(inst, consts.PassSlug, copier, inst.Registries()) 201 if err != nil { 202 installer, err := app.NewInstaller(inst, copier, 203 &app.InstallerOptions{ 204 Operation: app.Install, 205 Type: consts.WebappType, 206 SourceURL: "registry://" + consts.PassSlug, 207 Slug: consts.PassSlug, 208 Registries: inst.Registries(), 209 }, 210 ) 211 if err == nil { 212 _, _ = installer.RunSync() 213 } 214 } 215 216 switch c.FormValue("grant_type") { 217 case "password": 218 return getInitialCredentials(c) 219 case "refresh_token": 220 return refreshToken(c) 221 case "": 222 return c.JSON(http.StatusBadRequest, echo.Map{ 223 "error": "the grant_type parameter is mandatory", 224 }) 225 default: 226 return c.JSON(http.StatusBadRequest, echo.Map{ 227 "error": "invalid grant type", 228 }) 229 } 230 } 231 232 // AccessTokenReponse is the stuct used for serializing to JSON the response 233 // for an access token. 234 type AccessTokenReponse struct { 235 ClientID string `json:"client_id,omitempty"` 236 RegToken string `json:"registration_access_token,omitempty"` 237 Type string `json:"token_type"` 238 ExpiresIn int `json:"expires_in"` 239 Access string `json:"access_token"` 240 Refresh string `json:"refresh_token"` 241 Key string `json:"Key"` 242 PrivateKey interface{} `json:"PrivateKey"` 243 Kdf int `json:"Kdf"` 244 Iterations int `json:"KdfIterations"` 245 } 246 247 func getInitialCredentials(c echo.Context) error { 248 inst := middlewares.GetInstance(c) 249 log := inst.Logger().WithNamespace("bitwarden") 250 pass := []byte(c.FormValue("password")) 251 252 // Authentication 253 if err := instance.CheckPassphrase(inst, pass); err != nil { 254 return c.JSON(http.StatusUnauthorized, echo.Map{ 255 "error": "invalid password", 256 }) 257 } 258 259 if inst.HasAuthMode(instance.TwoFactorMail) { 260 if !checkTwoFactor(c, inst) { 261 return nil 262 } 263 } 264 265 // Register the client 266 kind := bitwarden.ParseBitwardenDeviceType(c.FormValue("deviceType")) 267 clientName := c.FormValue("clientName") 268 if clientName == "" { 269 clientName = "Bitwarden " + c.FormValue("deviceName") 270 } 271 client := &oauth.Client{ 272 RedirectURIs: []string{"https://cozy.io/"}, 273 ClientName: clientName, 274 ClientKind: kind, 275 SoftwareID: "registry://" + consts.PassSlug, 276 } 277 if err := client.Create(inst, oauth.NotPending); err != nil { 278 return c.JSON(err.Code, err) 279 } 280 client.CouchID = client.ClientID 281 if _, ok := middlewares.GetSession(c); !ok { 282 if err := session.SendNewRegistrationNotification(inst, client.ClientID); err != nil { 283 return c.JSON(http.StatusInternalServerError, echo.Map{ 284 "error": err.Error(), 285 }) 286 } 287 } 288 289 // Create the credentials 290 access, err := bitwarden.CreateAccessJWT(inst, client) 291 if err != nil { 292 return c.JSON(http.StatusInternalServerError, echo.Map{ 293 "error": "Can't generate access token", 294 }) 295 } 296 refresh, err := bitwarden.CreateRefreshJWT(inst, client) 297 if err != nil { 298 return c.JSON(http.StatusInternalServerError, echo.Map{ 299 "error": "Can't generate refresh token", 300 }) 301 } 302 setting, err := settings.Get(inst) 303 if err != nil { 304 return err 305 } 306 key := setting.Key 307 308 if _, err := setting.OrganizationKey(); errors.Is(err, settings.ErrMissingOrgKey) { 309 // The organization key should exist at this moment as it is created at the 310 // instance creation or at the login-hashed migration. 311 log.Warnf("Organization key does not exist") 312 err := setting.EnsureCozyOrganization(inst) 313 if err != nil { 314 return err 315 } 316 err = couchdb.UpdateDoc(inst, setting) 317 if err != nil { 318 return err 319 } 320 } 321 if client.ClientKind != "web" && !setting.ExtensionInstalled { 322 // This is the first time the bitwarden extension is installed: make sure 323 // the user gets the existing accounts into the vault. 324 // ClientKind is "web" for web apps, e.g. Settings 325 if err := settings.MigrateAccountsToCiphers(inst); err != nil { 326 log.Errorf("Cannot push job for ciphers migration: %s", err) 327 } 328 } 329 330 var ip string 331 if forwardedFor := c.Request().Header.Get(echo.HeaderXForwardedFor); forwardedFor != "" { 332 ip = strings.TrimSpace(strings.SplitN(forwardedFor, ",", 2)[0]) 333 } 334 if ip == "" { 335 ip = strings.Split(c.Request().RemoteAddr, ":")[0] 336 } 337 inst.Logger().WithNamespace("loginaudit"). 338 Infof("New bitwarden client from %s at %s", ip, time.Now()) 339 340 // Send the response 341 out := AccessTokenReponse{ 342 ClientID: client.ClientID, 343 RegToken: client.RegistrationToken, 344 Type: "Bearer", 345 ExpiresIn: int(consts.AccessTokenValidityDuration.Seconds()), 346 Access: access, 347 Refresh: refresh, 348 Key: key, 349 Kdf: setting.PassphraseKdf, 350 Iterations: setting.PassphraseKdfIterations, 351 } 352 if setting.PrivateKey != "" { 353 out.PrivateKey = setting.PrivateKey 354 } 355 return c.JSON(http.StatusOK, out) 356 } 357 358 // checkTwoFactor returns true if the request has a valid 2FA code. 359 func checkTwoFactor(c echo.Context, inst *instance.Instance) bool { 360 cache := config.GetConfig().CacheStorage 361 key := "bw-2fa:" + inst.Domain 362 363 if passcode := c.FormValue("twoFactorToken"); passcode != "" { 364 if token, ok := cache.Get(key); ok { 365 if inst.ValidateTwoFactorPasscode(token, passcode) { 366 return true 367 } else { 368 _ = c.JSON(http.StatusBadRequest, echo.Map{ 369 "error": "invalid_grant", 370 "error_description": "invalid_username_or_password", 371 "ErrorModel": map[string]string{ 372 "Message": "Two-step token is invalid. Try again.", 373 "Object": "error", 374 }, 375 }) 376 return false 377 } 378 } 379 } 380 381 // Allow the settings webapp get a bitwarden token without the 2FA. It's OK 382 // from a security point of view as we still have 2 factors: the password 383 // and a valid session cookie. 384 if _, ok := middlewares.GetSession(c); ok { 385 return true 386 } 387 388 email, err := inst.SettingsEMail() 389 if err != nil { 390 _ = c.JSON(http.StatusInternalServerError, echo.Map{ 391 "error": err.Error(), 392 }) 393 return false 394 } 395 var obscured string 396 if parts := strings.SplitN(email, "@", 2); len(parts) == 2 { 397 s := strings.Map(func(_ rune) rune { return '*' }, parts[0]) 398 obscured = s + "@" + parts[1] 399 } 400 401 token, err := lifecycle.SendTwoFactorPasscode(inst) 402 if err != nil { 403 _ = c.JSON(http.StatusInternalServerError, echo.Map{ 404 "error": err.Error(), 405 }) 406 return false 407 } 408 cache.Set(key, token, 5*time.Minute) 409 410 _ = c.JSON(http.StatusBadRequest, echo.Map{ 411 "error": "invalid_grant", 412 "error_description": "Two factor required.", 413 // 1 means email 414 // https://github.com/bitwarden/jslib/blob/master/common/src/enums/twoFactorProviderType.ts 415 "TwoFactorProviders": []int{1}, 416 "TwoFactorProviders2": map[string]map[string]string{ 417 "1": {"Email": obscured}, 418 }, 419 }) 420 return false 421 } 422 423 func refreshToken(c echo.Context) error { 424 inst := middlewares.GetInstance(c) 425 refresh := c.FormValue("refresh_token") 426 427 // Check the refresh token 428 claims, ok := oauth.ValidTokenWithSStamp(inst, consts.RefreshTokenAudience, refresh) 429 if !ok { 430 return c.JSON(http.StatusBadRequest, echo.Map{ 431 "error": "invalid refresh token", 432 }) 433 } 434 435 // Find the OAuth client 436 client, err := oauth.FindClient(inst, claims.Subject) 437 if err != nil { 438 if couchErr, isCouchErr := couchdb.IsCouchError(err); isCouchErr && couchErr.StatusCode >= 500 { 439 return err 440 } 441 return c.JSON(http.StatusBadRequest, echo.Map{ 442 "error": "the client must be registered", 443 }) 444 } 445 if !bitwarden.IsBitwardenClient(client, claims.Scope) { 446 return c.JSON(http.StatusBadRequest, echo.Map{ 447 "error": "invalid refresh token", 448 }) 449 } 450 451 // Create the credentials 452 access, err := bitwarden.CreateAccessJWT(inst, client) 453 if err != nil { 454 return c.JSON(http.StatusInternalServerError, echo.Map{ 455 "error": "Can't generate access token", 456 }) 457 } 458 setting, err := settings.Get(inst) 459 if err != nil { 460 return err 461 } 462 key := setting.Key 463 464 // Send the response 465 out := AccessTokenReponse{ 466 Type: "Bearer", 467 ExpiresIn: int(consts.AccessTokenValidityDuration.Seconds()), 468 Access: access, 469 Refresh: refresh, 470 Key: key, 471 Kdf: setting.PassphraseKdf, 472 Iterations: setting.PassphraseKdfIterations, 473 } 474 if setting.PrivateKey != "" { 475 out.PrivateKey = setting.PrivateKey 476 } 477 return c.JSON(http.StatusOK, out) 478 } 479 480 // GetCozy returns the information about the cozy organization, including the 481 // organization key. 482 func GetCozy(c echo.Context) error { 483 inst := middlewares.GetInstance(c) 484 if err := middlewares.AllowWholeType(c, permission.GET, consts.BitwardenOrganizations); err != nil { 485 return c.JSON(http.StatusUnauthorized, echo.Map{ 486 "error": "invalid token", 487 }) 488 } 489 490 setting, err := settings.Get(inst) 491 if err != nil { 492 return c.JSON(http.StatusInternalServerError, echo.Map{ 493 "error": err.Error(), 494 }) 495 } 496 orgKey, err := setting.OrganizationKey() 497 if err != nil { 498 return c.JSON(http.StatusInternalServerError, echo.Map{ 499 "error": err.Error(), 500 }) 501 } 502 503 res := map[string]interface{}{ 504 "organizationId": setting.OrganizationID, 505 "collectionId": setting.CollectionID, 506 "organizationKey": orgKey, 507 } 508 return c.JSON(http.StatusOK, res) 509 } 510 511 // Routes sets the routing for the Bitwarden-like API 512 func Routes(router *echo.Group) { 513 identity := router.Group("/identity") 514 identity.POST("/connect/token", GetToken) 515 identity.POST("/accounts/prelogin", Prelogin) 516 517 api := router.Group("/api") 518 api.GET("/sync", Sync) 519 520 accounts := api.Group("/accounts") 521 accounts.POST("/prelogin", Prelogin) 522 accounts.POST("/password-hint", SendHint) 523 accounts.GET("/profile", GetProfile) 524 accounts.POST("/profile", UpdateProfile) 525 accounts.PUT("/profile", UpdateProfile) 526 accounts.POST("/keys", SetKeyPair) 527 accounts.POST("/security-stamp", ChangeSecurityStamp) 528 accounts.GET("/revision-date", GetRevisionDate) 529 530 settings := api.Group("/settings") 531 settings.GET("/domains", GetDomains) 532 settings.PUT("/domains", UpdateDomains) 533 settings.POST("/domains", UpdateDomains) 534 535 ciphers := api.Group("/ciphers") 536 ciphers.GET("", ListCiphers) 537 ciphers.POST("", CreateCipher) 538 ciphers.POST("/create", CreateSharedCipher) 539 ciphers.GET("/:id", GetCipher) 540 ciphers.GET("/:id/details", GetCipher) 541 ciphers.POST("/:id", UpdateCipher) 542 ciphers.PUT("/:id", UpdateCipher) 543 ciphers.POST("/import", ImportCiphers) 544 545 ciphers.DELETE("/:id", DeleteCipher) 546 ciphers.POST("/:id/delete", DeleteCipher) 547 ciphers.PUT("/:id/delete", SoftDeleteCipher) 548 ciphers.PUT("/:id/restore", RestoreCipher) 549 ciphers.DELETE("", BulkDeleteCiphers) 550 ciphers.POST("/delete", BulkDeleteCiphers) 551 ciphers.PUT("/delete", BulkSoftDeleteCiphers) 552 ciphers.PUT("/restore", BulkRestoreCiphers) 553 554 ciphers.POST("/:id/share", ShareCipher) 555 ciphers.PUT("/:id/share", ShareCipher) 556 557 folders := api.Group("/folders") 558 folders.GET("", ListFolders) 559 folders.POST("", CreateFolder) 560 folders.GET("/:id", GetFolder) 561 folders.POST("/:id", RenameFolder) 562 folders.PUT("/:id", RenameFolder) 563 folders.DELETE("/:id", DeleteFolder) 564 folders.POST("/:id/delete", DeleteFolder) 565 566 orgs := api.Group("/organizations") 567 orgs.POST("", CreateOrganization) 568 orgs.GET("/:id", GetOrganization) 569 orgs.GET("/:id/collections", GetCollections) 570 orgs.DELETE("/:id", DeleteOrganization) 571 orgs.GET("/:id/users", ListOrganizationUser) 572 orgs.POST("/:id/users/:user-id/confirm", ConfirmUser) 573 574 router.GET("/organizations/cozy", GetCozy) 575 router.DELETE("/contacts/:id", RefuseContact) 576 577 api.GET("/users/:id/public-key", GetPublicKey) 578 579 hub := router.Group("/notifications/hub") 580 hub.GET("", WebsocketHub) 581 hub.POST("/negotiate", NegotiateHub) 582 583 icons := router.Group("/icons") 584 cacheControl := middlewares.CacheControl(middlewares.CacheOptions{ 585 MaxAge: 24 * time.Hour, 586 }) 587 icons.GET("/:domain/icon.png", GetIcon, cacheControl) 588 }