github.com/decred/politeia@v1.4.0/politeiawww/legacy/totp.go (about) 1 // Copyright (c) 2020 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package legacy 6 7 import ( 8 "fmt" 9 "time" 10 11 www "github.com/decred/politeia/politeiawww/api/www/v1" 12 "github.com/decred/politeia/politeiawww/legacy/user" 13 "github.com/pquerna/otp" 14 "github.com/pquerna/otp/totp" 15 ) 16 17 const ( 18 defaultPoliteiaIssuer = "politeia" 19 defaultCMSIssuer = "cms" 20 21 // Period (in seconds) of TOTP for testing used for generating codes during 22 // tests. A low period allows for codes to be generated and tested very 23 // quickly. 24 totpTestPeriod = 1 25 ) 26 27 var ( 28 validTOTPTypes = map[www.TOTPMethodT]bool{ 29 www.TOTPTypeBasic: true, 30 } 31 ) 32 33 func (p *Politeiawww) totpGenerateOpts(issuer, accountName string) totp.GenerateOpts { 34 if p.test { 35 // Set the period a totp code is valid for to 1 second when 36 // testing so the unit tests don't take forever. 37 return totp.GenerateOpts{ 38 Issuer: issuer, 39 AccountName: accountName, 40 Period: totpTestPeriod, 41 } 42 } 43 return totp.GenerateOpts{ 44 Issuer: issuer, 45 AccountName: accountName, 46 } 47 } 48 49 func (p *Politeiawww) totpGenerateCode(secret string, t time.Time) (string, error) { 50 if p.test { 51 // Set the period a totp code is valid for to 1 second when 52 // testing so the unit tests don't take forever. 53 return totp.GenerateCodeCustom(secret, t, totp.ValidateOpts{ 54 Period: totpTestPeriod, 55 Skew: 0, 56 Digits: 6, 57 Algorithm: otp.AlgorithmSHA1, 58 }) 59 } 60 return totp.GenerateCode(secret, t) 61 } 62 63 func (p *Politeiawww) totpValidate(code, secret string, t time.Time) (bool, error) { 64 if p.test { 65 // Set the period a totp code is valid for to 1 second when 66 // testing so the unit tests don't take forever. 67 return totp.ValidateCustom(code, secret, t, totp.ValidateOpts{ 68 Period: totpTestPeriod, 69 Skew: 0, 70 Digits: 6, 71 Algorithm: otp.AlgorithmSHA1, 72 }) 73 } 74 return totp.Validate(code, secret), nil 75 } 76 77 func (p *Politeiawww) totpCheck(code string, u *user.User) error { 78 // Return error to alert that a code is required. 79 if code == "" { 80 log.Debugf("login: totp code required %v", u.Email) 81 return www.UserError{ 82 ErrorCode: www.ErrorStatusRequiresTOTPCode, 83 } 84 } 85 86 // Get the generated totp code. The provided code must match this 87 // generated code. 88 requestTime := time.Now() 89 currentCode, err := p.totpGenerateCode(u.TOTPSecret, requestTime) 90 if err != nil { 91 return fmt.Errorf("totpGenerateCode: %v", err) 92 } 93 94 // Verify the user does not have too many failed attempts for this 95 // epoch. 96 if len(u.TOTPLastFailedCodeTime) >= totpFailedAttempts { 97 // The user has too many failed attempts. We must first verify 98 // that the failed attempts are from this epoch before this is 99 // considered to be an error. If the generated code from the 100 // failed timestamp matches the generated code from the current 101 // timestamp then we know the failures occurred during this epoch. 102 failedTS := u.TOTPLastFailedCodeTime[len(u.TOTPLastFailedCodeTime)-1] 103 oldCode, err := p.totpGenerateCode(u.TOTPSecret, time.Unix(failedTS, 0)) 104 if err != nil { 105 return fmt.Errorf("totpGenerateCode: %v", err) 106 } 107 if oldCode == currentCode { 108 // The failures occurred in the same epoch which means the user 109 // has exceeded their max allowed attempts. 110 return www.UserError{ 111 ErrorCode: www.ErrorStatusTOTPWaitForNewCode, 112 } 113 } 114 115 // Previous failures are not from this epoch. Clear them out. 116 u.TOTPLastFailedCodeTime = []int64{} 117 } 118 119 // Verify the provided code matches the generated code 120 var replyError error 121 if currentCode == code { 122 // The code matches. Clear out all previous failed attempts 123 // before returning. 124 u.TOTPLastFailedCodeTime = []int64{} 125 } else { 126 // The code doesn't match. Save the failure and return an error. 127 ts := requestTime.Unix() 128 u.TOTPLastFailedCodeTime = append(u.TOTPLastFailedCodeTime, ts) 129 replyError = www.UserError{ 130 ErrorCode: www.ErrorStatusTOTPFailedValidation, 131 } 132 } 133 134 // Update the user database 135 err = p.db.UserUpdate(*u) 136 if err != nil { 137 return fmt.Errorf("UserUpdate: %v", err) 138 } 139 140 return replyError 141 }