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 }