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

     1  // Package settings regroups some API methods to facilitate the usage of the
     2  // io.cozy settings documents.
     3  package settings
     4  
     5  import (
     6  	"crypto/subtle"
     7  	"encoding/hex"
     8  	"errors"
     9  	"fmt"
    10  	"net/http"
    11  	"strings"
    12  
    13  	"github.com/cozy/cozy-stack/model/bitwarden/settings"
    14  	"github.com/cozy/cozy-stack/model/instance"
    15  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    16  	"github.com/cozy/cozy-stack/model/oauth"
    17  	"github.com/cozy/cozy-stack/model/permission"
    18  	"github.com/cozy/cozy-stack/model/session"
    19  	"github.com/cozy/cozy-stack/model/sharing"
    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/pkg/crypto"
    24  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    25  	"github.com/cozy/cozy-stack/web/auth"
    26  	"github.com/cozy/cozy-stack/web/middlewares"
    27  	"github.com/labstack/echo/v4"
    28  )
    29  
    30  type apiPassphraseParameters struct {
    31  	Salt       string `json:"salt"`
    32  	Kdf        int    `json:"kdf"`
    33  	Iterations int    `json:"iterations"`
    34  }
    35  
    36  func (p *apiPassphraseParameters) ID() string                             { return consts.PassphraseParametersID }
    37  func (p *apiPassphraseParameters) Rev() string                            { return "" }
    38  func (p *apiPassphraseParameters) DocType() string                        { return consts.Settings }
    39  func (p *apiPassphraseParameters) Clone() couchdb.Doc                     { return p }
    40  func (p *apiPassphraseParameters) SetID(_ string)                         {}
    41  func (p *apiPassphraseParameters) SetRev(_ string)                        {}
    42  func (p *apiPassphraseParameters) Relationships() jsonapi.RelationshipMap { return nil }
    43  func (p *apiPassphraseParameters) Included() []jsonapi.Object             { return nil }
    44  func (p *apiPassphraseParameters) Links() *jsonapi.LinksList {
    45  	return &jsonapi.LinksList{Self: "/settings/passphrase"}
    46  }
    47  
    48  func (h *HTTPHandler) getPassphraseParameters(c echo.Context) error {
    49  	if err := middlewares.AllowWholeType(c, permission.GET, consts.Settings); err != nil {
    50  		return err
    51  	}
    52  	inst := middlewares.GetInstance(c)
    53  	settings, err := settings.Get(inst)
    54  	if err != nil {
    55  		return err
    56  	}
    57  	params := apiPassphraseParameters{
    58  		Salt:       string(inst.PassphraseSalt()),
    59  		Kdf:        settings.PassphraseKdf,
    60  		Iterations: settings.PassphraseKdfIterations,
    61  	}
    62  	return jsonapi.Data(c, http.StatusOK, &params, nil)
    63  }
    64  
    65  type passphraseRegistrationParameters struct {
    66  	Redirection string `json:"redirection" form:"redirection"`
    67  	Register    string `json:"register_token" form:"register_token"`
    68  	Passphrase  string `json:"passphrase" form:"passphrase"`
    69  	Hint        string `json:"hint" form:"hint"`
    70  	Key         string `json:"key" form:"key"`
    71  	PublicKey   string `json:"public_key" form:"public_key"`
    72  	PrivateKey  string `json:"private_key" form:"private_key"`
    73  	Iterations  int    `json:"iterations" form:"iterations"`
    74  
    75  	// For flagship app
    76  	ClientID     string `json:"client_id"`
    77  	ClientSecret string `json:"client_secret"`
    78  }
    79  
    80  func (h *HTTPHandler) registerPassphrase(c echo.Context) error {
    81  	inst := middlewares.GetInstance(c)
    82  
    83  	accept := c.Request().Header.Get(echo.HeaderAccept)
    84  	acceptHTML := strings.Contains(accept, echo.MIMETextHTML)
    85  
    86  	var args passphraseRegistrationParameters
    87  	if err := c.Bind(&args); err != nil {
    88  		return err
    89  	}
    90  
    91  	registerToken, err := hex.DecodeString(args.Register)
    92  	if err != nil {
    93  		return jsonapi.Errorf(http.StatusBadRequest, "%s", err)
    94  	}
    95  
    96  	if args.Iterations < crypto.MinPBKDF2Iterations && args.Iterations != 0 {
    97  		err := errors.New("The KdfIterations number is too low")
    98  		return jsonapi.InvalidParameter("KdfIterations", err)
    99  	}
   100  	if args.Iterations > crypto.MaxPBKDF2Iterations {
   101  		err := errors.New("The KdfIterations number is too high")
   102  		return jsonapi.InvalidParameter("KdfIterations", err)
   103  	}
   104  
   105  	passphrase := []byte(args.Passphrase)
   106  	err = lifecycle.RegisterPassphrase(inst, registerToken, lifecycle.PassParameters{
   107  		Pass:       passphrase,
   108  		Iterations: args.Iterations,
   109  		Key:        args.Key,
   110  		PublicKey:  args.PublicKey,
   111  		PrivateKey: args.PrivateKey,
   112  	})
   113  	if err != nil {
   114  		return jsonapi.BadRequest(err)
   115  	}
   116  
   117  	if args.Hint != "" {
   118  		setting, err := settings.Get(inst)
   119  		if err != nil {
   120  			return err
   121  		}
   122  		setting.PassphraseHint = args.Hint
   123  		if err := setting.Save(inst); err != nil {
   124  			return err
   125  		}
   126  	}
   127  
   128  	sessionID, err := auth.SetCookieForNewSession(c, session.LongRun)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	if err := session.StoreNewLoginEntry(inst, sessionID, "", c.Request(), "registration", false); err != nil {
   133  		inst.Logger().Errorf("Could not store session history %q: %s", sessionID, err)
   134  	}
   135  
   136  	return finishOnboarding(c, args.Redirection, acceptHTML)
   137  }
   138  
   139  func (h *HTTPHandler) registerPassphraseFlagship(c echo.Context) error {
   140  	inst := middlewares.GetInstance(c)
   141  
   142  	var args passphraseRegistrationParameters
   143  	if err := c.Bind(&args); err != nil {
   144  		return jsonapi.Errorf(http.StatusBadRequest, "%s", err)
   145  	}
   146  
   147  	registerToken, err := hex.DecodeString(args.Register)
   148  	if err != nil {
   149  		return jsonapi.Errorf(http.StatusBadRequest, "%s", err)
   150  	}
   151  
   152  	if args.Iterations < crypto.MinPBKDF2Iterations {
   153  		err := errors.New("The KdfIterations number is too low")
   154  		return jsonapi.InvalidParameter("KdfIterations", err)
   155  	}
   156  	if args.Iterations > crypto.MaxPBKDF2Iterations {
   157  		err := errors.New("The KdfIterations number is too high")
   158  		return jsonapi.InvalidParameter("KdfIterations", err)
   159  	}
   160  
   161  	client, err := oauth.FindClient(inst, args.ClientID)
   162  	if err != nil {
   163  		if couchErr, isCouchErr := couchdb.IsCouchError(err); isCouchErr && couchErr.StatusCode >= 500 {
   164  			return err
   165  		}
   166  		return c.JSON(http.StatusBadRequest, echo.Map{
   167  			"error": "the client must be registered",
   168  		})
   169  	}
   170  	if subtle.ConstantTimeCompare([]byte(args.ClientSecret), []byte(client.ClientSecret)) == 0 {
   171  		return c.JSON(http.StatusBadRequest, echo.Map{
   172  			"error": "invalid client_secret",
   173  		})
   174  	}
   175  
   176  	passphrase := []byte(args.Passphrase)
   177  	inst.OnboardingFinished = true
   178  	err = lifecycle.RegisterPassphrase(inst, registerToken, lifecycle.PassParameters{
   179  		Pass:       passphrase,
   180  		Iterations: args.Iterations,
   181  		Key:        args.Key,
   182  		PublicKey:  args.PublicKey,
   183  		PrivateKey: args.PrivateKey,
   184  	})
   185  	if err != nil {
   186  		return jsonapi.BadRequest(err)
   187  	}
   188  
   189  	if args.Hint != "" {
   190  		setting, err := settings.Get(inst)
   191  		if err != nil {
   192  			return err
   193  		}
   194  		setting.PassphraseHint = args.Hint
   195  		if err := setting.Save(inst); err != nil {
   196  			return err
   197  		}
   198  	}
   199  
   200  	if !client.Flagship {
   201  		context := inst.ContextName
   202  		if context == "" {
   203  			context = config.DefaultInstanceContext
   204  		}
   205  		cfg := config.GetConfig().Flagship.Contexts[context]
   206  		skipCertification := false
   207  		if cfg, ok := cfg.(map[string]interface{}); ok {
   208  			skipCertification = cfg["skip_certification"] == true
   209  		}
   210  		if !skipCertification {
   211  			_ = client.SetCreatedAtOnboarding(inst)
   212  			return auth.ReturnSessionCode(c, http.StatusAccepted, inst)
   213  		}
   214  	}
   215  
   216  	out := auth.AccessTokenReponse{
   217  		Type:  "bearer",
   218  		Scope: "*",
   219  	}
   220  	out.Refresh, err = client.CreateJWT(inst, consts.RefreshTokenAudience, out.Scope)
   221  	if err != nil {
   222  		return c.JSON(http.StatusInternalServerError, echo.Map{
   223  			"error": "Can't generate refresh token",
   224  		})
   225  	}
   226  	out.Access, err = client.CreateJWT(inst, consts.AccessTokenAudience, out.Scope)
   227  	if err != nil {
   228  		return c.JSON(http.StatusInternalServerError, echo.Map{
   229  			"error": "Can't generate access token",
   230  		})
   231  	}
   232  	return c.JSON(http.StatusOK, out)
   233  }
   234  
   235  func (h *HTTPHandler) updatePassphrase(c echo.Context) error {
   236  	inst := middlewares.GetInstance(c)
   237  	currentSession, hasSession := middlewares.GetSession(c)
   238  
   239  	// Even if the current passphrase is needed for this request to work, we
   240  	// enforce a valid permission to avoid having an unauthorized enpoint that
   241  	// can be bruteforced.
   242  	if err := middlewares.AllowWholeType(c, permission.PUT, consts.Settings); err != nil {
   243  		return err
   244  	}
   245  
   246  	args := struct {
   247  		Current           string `json:"current_passphrase"`
   248  		Passphrase        string `json:"new_passphrase"`
   249  		Iterations        int    `json:"iterations"`
   250  		TwoFactorPasscode string `json:"two_factor_passcode"`
   251  		TwoFactorToken    []byte `json:"two_factor_token"`
   252  		Force             bool   `json:"force,omitempty"`
   253  		Key               string `json:"key"`
   254  		PublicKey         string `json:"publicKey"`
   255  		PrivateKey        string `json:"privateKey"`
   256  	}{}
   257  	err := c.Bind(&args)
   258  	if err != nil {
   259  		return jsonapi.BadRequest(err)
   260  	}
   261  	newPassphrase := []byte(args.Passphrase)
   262  	currentPassphrase := []byte(args.Current)
   263  
   264  	// If we want to force the update
   265  	if args.Force {
   266  		canForce := false
   267  
   268  		// CLI can force the passphrase
   269  		if _, ok := middlewares.GetCLIPermission(c); ok {
   270  			canForce = true
   271  		}
   272  
   273  		// On cozy with OIDC and empty vault, the password can be forced to
   274  		// allow the setup of Cozy Pass. Same for magic links. But only to
   275  		// set a password for the first time.
   276  		if inst.PasswordDefined == nil {
   277  			if inst.HasForcedOIDC() || inst.MagicLink {
   278  				bitwarden, err := settings.Get(inst)
   279  				if err == nil && !bitwarden.ExtensionInstalled {
   280  					canForce = true
   281  				}
   282  			}
   283  		} else if !*inst.PasswordDefined {
   284  			canForce = true
   285  		}
   286  
   287  		if !canForce {
   288  			err = fmt.Errorf("Bitwarden extension has already been installed on this Cozy, cannot force update the passphrase.")
   289  			return jsonapi.BadRequest(err)
   290  		}
   291  
   292  		params := lifecycle.PassParameters{
   293  			Pass:       []byte(args.Passphrase),
   294  			Iterations: args.Iterations,
   295  			PublicKey:  args.PublicKey,
   296  			PrivateKey: args.PrivateKey,
   297  			Key:        args.Key,
   298  		}
   299  		err = lifecycle.ForceUpdatePassphrase(inst, newPassphrase, params)
   300  		if err != nil {
   301  			return err
   302  		}
   303  		go func() {
   304  			_ = sharing.SendPublicKey(inst, params.PublicKey)
   305  		}()
   306  		if hasSession {
   307  			_, _ = auth.SetCookieForNewSession(c, currentSession.Duration())
   308  		}
   309  		return c.NoContent(http.StatusNoContent)
   310  	}
   311  
   312  	// Else, we keep going on the standard checks (2FA, current passphrase, ...)
   313  	if inst.HasAuthMode(instance.TwoFactorMail) && len(args.TwoFactorToken) == 0 {
   314  		if instance.CheckPassphrase(inst, currentPassphrase) == nil {
   315  			var twoFactorToken []byte
   316  			twoFactorToken, err = lifecycle.SendTwoFactorPasscode(inst)
   317  			if err != nil {
   318  				return err
   319  			}
   320  			return c.JSON(http.StatusOK, echo.Map{
   321  				"two_factor_token": twoFactorToken,
   322  			})
   323  		}
   324  		return instance.ErrInvalidPassphrase
   325  	}
   326  
   327  	if args.Iterations < crypto.MinPBKDF2Iterations && args.Iterations != 0 {
   328  		err := errors.New("The KdfIterations number is too low")
   329  		return jsonapi.InvalidParameter("KdfIterations", err)
   330  	}
   331  	if args.Iterations > crypto.MaxPBKDF2Iterations {
   332  		err := errors.New("The KdfIterations number is too high")
   333  		return jsonapi.InvalidParameter("KdfIterations", err)
   334  	}
   335  
   336  	err = lifecycle.UpdatePassphrase(inst, currentPassphrase,
   337  		args.TwoFactorPasscode, args.TwoFactorToken,
   338  		lifecycle.PassParameters{
   339  			Pass:       newPassphrase,
   340  			Iterations: args.Iterations,
   341  			Key:        args.Key,
   342  		})
   343  	if err != nil {
   344  		return jsonapi.BadRequest(err)
   345  	}
   346  
   347  	duration := session.LongRun
   348  	if hasSession {
   349  		duration = currentSession.Duration()
   350  	}
   351  	if _, err = auth.SetCookieForNewSession(c, duration); err != nil {
   352  		return err
   353  	}
   354  
   355  	return c.NoContent(http.StatusNoContent)
   356  }
   357  
   358  func (h *HTTPHandler) checkPassphrase(c echo.Context) error {
   359  	// Even if the current passphrase is needed for this request to work, we
   360  	// enforce a valid permission to avoid having an unauthorized enpoint that
   361  	// can be bruteforced.
   362  	if err := middlewares.AllowWholeType(c, permission.PUT, consts.Settings); err != nil {
   363  		return err
   364  	}
   365  
   366  	inst := middlewares.GetInstance(c)
   367  	args := struct {
   368  		Passphrase string `json:"passphrase"`
   369  	}{}
   370  	err := c.Bind(&args)
   371  	if err != nil {
   372  		return jsonapi.BadRequest(err)
   373  	}
   374  
   375  	if instance.CheckPassphrase(inst, []byte(args.Passphrase)) != nil {
   376  		return jsonapi.Forbidden(instance.ErrInvalidPassphrase)
   377  	}
   378  
   379  	return c.NoContent(http.StatusNoContent)
   380  }
   381  
   382  func (h *HTTPHandler) getHint(c echo.Context) error {
   383  	inst := middlewares.GetInstance(c)
   384  
   385  	if err := middlewares.AllowWholeType(c, permission.GET, consts.Settings); err != nil {
   386  		return err
   387  	}
   388  
   389  	setting, err := settings.Get(inst)
   390  	if err != nil {
   391  		return err
   392  	}
   393  
   394  	if setting.PassphraseHint == "" {
   395  		return jsonapi.NotFound(errors.New("No hint"))
   396  	}
   397  
   398  	return c.NoContent(http.StatusNoContent)
   399  }
   400  
   401  func (h *HTTPHandler) updateHint(c echo.Context) error {
   402  	inst := middlewares.GetInstance(c)
   403  
   404  	if err := middlewares.AllowWholeType(c, permission.PUT, consts.Settings); err != nil {
   405  		return err
   406  	}
   407  
   408  	args := struct {
   409  		Hint string `json:"hint"`
   410  	}{}
   411  	if err := c.Bind(&args); err != nil {
   412  		return jsonapi.BadRequest(err)
   413  	}
   414  
   415  	setting, err := settings.Get(inst)
   416  	if err != nil {
   417  		return err
   418  	}
   419  	if err := lifecycle.CheckHint(inst, setting, args.Hint); err != nil {
   420  		return jsonapi.InvalidParameter("hint", err)
   421  	}
   422  
   423  	setting.PassphraseHint = args.Hint
   424  	if err := setting.Save(inst); err != nil {
   425  		return err
   426  	}
   427  	return c.NoContent(http.StatusNoContent)
   428  }
   429  
   430  func (h *HTTPHandler) createVault(c echo.Context) error {
   431  	inst := middlewares.GetInstance(c)
   432  
   433  	if err := middlewares.AllowWholeType(c, permission.POST, consts.BitwardenCiphers); err != nil {
   434  		return err
   435  	}
   436  
   437  	setting, err := settings.Get(inst)
   438  	if err != nil {
   439  		return err
   440  	}
   441  
   442  	if !setting.ExtensionInstalled {
   443  		if err := settings.MigrateAccountsToCiphers(inst); err != nil {
   444  			return jsonapi.InternalServerError(err)
   445  		}
   446  	}
   447  	return c.NoContent(http.StatusNoContent)
   448  }