github.com/greenpau/go-identity@v1.1.6/password.go (about)

     1  // Copyright 2020 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  	"github.com/greenpau/go-identity/pkg/errors"
    19  	"golang.org/x/crypto/bcrypt"
    20  	"strings"
    21  	"time"
    22  )
    23  
    24  // Password is a memorized secret, typically a string of characters,
    25  // used to confirm the identity of a user.
    26  type Password struct {
    27  	Purpose    string    `json:"purpose,omitempty" xml:"purpose,omitempty" yaml:"purpose,omitempty"`
    28  	Algorithm  string    `json:"algorithm,omitempty" xml:"algorithm,omitempty" yaml:"algorithm,omitempty"`
    29  	Hash       string    `json:"hash,omitempty" xml:"hash,omitempty" yaml:"hash,omitempty"`
    30  	Cost       int       `json:"cost,omitempty" xml:"cost,omitempty" yaml:"cost,omitempty"`
    31  	Expired    bool      `json:"expired,omitempty" xml:"expired,omitempty" yaml:"expired,omitempty"`
    32  	ExpiredAt  time.Time `json:"expired_at,omitempty" xml:"expired_at,omitempty" yaml:"expired_at,omitempty"`
    33  	CreatedAt  time.Time `json:"created_at,omitempty" xml:"created_at,omitempty" yaml:"created_at,omitempty"`
    34  	Disabled   bool      `json:"disabled,omitempty" xml:"disabled,omitempty" yaml:"disabled,omitempty"`
    35  	DisabledAt time.Time `json:"disabled_at,omitempty" xml:"disabled_at,omitempty" yaml:"disabled_at,omitempty"`
    36  }
    37  
    38  // NewPassword returns an instance of Password.
    39  func NewPassword(s string) (*Password, error) {
    40  	return NewPasswordWithOptions(s, "generic", "bcrypt", nil)
    41  }
    42  
    43  // NewPasswordWithOptions returns an instance of Password based on the
    44  // provided parameters.
    45  func NewPasswordWithOptions(s, purpose, algo string, params map[string]interface{}) (*Password, error) {
    46  	p := &Password{
    47  		Purpose:   purpose,
    48  		Algorithm: algo,
    49  		CreatedAt: time.Now().UTC(),
    50  	}
    51  
    52  	if params != nil {
    53  		if v, exists := params["cost"]; exists {
    54  			p.Cost = v.(int)
    55  		}
    56  	}
    57  
    58  	if err := p.hash(s); err != nil {
    59  		return nil, err
    60  	}
    61  	return p, nil
    62  }
    63  
    64  // Disable disables Password instance.
    65  func (p *Password) Disable() {
    66  	p.Expired = true
    67  	p.ExpiredAt = time.Now().UTC()
    68  	p.Disabled = true
    69  	p.DisabledAt = time.Now().UTC()
    70  }
    71  
    72  func (p *Password) hash(s string) error {
    73  	s = strings.TrimSpace(s)
    74  	if s == "" {
    75  		return errors.ErrPasswordEmpty
    76  	}
    77  	switch p.Algorithm {
    78  	case "bcrypt":
    79  		if p.Cost < 8 {
    80  			p.Cost = 10
    81  		}
    82  		ph, err := bcrypt.GenerateFromPassword([]byte(s), p.Cost)
    83  		if err != nil {
    84  			return errors.ErrPasswordGenerate.WithArgs(err)
    85  		}
    86  		p.Hash = string(ph)
    87  		return nil
    88  	case "":
    89  		return errors.ErrPasswordEmptyAlgorithm
    90  	}
    91  	return errors.ErrPasswordUnsupportedAlgorithm.WithArgs(p.Algorithm)
    92  }
    93  
    94  // Match returns true when the provided password matches the user.
    95  func (p *Password) Match(s string) bool {
    96  	if err := bcrypt.CompareHashAndPassword([]byte(p.Hash), []byte(s)); err == nil {
    97  		return true
    98  	}
    99  	return false
   100  }