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  }