github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libpages/config/passwords.go (about)

     1  // Copyright 2018 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package config
     6  
     7  import (
     8  	"context"
     9  	"crypto/rand"
    10  	"crypto/sha256"
    11  	"encoding/hex"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"strings"
    16  	"sync"
    17  
    18  	"golang.org/x/crypto/bcrypt"
    19  	"golang.org/x/time/rate"
    20  )
    21  
    22  type passwordType int
    23  
    24  const (
    25  	_ passwordType = iota
    26  	passwordTypeBcrypt
    27  	passwordTypeSHA256
    28  )
    29  
    30  const passwordHashDivider = ":"
    31  const sha256PasswordHashPrefix = "sha256"
    32  const saltSize = 12
    33  
    34  var sha256PasswordHashLength = len(sha256PasswordHashPrefix) +
    35  	len(passwordHashDivider) +
    36  	hex.EncodedLen(saltSize) +
    37  	len(passwordHashDivider) +
    38  	hex.EncodedLen(sha256.Size)
    39  var sha256PasswordHashSaltIndex = len(sha256PasswordHashPrefix) +
    40  	len(passwordHashDivider)
    41  var sha256PasswordHashSaltEnd = sha256PasswordHashSaltIndex +
    42  	hex.EncodedLen(saltSize)
    43  var sha256PasswordHashSHA256Index = sha256PasswordHashSaltEnd +
    44  	len(passwordHashDivider)
    45  
    46  // InvalidPasswordHash is the error that happens when there's an invalid
    47  // password hash in the config.
    48  type InvalidPasswordHash struct{}
    49  
    50  // Error implements the error interface.
    51  func (InvalidPasswordHash) Error() string {
    52  	return "invalid passwordhash"
    53  }
    54  
    55  type password interface {
    56  	check(ctx context.Context,
    57  		limiter *rate.Limiter, cleartext string) (bool, error)
    58  	passwordType() passwordType
    59  }
    60  
    61  type sha256Password struct {
    62  	hash [sha256.Size]byte
    63  	salt [saltSize]byte
    64  }
    65  
    66  var _ password = (*sha256Password)(nil)
    67  
    68  func (p *sha256Password) check(_ context.Context, _ *rate.Limiter,
    69  	cleartext string) (match bool, err error) {
    70  	sum := sha256.New()
    71  	if _, err = sum.Write(p.salt[:]); err != nil {
    72  		return false, fmt.Errorf("calculating sha256 error: %v", err)
    73  	}
    74  	if _, err = io.WriteString(sum, cleartext); err != nil {
    75  		return false, fmt.Errorf("calculating sha256 error: %v", err)
    76  	}
    77  	return p.hash == sha256.Sum256(append(p.salt[:], cleartext...)), nil
    78  }
    79  
    80  func (p *sha256Password) passwordType() passwordType {
    81  	return passwordTypeSHA256
    82  }
    83  
    84  type bcryptCachingPassword struct {
    85  	bcryptHash []byte
    86  
    87  	lock      sync.RWMutex
    88  	cleartext string
    89  }
    90  
    91  var _ password = (*bcryptCachingPassword)(nil)
    92  
    93  func (p *bcryptCachingPassword) getCachedCleartext() string {
    94  	p.lock.RLock()
    95  	defer p.lock.RUnlock()
    96  	return p.cleartext
    97  }
    98  
    99  func (p *bcryptCachingPassword) setCachedCleartext(cleartext string) {
   100  	p.lock.Lock()
   101  	defer p.lock.Unlock()
   102  	p.cleartext = cleartext
   103  }
   104  
   105  func (p *bcryptCachingPassword) passwordType() passwordType {
   106  	return passwordTypeBcrypt
   107  }
   108  
   109  func (p *bcryptCachingPassword) check(ctx context.Context,
   110  	limiter *rate.Limiter, cleartext string) (bool, error) {
   111  	cachedCleartext := p.getCachedCleartext()
   112  	if len(cachedCleartext) > 0 {
   113  		return cachedCleartext == cleartext, nil
   114  	}
   115  
   116  	if err := limiter.Wait(ctx); err != nil {
   117  		return false, err
   118  	}
   119  
   120  	match := bcrypt.CompareHashAndPassword(
   121  		p.bcryptHash, []byte(cleartext)) == nil
   122  	if match {
   123  		p.setCachedCleartext(cleartext)
   124  	}
   125  
   126  	return match, nil
   127  }
   128  
   129  // GenerateSHA256PasswordHash generates a SHA256 based password hash.
   130  func GenerateSHA256PasswordHash(cleartext string) (string, error) {
   131  	salt := make([]byte, saltSize)
   132  	n, err := rand.Read(salt)
   133  	if err != nil || n != saltSize {
   134  		return "", errors.New("reading random bytes error")
   135  	}
   136  
   137  	hash := sha256.Sum256(append(salt, cleartext...))
   138  	return sha256PasswordHashPrefix + passwordHashDivider +
   139  		hex.EncodeToString(salt) + passwordHashDivider +
   140  		hex.EncodeToString(hash[:]), nil
   141  }
   142  
   143  // newPassword takes a password hash (usually from a .kbp_config file) and
   144  // makes a password object out of it. Accepted password hashes are:
   145  //  1. bcrypt hashes. For example:
   146  //     $2a$04$DXabUWtVUX/nOEQ2R8aBT.wRUZxllKA2Lbm6Z3cGhkRLwMb6u8Esq
   147  //  2. sha256 hashes in format of sha256:<hex of salt>:<hex of sha256sum>.
   148  //     For example:
   149  //     sha256:249704a205894bb003b9f82a:6f2e235f076f1c7e1cfedec477091343dd4b1a678b11554321ee1a493925695c
   150  func newPassword(passwordHashFromConfig string) (password, error) {
   151  	if strings.HasPrefix(passwordHashFromConfig, sha256PasswordHashPrefix) {
   152  		if len(passwordHashFromConfig) != sha256PasswordHashLength {
   153  			return nil, InvalidPasswordHash{}
   154  		}
   155  		salt, err := hex.DecodeString(
   156  			passwordHashFromConfig[sha256PasswordHashSaltIndex:sha256PasswordHashSaltEnd])
   157  		if err != nil {
   158  			return nil, InvalidPasswordHash{}
   159  		}
   160  		hash, err := hex.DecodeString(
   161  			passwordHashFromConfig[sha256PasswordHashSHA256Index:])
   162  		if err != nil {
   163  			return nil, InvalidPasswordHash{}
   164  		}
   165  		p := &sha256Password{}
   166  		copy(p.hash[:], hash)
   167  		copy(p.salt[:], salt)
   168  		return p, nil
   169  	}
   170  	if _, err := bcrypt.Cost([]byte(passwordHashFromConfig)); err == nil {
   171  		return &bcryptCachingPassword{
   172  			bcryptHash: []byte(passwordHashFromConfig),
   173  		}, nil
   174  	}
   175  	return nil, InvalidPasswordHash{}
   176  }