pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/passwd/passwd.go (about)

     1  // Package passwd contains methods for working with passwords
     2  package passwd
     3  
     4  // ////////////////////////////////////////////////////////////////////////////////// //
     5  //                                                                                    //
     6  //                         Copyright (c) 2022 ESSENTIAL KAOS                          //
     7  //      Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>     //
     8  //                                                                                    //
     9  // ////////////////////////////////////////////////////////////////////////////////// //
    10  
    11  import (
    12  	"bytes"
    13  	"crypto/aes"
    14  	"crypto/cipher"
    15  	crand "crypto/rand"
    16  	"crypto/sha512"
    17  	"encoding/base64"
    18  	"errors"
    19  	"io"
    20  	"math/rand"
    21  	"strings"
    22  	"time"
    23  
    24  	"golang.org/x/crypto/bcrypt"
    25  )
    26  
    27  // ////////////////////////////////////////////////////////////////////////////////// //
    28  
    29  type Strength int
    30  
    31  // ////////////////////////////////////////////////////////////////////////////////// //
    32  
    33  const (
    34  	STRENGTH_WEAK   Strength = iota // Only lowercase English alphabet characters
    35  	STRENGTH_MEDIUM                 // Lowercase and uppercase English alphabet characters, digits
    36  	STRENGTH_STRONG                 // Lowercase and uppercase English alphabet characters, digits, special symbols
    37  )
    38  
    39  // ////////////////////////////////////////////////////////////////////////////////// //
    40  
    41  const (
    42  	_SYMBOLS_WEAK   = "abcdefghijklmnopqrstuvwxyz"
    43  	_SYMBOLS_MEDIUM = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    44  	_SYMBOLS_STRONG = "!\";%:?*()_+=-~/\\<>,.[]{}"
    45  )
    46  
    47  // ////////////////////////////////////////////////////////////////////////////////// //
    48  
    49  // Encrypt creates hash and encrypts it with salt and pepper
    50  // Deprecated: Use Hash method instead
    51  func Encrypt(password, pepper string) (string, error) {
    52  	return Hash(password, pepper)
    53  }
    54  
    55  // Hash creates hash and encrypts it with salt and pepper
    56  func Hash(password, pepper string) (string, error) {
    57  	return HashBytes([]byte(password), []byte(pepper))
    58  }
    59  
    60  // HashBytes creates hash and encrypts it with salt and pepper
    61  func HashBytes(password, pepper []byte) (string, error) {
    62  	switch {
    63  	case len(password) == 0:
    64  		return "", errors.New("Password can't be empty")
    65  	case len(pepper) == 0:
    66  		return "", errors.New("Pepper can't be empty")
    67  	}
    68  
    69  	if !isValidPepper(pepper) {
    70  		return "", errors.New("Pepper have invalid size")
    71  	}
    72  
    73  	hasher := sha512.New()
    74  	hasher.Write(password)
    75  
    76  	hp, err := bcrypt.GenerateFromPassword(hasher.Sum(nil), 10)
    77  
    78  	if err != nil {
    79  		return "", err
    80  	}
    81  
    82  	block, _ := aes.NewCipher(pepper)
    83  	hpd := padData(hp)
    84  
    85  	ct := make([]byte, aes.BlockSize+len(hpd))
    86  	iv := ct[:aes.BlockSize]
    87  
    88  	_, err = io.ReadFull(crand.Reader, iv)
    89  
    90  	if err != nil {
    91  		return "", err
    92  	}
    93  
    94  	cfb := cipher.NewCFBEncrypter(block, iv)
    95  	cfb.XORKeyStream(ct[aes.BlockSize:], hpd)
    96  
    97  	return removeBase64Padding(base64.URLEncoding.EncodeToString(ct)), nil
    98  }
    99  
   100  // Check compares password with encrypted hash
   101  func Check(password, pepper, hash string) bool {
   102  	return CheckBytes([]byte(password), []byte(pepper), hash)
   103  }
   104  
   105  // CheckBytes compares password with encrypted hash
   106  func CheckBytes(password, pepper []byte, hash string) bool {
   107  	if len(password) == 0 || len(hash) == 0 || !isValidPepper(pepper) {
   108  		return false
   109  	}
   110  
   111  	block, _ := aes.NewCipher(pepper)
   112  	hpd, err := base64.URLEncoding.DecodeString(addBase64Padding(hash))
   113  
   114  	if err != nil {
   115  		return false
   116  	}
   117  
   118  	hdpl := len(hpd)
   119  
   120  	if hdpl < aes.BlockSize || (hdpl%aes.BlockSize) != 0 {
   121  		return false
   122  	}
   123  
   124  	iv := hpd[:aes.BlockSize]
   125  	hp := hpd[aes.BlockSize:]
   126  
   127  	if len(hp) == 0 {
   128  		return false
   129  	}
   130  
   131  	cfb := cipher.NewCFBDecrypter(block, iv)
   132  	cfb.XORKeyStream(hp, hp)
   133  
   134  	h, ok := unpadData(hp)
   135  
   136  	if !ok {
   137  		return false
   138  	}
   139  
   140  	hasher := sha512.New()
   141  	hasher.Write(password)
   142  
   143  	return bcrypt.CompareHashAndPassword(h, hasher.Sum(nil)) == nil
   144  }
   145  
   146  // GenPassword generates random password
   147  func GenPassword(length int, strength Strength) string {
   148  	return string(GenPasswordBytes(length, strength))
   149  }
   150  
   151  // GenPassword generates random password
   152  func GenPasswordBytes(length int, strength Strength) []byte {
   153  	return getRandomPasswordBytes(length, getStrength(strength))
   154  }
   155  
   156  // GetPasswordStrength returns password strength
   157  func GetPasswordStrength(password string) Strength {
   158  	return GetPasswordBytesStrength([]byte(password))
   159  }
   160  
   161  // GetPasswordBytesStrength returns password strength
   162  func GetPasswordBytesStrength(password []byte) Strength {
   163  	if len(password) == 0 {
   164  		return STRENGTH_WEAK
   165  	}
   166  
   167  	var conditions int
   168  
   169  	if bytes.ContainsAny(password, "abcdefghijklmnopqrstuvwxyz") &&
   170  		bytes.ContainsAny(password, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
   171  		conditions++
   172  	}
   173  
   174  	if bytes.ContainsAny(password, "1234567890") {
   175  		conditions++
   176  	}
   177  
   178  	if bytes.ContainsAny(password, _SYMBOLS_STRONG) {
   179  		conditions++
   180  	}
   181  
   182  	if len(password) < 6 {
   183  		conditions = 1
   184  	} else {
   185  		conditions++
   186  	}
   187  
   188  	switch conditions {
   189  	case 4:
   190  		return STRENGTH_STRONG
   191  	case 3:
   192  		return STRENGTH_MEDIUM
   193  	}
   194  
   195  	return STRENGTH_WEAK
   196  }
   197  
   198  // GenPasswordVariations generates password variants with possible
   199  // typos fixes (case swap for all letters, first leter swap, password
   200  // without last symbol)
   201  func GenPasswordVariations(password string) []string {
   202  	if len(password) < 6 {
   203  		return nil
   204  	}
   205  
   206  	var result []string
   207  
   208  	passwordBytes := []byte(password)
   209  
   210  	result = append(result, string(genVariantFlipAll(passwordBytes)))
   211  	result = append(result, string(genVariantFlipFirst(passwordBytes)))
   212  	result = append(result, string(genVariantTrimLast(passwordBytes)))
   213  
   214  	return result
   215  }
   216  
   217  // GenPasswordBytesVariations generates password variants with possible
   218  // typos fixes (case swap for all letters, first leter swap, password
   219  // without last symbol)
   220  func GenPasswordBytesVariations(password []byte) [][]byte {
   221  	if len(password) < 6 {
   222  		return nil
   223  	}
   224  
   225  	var result [][]byte
   226  
   227  	result = append(result, genVariantFlipAll(password))
   228  	result = append(result, genVariantFlipFirst(password))
   229  	result = append(result, genVariantTrimLast(password))
   230  
   231  	return result
   232  }
   233  
   234  // ////////////////////////////////////////////////////////////////////////////////// //
   235  
   236  func getStrength(s Strength) Strength {
   237  	if s < STRENGTH_WEAK {
   238  		return STRENGTH_WEAK
   239  	}
   240  
   241  	if s > STRENGTH_STRONG {
   242  		return STRENGTH_STRONG
   243  	}
   244  
   245  	return s
   246  }
   247  
   248  func padData(src []byte) []byte {
   249  	padding := aes.BlockSize - len(src)%aes.BlockSize
   250  	padText := bytes.Repeat([]byte{byte(padding)}, padding)
   251  
   252  	return append(src, padText...)
   253  }
   254  
   255  func unpadData(src []byte) ([]byte, bool) {
   256  	length := len(src)
   257  	unpadding := int(src[length-1])
   258  
   259  	if unpadding > length {
   260  		return nil, false
   261  	}
   262  
   263  	return src[:(length - unpadding)], true
   264  }
   265  
   266  func addBase64Padding(src string) string {
   267  	m := len(src) % 4
   268  
   269  	if m != 0 {
   270  		src += strings.Repeat("=", 4-m)
   271  	}
   272  
   273  	return src
   274  }
   275  
   276  func removeBase64Padding(src string) string {
   277  	return strings.TrimRight(src, "=")
   278  }
   279  
   280  func getRandomPasswordBytes(length int, strength Strength) []byte {
   281  	if length == 0 {
   282  		return nil
   283  	}
   284  
   285  	if strength == STRENGTH_STRONG && length < 6 {
   286  		length = 6
   287  	}
   288  
   289  	var symbols = _SYMBOLS_WEAK
   290  
   291  	switch strength {
   292  	case STRENGTH_MEDIUM:
   293  		symbols += _SYMBOLS_MEDIUM
   294  	case STRENGTH_STRONG:
   295  		symbols += _SYMBOLS_MEDIUM + _SYMBOLS_STRONG
   296  	}
   297  
   298  	ls := len(symbols)
   299  	buf := make([]byte, length)
   300  
   301  	for {
   302  		rand.Seed(time.Now().UTC().UnixNano())
   303  
   304  		for i := 0; i < length; i++ {
   305  			buf[i] = symbols[rand.Intn(ls)]
   306  		}
   307  
   308  		if GetPasswordBytesStrength(buf) == strength {
   309  			return buf
   310  		}
   311  	}
   312  }
   313  
   314  func isValidPepper(pepper []byte) bool {
   315  	switch len(pepper) {
   316  	case 16, 24, 32:
   317  		return true
   318  	}
   319  
   320  	return false
   321  }
   322  
   323  func genVariantFlipAll(password []byte) []byte {
   324  	result := make([]byte, len(password))
   325  
   326  	for i := 0; i < len(password); i++ {
   327  		result[i] = flipCase(password[i])
   328  	}
   329  
   330  	return result
   331  }
   332  
   333  func genVariantFlipFirst(password []byte) []byte {
   334  	result := make([]byte, len(password))
   335  
   336  	copy(result, password)
   337  
   338  	result[0] = flipCase(password[0])
   339  
   340  	return result
   341  }
   342  
   343  func genVariantTrimLast(password []byte) []byte {
   344  	return append(password[:0:0], password[:len(password)-1]...)
   345  }
   346  
   347  func flipCase(b byte) byte {
   348  	s := string(b)
   349  	sc := strings.ToLower(s)
   350  
   351  	if s != sc {
   352  		return byte(sc[0])
   353  	}
   354  
   355  	return byte(strings.ToUpper(s)[0])
   356  }