1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package pass_table2021import (22 "crypto/rand"23 "crypto/sha256"24 "crypto/subtle"25 "encoding/base64"26 "fmt"27 "io"28 "strconv"29 "strings"3031 "golang.org/x/crypto/argon2"32 "golang.org/x/crypto/bcrypt"33)3435const (36 HashSHA256 = "sha256"37 HashBcrypt = "bcrypt"38 HashArgon2 = "argon2"3940 DefaultHash = HashBcrypt4142 Argon2Salt = 1643 Argon2Size = 6444)4546type (47 // HashOpts is the structure that holds additional parameters for used hash48 // functions. They are used for new passwords.49 //50 // These parameters should be stored together with the hashed password51 // so it can be verified independently of the used HashOpts.52 HashOpts struct {53 // Bcrypt cost value to use. Should be at least 10.54 BcryptCost int5556 Argon2Time uint3257 Argon2Memory uint3258 Argon2Threads uint859 }6061 FuncHashCompute func(opts HashOpts, pass string) (string, error)62 FuncHashVerify func(pass, hashSalt string) error63)6465var (66 HashCompute = map[string]FuncHashCompute{67 HashBcrypt: computeBcrypt,68 HashArgon2: computeArgon2,69 }70 HashVerify = map[string]FuncHashVerify{71 HashBcrypt: verifyBcrypt,72 HashArgon2: verifyArgon2,73 }7475 Hashes = []string{HashSHA256, HashBcrypt, HashArgon2}76)7778func computeArgon2(opts HashOpts, pass string) (string, error) {79 salt := make([]byte, Argon2Salt)80 if _, err := io.ReadFull(rand.Reader, salt); err != nil {81 return "", fmt.Errorf("pass_table: failed to generate salt: %w", err)82 }8384 hash := argon2.IDKey([]byte(pass), salt, opts.Argon2Time, opts.Argon2Memory, opts.Argon2Threads, Argon2Size)85 var out strings.Builder86 out.WriteString(strconv.FormatUint(uint64(opts.Argon2Time), 10))87 out.WriteRune(':')88 out.WriteString(strconv.FormatUint(uint64(opts.Argon2Memory), 10))89 out.WriteRune(':')90 out.WriteString(strconv.FormatUint(uint64(opts.Argon2Threads), 10))91 out.WriteRune(':')92 out.WriteString(base64.StdEncoding.EncodeToString(salt))93 out.WriteRune(':')94 out.WriteString(base64.StdEncoding.EncodeToString(hash))95 return out.String(), nil96}9798func verifyArgon2(pass, hashSalt string) error {99 parts := strings.SplitN(hashSalt, ":", 5)100101 time, err := strconv.ParseUint(parts[0], 10, 32)102 if err != nil {103 return fmt.Errorf("pass_table: malformed hash string: %w", err)104 }105 memory, err := strconv.ParseUint(parts[1], 10, 32)106 if err != nil {107 return fmt.Errorf("pass_table: malformed hash string: %w", err)108 }109 threads, err := strconv.ParseUint(parts[2], 10, 8)110 if err != nil {111 return fmt.Errorf("pass_table: malformed hash string: %w", err)112 }113 salt, err := base64.StdEncoding.DecodeString(parts[3])114 if err != nil {115 return fmt.Errorf("pass_table: malformed hash string: %w", err)116 }117 hash, err := base64.StdEncoding.DecodeString(parts[4])118 if err != nil {119 return fmt.Errorf("pass_table: malformed hash string: %w", err)120 }121122 passHash := argon2.IDKey([]byte(pass), salt, uint32(time), uint32(memory), uint8(threads), Argon2Size)123 if subtle.ConstantTimeCompare(passHash, hash) != 1 {124 return fmt.Errorf("pass_table: hash mismatch")125 }126 return nil127}128129func computeSHA256(_ HashOpts, pass string) (string, error) {130 salt := make([]byte, 32)131 if _, err := io.ReadFull(rand.Reader, salt); err != nil {132 return "", fmt.Errorf("pass_table: failed to generate salt: %w", err)133 }134135 hashInput := salt136 hashInput = append(hashInput, []byte(pass)...)137 sum := sha256.Sum256(hashInput)138 return base64.StdEncoding.EncodeToString(salt) + ":" + base64.StdEncoding.EncodeToString(sum[:]), nil139}140141func verifySHA256(pass, hashSalt string) error {142 parts := strings.Split(hashSalt, ":")143 if len(parts) != 2 {144 return fmt.Errorf("pass_table: malformed hash string, no salt")145 }146 salt, err := base64.StdEncoding.DecodeString(parts[0])147 if err != nil {148 return fmt.Errorf("pass_table: malformed hash string, cannot decode pass: %w", err)149 }150 hash, err := base64.StdEncoding.DecodeString(parts[1])151 if err != nil {152 return fmt.Errorf("pass_table: malformed hash string, cannot decode pass: %w", err)153 }154155 hashInput := salt156 hashInput = append(hashInput, []byte(pass)...)157 sum := sha256.Sum256(hashInput)158159 if subtle.ConstantTimeCompare(sum[:], hash) != 1 {160 return fmt.Errorf("pass_table: hash mismatch")161 }162 return nil163}164165func computeBcrypt(opts HashOpts, pass string) (string, error) {166 hash, err := bcrypt.GenerateFromPassword([]byte(pass), opts.BcryptCost)167 if err != nil {168 return "", err169 }170 return string(hash), nil171}172173func verifyBcrypt(pass, hashSalt string) error {174 return bcrypt.CompareHashAndPassword([]byte(hashSalt), []byte(pass))175}176177func addSHA256() {178 HashCompute[HashSHA256] = computeSHA256179 HashVerify[HashSHA256] = verifySHA256180}