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, &params); 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, &params); 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, &params); 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  }