github.com/greenpau/go-authcrunch@v1.1.4/pkg/identity/password.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package identity
    16  
    17  import (
    18  	"strconv"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/greenpau/go-authcrunch/pkg/errors"
    23  	"golang.org/x/crypto/bcrypt"
    24  )
    25  
    26  // Password is a memorized secret, typically a string of characters,
    27  // used to confirm the identity of a user.
    28  type Password struct {
    29  	Purpose    string    `json:"purpose,omitempty" xml:"purpose,omitempty" yaml:"purpose,omitempty"`
    30  	Algorithm  string    `json:"algorithm,omitempty" xml:"algorithm,omitempty" yaml:"algorithm,omitempty"`
    31  	Hash       string    `json:"hash,omitempty" xml:"hash,omitempty" yaml:"hash,omitempty"`
    32  	Cost       int       `json:"cost,omitempty" xml:"cost,omitempty" yaml:"cost,omitempty"`
    33  	Expired    bool      `json:"expired,omitempty" xml:"expired,omitempty" yaml:"expired,omitempty"`
    34  	ExpiredAt  time.Time `json:"expired_at,omitempty" xml:"expired_at,omitempty" yaml:"expired_at,omitempty"`
    35  	CreatedAt  time.Time `json:"created_at,omitempty" xml:"created_at,omitempty" yaml:"created_at,omitempty"`
    36  	Disabled   bool      `json:"disabled,omitempty" xml:"disabled,omitempty" yaml:"disabled,omitempty"`
    37  	DisabledAt time.Time `json:"disabled_at,omitempty" xml:"disabled_at,omitempty" yaml:"disabled_at,omitempty"`
    38  }
    39  
    40  // NewPassword returns an instance of Password.
    41  func NewPassword(s string) (*Password, error) {
    42  	return NewPasswordWithOptions(s, "generic", "bcrypt", nil)
    43  }
    44  
    45  // NewPasswordWithOptions returns an instance of Password based on the
    46  // provided parameters.
    47  func NewPasswordWithOptions(s, purpose, algo string, params map[string]interface{}) (*Password, error) {
    48  	p := &Password{
    49  		Purpose:   purpose,
    50  		Algorithm: algo,
    51  		CreatedAt: time.Now().UTC(),
    52  	}
    53  
    54  	if params != nil {
    55  		if v, exists := params["cost"]; exists {
    56  			p.Cost = v.(int)
    57  		}
    58  	}
    59  
    60  	if err := p.hash(s); err != nil {
    61  		return nil, err
    62  	}
    63  	return p, nil
    64  }
    65  
    66  // Disable disables Password instance.
    67  func (p *Password) Disable() {
    68  	p.Expired = true
    69  	p.ExpiredAt = time.Now().UTC()
    70  	p.Disabled = true
    71  	p.DisabledAt = time.Now().UTC()
    72  }
    73  
    74  func (p *Password) hash(s string) error {
    75  	s = strings.TrimSpace(s)
    76  	if s == "" {
    77  		return errors.ErrPasswordEmpty
    78  	}
    79  
    80  	// Handle bcrypt hashed password.
    81  	if strings.HasPrefix(s, "bcrypt:") {
    82  		arr := strings.SplitN(s, ":", 3)
    83  		if len(arr) != 3 {
    84  			return errors.ErrPasswordHashed.WithArgs("unsupported format")
    85  		}
    86  		cost, err := strconv.Atoi(arr[1])
    87  		if err != nil {
    88  			return errors.ErrPasswordHashed.WithArgs("cost converstion failed")
    89  		}
    90  		if cost < 8 {
    91  			return errors.ErrPasswordHashed.WithArgs("cost value is too low")
    92  		}
    93  		p.Cost = cost
    94  		p.Hash = arr[2]
    95  		return nil
    96  	}
    97  
    98  	switch p.Algorithm {
    99  	case "bcrypt":
   100  		if p.Cost < 8 {
   101  			p.Cost = 10
   102  		}
   103  		ph, err := bcrypt.GenerateFromPassword([]byte(s), p.Cost)
   104  		if err != nil {
   105  			return errors.ErrPasswordGenerate.WithArgs(err)
   106  		}
   107  		p.Hash = string(ph)
   108  		return nil
   109  	case "":
   110  		return errors.ErrPasswordEmptyAlgorithm
   111  	}
   112  	return errors.ErrPasswordUnsupportedAlgorithm.WithArgs(p.Algorithm)
   113  }
   114  
   115  // Match returns true when the provided password matches the user.
   116  func (p *Password) Match(s string) bool {
   117  	if err := bcrypt.CompareHashAndPassword([]byte(p.Hash), []byte(s)); err == nil {
   118  		return true
   119  	}
   120  	return false
   121  }