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 }