github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/instance/auth.go (about)

     1  package instance
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/base32"
     6  	"io"
     7  	"net/http"
     8  	"time"
     9  
    10  	"github.com/cozy/cozy-stack/pkg/crypto"
    11  	"github.com/mssola/user_agent"
    12  	"github.com/pquerna/otp"
    13  	"github.com/pquerna/otp/totp"
    14  	"golang.org/x/crypto/hkdf"
    15  )
    16  
    17  // This is the lengths of our tokens (in bytes).
    18  const (
    19  	RegisterTokenLen      = 16
    20  	PasswordResetTokenLen = 16
    21  	SessionCodeLen        = 32
    22  	EmailVerifiedCodeLen  = 32
    23  	SessionSecretLen      = 64
    24  	MagicLinkCodeLen      = 32
    25  	OauthSecretLen        = 128
    26  )
    27  
    28  var twoFactorTOTPOptions = totp.ValidateOpts{
    29  	Period:    60, // 60s
    30  	Skew:      14, // 60s +- 14*60s = [-13min; 15min]
    31  	Digits:    otp.DigitsSix,
    32  	Algorithm: otp.AlgorithmSHA256,
    33  }
    34  
    35  var totpMACConfig = crypto.MACConfig{
    36  	Name:   "totp",
    37  	MaxAge: 0,
    38  	MaxLen: 256,
    39  }
    40  
    41  var trustedDeviceMACConfig = crypto.MACConfig{
    42  	Name:   "trusted-device",
    43  	MaxAge: 0,
    44  	MaxLen: 256,
    45  }
    46  
    47  // AuthMode defines the authentication mode chosen for the connection to this
    48  // instance.
    49  type AuthMode int
    50  
    51  const (
    52  	// Basic authentication mode, only passphrase
    53  	Basic AuthMode = iota
    54  	// TwoFactorMail authentication mode, with passcode sent via email
    55  	TwoFactorMail
    56  )
    57  
    58  // AuthModeToString encode authentication mode in a string
    59  func AuthModeToString(authMode AuthMode) string {
    60  	switch authMode {
    61  	case TwoFactorMail:
    62  		return "two_factor_mail"
    63  	default:
    64  		return "basic"
    65  	}
    66  }
    67  
    68  // StringToAuthMode converts a string encoded authentication mode into a
    69  // AuthMode int.
    70  func StringToAuthMode(authMode string) (AuthMode, error) {
    71  	switch authMode {
    72  	case "two_factor_mail":
    73  		return TwoFactorMail, nil
    74  	case "basic":
    75  		return Basic, nil
    76  	default:
    77  		return 0, ErrUnknownAuthMode
    78  	}
    79  }
    80  
    81  // HasAuthMode returns whether or not the instance has the given authentication
    82  // mode activated.
    83  func (i *Instance) HasAuthMode(authMode AuthMode) bool {
    84  	return i.AuthMode == authMode
    85  }
    86  
    87  // GenerateTwoFactorSecrets generates a (token, passcode) pair that can be
    88  // used as a two factor authentication secret value. The token is used to allow
    89  // the two-factor form — meaning the user has correctly entered its passphrase
    90  // and successfully done the first part of the two factor authentication.
    91  //
    92  // The passcode should be send to the user by another mean (mail, SMS, ...)
    93  func (i *Instance) GenerateTwoFactorSecrets() (token []byte, passcode string, err error) {
    94  	// A salt is used when we generate a new 2FA secret to derive a new TOTP
    95  	// function from. This allow us to have TOTP derived from a new key each time
    96  	// we check the first step of the 2FA ("the passphrase step"). This salt is
    97  	// given to the user and signed in the "two-factor-token" MAC.
    98  	salt := crypto.GenerateRandomBytes(sha256.Size)
    99  	token, err = crypto.EncodeAuthMessage(totpMACConfig, i.SessionSecret(), salt, nil)
   100  	if err != nil {
   101  		return
   102  	}
   103  
   104  	h := hkdf.New(sha256.New, i.SessionSecret(), salt, nil)
   105  	key := make([]byte, 32)
   106  	_, err = io.ReadFull(h, key)
   107  	if err != nil {
   108  		return
   109  	}
   110  	passcode, err = totp.GenerateCodeCustom(base32.StdEncoding.EncodeToString(key),
   111  		time.Now().UTC(), twoFactorTOTPOptions)
   112  	return
   113  }
   114  
   115  // ValidateTwoFactorPasscode validates the given (token, passcode) pair for two
   116  // factor authentication.
   117  func (i *Instance) ValidateTwoFactorPasscode(token []byte, passcode string) bool {
   118  	salt, err := crypto.DecodeAuthMessage(totpMACConfig, i.SessionSecret(), token, nil)
   119  	if err != nil {
   120  		return false
   121  	}
   122  
   123  	h := hkdf.New(sha256.New, i.SessionSecret(), salt, nil)
   124  	key := make([]byte, 32)
   125  	_, err = io.ReadFull(h, key)
   126  	if err != nil {
   127  		return false
   128  	}
   129  	ok, err := totp.ValidateCustom(passcode, base32.StdEncoding.EncodeToString(key),
   130  		time.Now().UTC(), twoFactorTOTPOptions)
   131  	return ok && err == nil
   132  }
   133  
   134  // GenerateTwoFactorTrustedDeviceSecret generates a token that can be kept by the
   135  // user on-demand to avoid having two-factor authentication on a specific
   136  // machine.
   137  func (i *Instance) GenerateTwoFactorTrustedDeviceSecret(req *http.Request) ([]byte, error) {
   138  	ua := user_agent.New(req.UserAgent())
   139  	browser, _ := ua.Browser()
   140  	additionalData := []byte(i.Domain + ua.OS() + browser)
   141  	return crypto.EncodeAuthMessage(trustedDeviceMACConfig, i.SessionSecret(), nil, additionalData)
   142  }
   143  
   144  // ValidateTwoFactorTrustedDeviceSecret validates the given token used to check
   145  // if the computer is trusted to avoid two-factor authorization.
   146  func (i *Instance) ValidateTwoFactorTrustedDeviceSecret(req *http.Request, token []byte) bool {
   147  	ua := user_agent.New(req.UserAgent())
   148  	browser, _ := ua.Browser()
   149  	additionalData := []byte(i.Domain + ua.OS() + browser)
   150  	_, err := crypto.DecodeAuthMessage(trustedDeviceMACConfig, i.SessionSecret(), token, additionalData)
   151  	return err == nil
   152  }
   153  
   154  // GenerateMailConfirmationCode generates a code for validating the user's
   155  // email.
   156  func (i *Instance) GenerateMailConfirmationCode() (string, error) {
   157  	email, err := i.SettingsEMail()
   158  	if err != nil {
   159  		return "", err
   160  	}
   161  	h := hkdf.New(sha256.New, i.SessionSecret(), nil, []byte(email))
   162  	key := make([]byte, 32)
   163  	_, err = io.ReadFull(h, key)
   164  	if err != nil {
   165  		return "", err
   166  	}
   167  	return totp.GenerateCodeCustom(base32.StdEncoding.EncodeToString(key),
   168  		time.Now().UTC(), twoFactorTOTPOptions)
   169  }
   170  
   171  // ValidateMailConfirmationCode returns true if the given passcode is valid.
   172  func (i *Instance) ValidateMailConfirmationCode(passcode string) bool {
   173  	email, err := i.SettingsEMail()
   174  	if err != nil {
   175  		return false
   176  	}
   177  	h := hkdf.New(sha256.New, i.SessionSecret(), nil, []byte(email))
   178  	key := make([]byte, 32)
   179  	_, err = io.ReadFull(h, key)
   180  	if err != nil {
   181  		return false
   182  	}
   183  	ok, err := totp.ValidateCustom(passcode, base32.StdEncoding.EncodeToString(key),
   184  		time.Now().UTC(), twoFactorTOTPOptions)
   185  	if !ok || err != nil {
   186  		return false
   187  	}
   188  	return true
   189  }
   190  
   191  // CreateSessionCode returns a session_code that can be used to open a webview
   192  // inside the flagship app and create the session.
   193  func (i *Instance) CreateSessionCode() (string, error) {
   194  	code := crypto.GenerateRandomString(SessionCodeLen)
   195  	store := GetStore()
   196  	if err := store.SaveSessionCode(i, code); err != nil {
   197  		return "", err
   198  	}
   199  	return code, nil
   200  }
   201  
   202  // CheckAndClearSessionCode will return true if the session code is valid. The
   203  // session code can only be used once, so it will be cleared after calling this
   204  // function.
   205  func (i *Instance) CheckAndClearSessionCode(code string) bool {
   206  	return GetStore().CheckAndClearSessionCode(i, code)
   207  }
   208  
   209  // CreateEmailVerifiedCode returns an email_verified_code that can be used to
   210  // avoid the 2FA by email.
   211  func (i *Instance) CreateEmailVerifiedCode() (string, error) {
   212  	code := crypto.GenerateRandomString(EmailVerifiedCodeLen)
   213  	store := GetStore()
   214  	if err := store.SaveEmailVerfiedCode(i, code); err != nil {
   215  		return "", err
   216  	}
   217  	return code, nil
   218  }
   219  
   220  // CheckEmailVerifiedCode will return true if the email verified code is valid.
   221  func (i *Instance) CheckEmailVerifiedCode(code string) bool {
   222  	if code == "" {
   223  		return false
   224  	}
   225  	return GetStore().CheckEmailVerifiedCode(i, code)
   226  }