maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package pass_table
 20
 21import (
 22	"crypto/rand"
 23	"crypto/sha256"
 24	"crypto/subtle"
 25	"encoding/base64"
 26	"fmt"
 27	"io"
 28	"strconv"
 29	"strings"
 30
 31	"golang.org/x/crypto/argon2"
 32	"golang.org/x/crypto/bcrypt"
 33)
 34
 35const (
 36	HashSHA256 = "sha256"
 37	HashBcrypt = "bcrypt"
 38	HashArgon2 = "argon2"
 39
 40	DefaultHash = HashBcrypt
 41
 42	Argon2Salt = 16
 43	Argon2Size = 64
 44)
 45
 46type (
 47	// HashOpts is the structure that holds additional parameters for used hash
 48	// functions. They are used for new passwords.
 49	//
 50	// These parameters should be stored together with the hashed password
 51	// 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 int
 55
 56		Argon2Time    uint32
 57		Argon2Memory  uint32
 58		Argon2Threads uint8
 59	}
 60
 61	FuncHashCompute func(opts HashOpts, pass string) (string, error)
 62	FuncHashVerify  func(pass, hashSalt string) error
 63)
 64
 65var (
 66	HashCompute = map[string]FuncHashCompute{
 67		HashBcrypt: computeBcrypt,
 68		HashArgon2: computeArgon2,
 69	}
 70	HashVerify = map[string]FuncHashVerify{
 71		HashBcrypt: verifyBcrypt,
 72		HashArgon2: verifyArgon2,
 73	}
 74
 75	Hashes = []string{HashSHA256, HashBcrypt, HashArgon2}
 76)
 77
 78func 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	}
 83
 84	hash := argon2.IDKey([]byte(pass), salt, opts.Argon2Time, opts.Argon2Memory, opts.Argon2Threads, Argon2Size)
 85	var out strings.Builder
 86	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(), nil
 96}
 97
 98func verifyArgon2(pass, hashSalt string) error {
 99	parts := strings.SplitN(hashSalt, ":", 5)
100
101	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	}
121
122	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 nil
127}
128
129func 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	}
134
135	hashInput := salt
136	hashInput = append(hashInput, []byte(pass)...)
137	sum := sha256.Sum256(hashInput)
138	return base64.StdEncoding.EncodeToString(salt) + ":" + base64.StdEncoding.EncodeToString(sum[:]), nil
139}
140
141func 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	}
154
155	hashInput := salt
156	hashInput = append(hashInput, []byte(pass)...)
157	sum := sha256.Sum256(hashInput)
158
159	if subtle.ConstantTimeCompare(sum[:], hash) != 1 {
160		return fmt.Errorf("pass_table: hash mismatch")
161	}
162	return nil
163}
164
165func computeBcrypt(opts HashOpts, pass string) (string, error) {
166	hash, err := bcrypt.GenerateFromPassword([]byte(pass), opts.BcryptCost)
167	if err != nil {
168		return "", err
169	}
170	return string(hash), nil
171}
172
173func verifyBcrypt(pass, hashSalt string) error {
174	return bcrypt.CompareHashAndPassword([]byte(hashSalt), []byte(pass))
175}
176
177func addSHA256() {
178	HashCompute[HashSHA256] = computeSHA256
179	HashVerify[HashSHA256] = verifySHA256
180}