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  }