github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/instance/lifecycle/passphrase.go (about) 1 package lifecycle 2 3 import ( 4 "crypto/subtle" 5 "encoding/hex" 6 "errors" 7 "net/url" 8 "time" 9 10 "github.com/cozy/cozy-stack/model/bitwarden/settings" 11 "github.com/cozy/cozy-stack/model/instance" 12 csettings "github.com/cozy/cozy-stack/model/settings" 13 "github.com/cozy/cozy-stack/pkg/config/config" 14 "github.com/cozy/cozy-stack/pkg/crypto" 15 "github.com/cozy/cozy-stack/pkg/emailer" 16 "github.com/gofrs/uuid/v5" 17 ) 18 19 // ErrHintSameAsPassword is used when trying to set an hint that is the same as 20 // the password, which would defeat security (e.g. the hint is not encrypted in 21 // CouchDB). 22 var ErrHintSameAsPassword = errors.New("The hint cannot be the same as the password") 23 24 // PassParameters are the parameters for setting a new passphrase 25 type PassParameters struct { 26 Pass []byte // Pass is the password hashed on client side, but not yet on server. 27 Iterations int // Iterations is the number of iterations applied by PBKDF2 on client side. 28 Key string // Key is the encryption key (encrypted, and in CipherString format). 29 PublicKey string // PublicKey is part of the key pair for bitwarden (encoded in base64). 30 PrivateKey string // PrivateKey is the other part (encrypted, in CipherString format). 31 Hint string // Hint is the hint for the user to find again their password 32 } 33 34 func registerPassphrase(inst *instance.Instance, tok []byte, params PassParameters) error { 35 if len(params.Pass) == 0 { 36 return instance.ErrMissingPassphrase 37 } 38 if len(inst.RegisterToken) == 0 { 39 return instance.ErrMissingToken 40 } 41 if subtle.ConstantTimeCompare(inst.RegisterToken, tok) != 1 { 42 return instance.ErrInvalidToken 43 } 44 settings, err := settings.Get(inst) 45 if err != nil { 46 return nil 47 } 48 if params.Iterations == 0 || params.Key == "" { 49 if err := setDefaultParameters(inst, ¶ms); err != nil { 50 return err 51 } 52 } 53 hash, err := crypto.GenerateFromPassphrase(params.Pass) 54 if err != nil { 55 return err 56 } 57 inst.RegisterToken = nil 58 setPassphraseKdfAndSecret(inst, settings, hash, params) 59 return settings.Save(inst) 60 } 61 62 // RegisterPassphrase replace the instance registerToken by a passphrase 63 func RegisterPassphrase(inst *instance.Instance, tok []byte, params PassParameters) error { 64 if err := registerPassphrase(inst, tok, params); err != nil { 65 return err 66 } 67 return update(inst) 68 } 69 70 // SendHint sends by mail the hint for the passphrase. 71 func SendHint(inst *instance.Instance) error { 72 if inst.RegisterToken != nil { 73 inst.Logger().Info("Send hint ignored: not registered") 74 return nil 75 } 76 publicName, err := csettings.PublicName(inst) 77 if err != nil { 78 return err 79 } 80 setting, err := settings.Get(inst) 81 if err != nil { 82 return err 83 } 84 return emailer.SendEmail(inst, &emailer.TransactionalEmailCmd{ 85 TemplateName: "passphrase_hint", 86 TemplateValues: map[string]interface{}{ 87 "BaseURL": inst.PageURL("/", nil), 88 "Hint": setting.PassphraseHint, 89 "PublicName": publicName, 90 "CozyPass": inst.HasForcedOIDC(), 91 }, 92 }) 93 } 94 95 // RequestPassphraseReset generates a new registration token for the user to 96 // renew its password. 97 func RequestPassphraseReset(inst *instance.Instance, from string) error { 98 // If a registration token is set, we do not generate another token than the 99 // registration one, and bail. 100 if inst.RegisterToken != nil { 101 inst.Logger().Info("Passphrase reset ignored: not registered") 102 return nil 103 } 104 // If a passphrase reset token is set and valid, we do not generate new one, 105 // and bail. 106 if inst.PassphraseResetToken != nil && inst.PassphraseResetTime != nil && 107 time.Now().UTC().Before(*inst.PassphraseResetTime) { 108 inst.Logger().Infof("Passphrase reset ignored: already sent at %s", 109 inst.PassphraseResetTime.String()) 110 return instance.ErrResetAlreadyRequested 111 } 112 resetTime := time.Now().UTC().Add(config.PasswordResetInterval()) 113 inst.PassphraseResetToken = crypto.GenerateRandomBytes(instance.PasswordResetTokenLen) 114 inst.PassphraseResetTime = &resetTime 115 if err := update(inst); err != nil { 116 return err 117 } 118 // Send a mail containing the reset url for the user to actually reset its 119 // passphrase. 120 resetURL := inst.PageURL("/auth/passphrase_renew", url.Values{ 121 "token": {hex.EncodeToString(inst.PassphraseResetToken)}, 122 "from": {from}, 123 }) 124 publicName, err := csettings.PublicName(inst) 125 if err != nil { 126 return err 127 } 128 return emailer.SendEmail(inst, &emailer.TransactionalEmailCmd{ 129 TemplateName: "passphrase_reset", 130 TemplateValues: map[string]interface{}{ 131 "BaseURL": inst.PageURL("/", nil), 132 "PassphraseResetLink": resetURL, 133 "PublicName": publicName, 134 "CozyPass": inst.HasForcedOIDC(), 135 }, 136 }) 137 } 138 139 // CheckPassphraseRenewToken checks whether the given token is good to use for 140 // resetting the passphrase. 141 func CheckPassphraseRenewToken(inst *instance.Instance, tok []byte) error { 142 if inst.PassphraseResetToken == nil { 143 return instance.ErrMissingToken 144 } 145 if inst.PassphraseResetTime != nil && !time.Now().UTC().Before(*inst.PassphraseResetTime) { 146 return instance.ErrMissingToken 147 } 148 if subtle.ConstantTimeCompare(inst.PassphraseResetToken, tok) != 1 { 149 return instance.ErrInvalidToken 150 } 151 return nil 152 } 153 154 // PassphraseRenew changes the passphrase to the specified one if the given 155 // token matches the `PassphraseResetToken` field. 156 func PassphraseRenew(inst *instance.Instance, tok []byte, params PassParameters) error { 157 err := CheckPassphraseRenewToken(inst, tok) 158 if err != nil { 159 return err 160 } 161 settings, err := settings.Get(inst) 162 if err != nil { 163 return nil 164 } 165 if params.Iterations == 0 || params.Key == "" { 166 if err := setDefaultParameters(inst, ¶ms); err != nil { 167 return err 168 } 169 } 170 hash, err := crypto.GenerateFromPassphrase(params.Pass) 171 if err != nil { 172 return err 173 } 174 inst.PassphraseResetToken = nil 175 inst.PassphraseResetTime = nil 176 settings.PassphraseHint = params.Hint 177 setPassphraseKdfAndSecret(inst, settings, hash, params) 178 if err := settings.Save(inst); err != nil { 179 return err 180 } 181 return update(inst) 182 } 183 184 // UpdatePassphrase replace the passphrase 185 func UpdatePassphrase( 186 inst *instance.Instance, 187 current []byte, 188 twoFactorPasscode string, 189 twoFactorToken []byte, 190 params PassParameters, 191 ) error { 192 if len(params.Pass) == 0 { 193 return instance.ErrMissingPassphrase 194 } 195 // With two factor authentication, we do not check the validity of the 196 // current passphrase, but the validity of the pair passcode/token which has 197 // been exchanged against the current passphrase. 198 if inst.HasAuthMode(instance.TwoFactorMail) { 199 if !inst.ValidateTwoFactorPasscode(twoFactorToken, twoFactorPasscode) { 200 return instance.ErrInvalidTwoFactor 201 } 202 } else { 203 // the needUpdate flag is not checked against since the passphrase will be 204 // regenerated with updated parameters just after, if the passphrase match. 205 _, err := crypto.CompareHashAndPassphrase(inst.PassphraseHash, current) 206 if err != nil { 207 return instance.ErrInvalidPassphrase 208 } 209 } 210 hash, err := crypto.GenerateFromPassphrase(params.Pass) 211 if err != nil { 212 return err 213 } 214 settings, err := settings.Get(inst) 215 if err != nil { 216 return nil 217 } 218 setPassphraseKdfAndSecret(inst, settings, hash, params) 219 if err := settings.Save(inst); err != nil { 220 return err 221 } 222 return update(inst) 223 } 224 225 // ForceUpdatePassphrase replace the passphrase without checking the current one 226 func ForceUpdatePassphrase(inst *instance.Instance, newPassword []byte, params PassParameters) error { 227 if len(newPassword) == 0 { 228 return instance.ErrMissingPassphrase 229 } 230 if params.Iterations == 0 { 231 if err := setDefaultParameters(inst, ¶ms); err != nil { 232 return err 233 } 234 } 235 settings, err := settings.Get(inst) 236 if err != nil { 237 return nil 238 } 239 hash, err := crypto.GenerateFromPassphrase(params.Pass) 240 if err != nil { 241 return err 242 } 243 setPassphraseKdfAndSecret(inst, settings, hash, params) 244 if err := settings.Save(inst); err != nil { 245 return err 246 } 247 return update(inst) 248 } 249 250 func setDefaultParameters(inst *instance.Instance, params *PassParameters) error { 251 pass, masterKey, iterations := emulateClientSideHashing(inst, params.Pass) 252 params.Pass, params.Iterations = pass, iterations 253 if params.Key == "" { 254 key, encKey, err := CreatePassphraseKey(masterKey) 255 if err != nil { 256 return err 257 } 258 params.Key = key 259 if params.PublicKey == "" && params.PrivateKey == "" { 260 pubKey, privKey, err := CreateKeyPair(encKey) 261 if err != nil { 262 return err 263 } 264 params.PublicKey = pubKey 265 params.PrivateKey = privKey 266 } 267 } 268 return nil 269 } 270 271 func emulateClientSideHashing(inst *instance.Instance, password []byte) ([]byte, []byte, int) { 272 kdfIterations := crypto.DefaultPBKDF2Iterations 273 salt := inst.PassphraseSalt() 274 hashed, masterKey := crypto.HashPassWithPBKDF2(password, salt, kdfIterations) 275 return hashed, masterKey, kdfIterations 276 } 277 278 func setPassphraseKdfAndSecret(inst *instance.Instance, settings *settings.Settings, hash []byte, params PassParameters) { 279 inst.PassphraseHash = hash 280 settings.SecurityStamp = NewSecurityStamp() 281 settings.PassphraseKdf = instance.PBKDF2_SHA256 282 settings.PassphraseKdfIterations = params.Iterations 283 inst.SessSecret = crypto.GenerateRandomBytes(instance.SessionSecretLen) 284 if params.Key != "" { 285 settings.Key = params.Key 286 } 287 if params.PublicKey != "" && params.PrivateKey != "" { 288 _ = settings.SetKeyPair(inst, params.PublicKey, params.PrivateKey) 289 } 290 inst.SetPasswordDefined(true) 291 } 292 293 // CreatePassphraseKey creates an encryption key for Bitwarden. It returns in 294 // the first position the key encrypted with the masterKey, and in clear in 295 // second position. 296 // See https://github.com/jcs/rubywarden/blob/master/API.md 297 func CreatePassphraseKey(masterKey []byte) (string, []byte, error) { 298 pt := crypto.GenerateRandomBytes(64) 299 iv := crypto.GenerateRandomBytes(16) 300 encrypted, err := crypto.EncryptWithAES256CBC(masterKey, pt, iv) 301 if err != nil { 302 return "", nil, err 303 } 304 return encrypted, pt, nil 305 } 306 307 // CreateKeyPair creates a key pair for sharing ciphers with a bitwarden 308 // organization. It returns in first position the public key, and in second 309 // position the private key. The public key is encoded in base64. The private 310 // key is encrypted, and in in the cipherString format. 311 func CreateKeyPair(symKey []byte) (string, string, error) { 312 iv := crypto.GenerateRandomBytes(16) 313 pubKey, privKey, err := crypto.GenerateRSAKeyPair() 314 if err != nil { 315 return "", "", err 316 } 317 encrypted, err := crypto.EncryptWithAES256HMAC(symKey[:32], symKey[32:], privKey, iv) 318 if err != nil { 319 return "", "", err 320 } 321 return pubKey, encrypted, nil 322 } 323 324 // CheckHint returns true if the hint is valid, ie it is not 325 // the same as the password. 326 func CheckHint(inst *instance.Instance, setting *settings.Settings, hint string) error { 327 salt := inst.PassphraseSalt() 328 iterations := setting.PassphraseKdfIterations 329 hashed, _ := crypto.HashPassWithPBKDF2([]byte(hint), salt, iterations) 330 if err := instance.CheckPassphrase(inst, hashed); err == nil { 331 return ErrHintSameAsPassword 332 } 333 return nil 334 } 335 336 // NewSecurityStamp returns a new UUID that can be used as a security stamp. 337 func NewSecurityStamp() string { 338 return uuid.Must(uuid.NewV7()).String() 339 }