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, ¶ms, 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 }