mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-30 18:22:49 -04:00 
			
		
		
		
	* feat: add argon2id hash-password command * feat: ardon2id owasp safe value * feat: add argon2id compare method * chore: fmt argon2id * docs: more argon2id docs * chore: upgrade x/crypto dep * revert: remove golangci * refactor: argon2id decode * chore: update deps * refactor: simplify argon2id compare return * chore: upgrade dependencies * chore: upgrade dependencies
		
			
				
	
	
		
			189 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			189 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2015 Matthew Holt and The Caddy Authors
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package caddyauth
 | |
| 
 | |
| import (
 | |
| 	"crypto/rand"
 | |
| 	"crypto/subtle"
 | |
| 	"encoding/base64"
 | |
| 	"fmt"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	"golang.org/x/crypto/argon2"
 | |
| 
 | |
| 	"github.com/caddyserver/caddy/v2"
 | |
| )
 | |
| 
 | |
| func init() {
 | |
| 	caddy.RegisterModule(Argon2idHash{})
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	argon2idName           = "argon2id"
 | |
| 	defaultArgon2idTime    = 1
 | |
| 	defaultArgon2idMemory  = 46 * 1024
 | |
| 	defaultArgon2idThreads = 1
 | |
| 	defaultArgon2idKeylen  = 32
 | |
| 	defaultSaltLength      = 16
 | |
| )
 | |
| 
 | |
| // Argon2idHash implements the Argon2id password hashing.
 | |
| type Argon2idHash struct {
 | |
| 	salt    []byte
 | |
| 	time    uint32
 | |
| 	memory  uint32
 | |
| 	threads uint8
 | |
| 	keyLen  uint32
 | |
| }
 | |
| 
 | |
| // CaddyModule returns the Caddy module information.
 | |
| func (Argon2idHash) CaddyModule() caddy.ModuleInfo {
 | |
| 	return caddy.ModuleInfo{
 | |
| 		ID:  "http.authentication.hashes.argon2id",
 | |
| 		New: func() caddy.Module { return new(Argon2idHash) },
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Compare checks if the plaintext password matches the given Argon2id hash.
 | |
| func (Argon2idHash) Compare(hashed, plaintext []byte) (bool, error) {
 | |
| 	argHash, storedKey, err := DecodeHash(hashed)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	computedKey := argon2.IDKey(
 | |
| 		plaintext,
 | |
| 		argHash.salt,
 | |
| 		argHash.time,
 | |
| 		argHash.memory,
 | |
| 		argHash.threads,
 | |
| 		argHash.keyLen,
 | |
| 	)
 | |
| 
 | |
| 	return subtle.ConstantTimeCompare(storedKey, computedKey) == 1, nil
 | |
| }
 | |
| 
 | |
| // Hash generates an Argon2id hash of the given plaintext using the configured parameters and salt.
 | |
| func (b Argon2idHash) Hash(plaintext []byte) ([]byte, error) {
 | |
| 	if b.salt == nil {
 | |
| 		s, err := generateSalt(defaultSaltLength)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		b.salt = s
 | |
| 	}
 | |
| 
 | |
| 	key := argon2.IDKey(
 | |
| 		plaintext,
 | |
| 		b.salt,
 | |
| 		b.time,
 | |
| 		b.memory,
 | |
| 		b.threads,
 | |
| 		b.keyLen,
 | |
| 	)
 | |
| 
 | |
| 	hash := fmt.Sprintf(
 | |
| 		"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
 | |
| 		argon2.Version,
 | |
| 		b.memory,
 | |
| 		b.time,
 | |
| 		b.threads,
 | |
| 		base64.RawStdEncoding.EncodeToString(b.salt),
 | |
| 		base64.RawStdEncoding.EncodeToString(key),
 | |
| 	)
 | |
| 
 | |
| 	return []byte(hash), nil
 | |
| }
 | |
| 
 | |
| // DecodeHash parses an Argon2id PHC string into an Argon2idHash struct and returns the struct along with the derived key.
 | |
| func DecodeHash(hash []byte) (*Argon2idHash, []byte, error) {
 | |
| 	parts := strings.Split(string(hash), "$")
 | |
| 	if len(parts) != 6 {
 | |
| 		return nil, nil, fmt.Errorf("invalid hash format")
 | |
| 	}
 | |
| 
 | |
| 	if parts[1] != argon2idName {
 | |
| 		return nil, nil, fmt.Errorf("unsupported variant: %s", parts[1])
 | |
| 	}
 | |
| 
 | |
| 	version, err := strconv.Atoi(strings.TrimPrefix(parts[2], "v="))
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("invalid version: %w", err)
 | |
| 	}
 | |
| 	if version != argon2.Version {
 | |
| 		return nil, nil, fmt.Errorf("incompatible version: %d", version)
 | |
| 	}
 | |
| 
 | |
| 	params := strings.Split(parts[3], ",")
 | |
| 	if len(params) != 3 {
 | |
| 		return nil, nil, fmt.Errorf("invalid parameters")
 | |
| 	}
 | |
| 
 | |
| 	mem, err := strconv.ParseUint(strings.TrimPrefix(params[0], "m="), 10, 32)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("invalid memory parameter: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	iter, err := strconv.ParseUint(strings.TrimPrefix(params[1], "t="), 10, 32)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("invalid iterations parameter: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	threads, err := strconv.ParseUint(strings.TrimPrefix(params[2], "p="), 10, 8)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("invalid parallelism parameter: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	salt, err := base64.RawStdEncoding.Strict().DecodeString(parts[4])
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("decode salt: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	key, err := base64.RawStdEncoding.Strict().DecodeString(parts[5])
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("decode key: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return &Argon2idHash{
 | |
| 		salt:    salt,
 | |
| 		time:    uint32(iter),
 | |
| 		memory:  uint32(mem),
 | |
| 		threads: uint8(threads),
 | |
| 		keyLen:  uint32(len(key)),
 | |
| 	}, key, nil
 | |
| }
 | |
| 
 | |
| // FakeHash returns a constant fake hash for timing attacks mitigation.
 | |
| func (Argon2idHash) FakeHash() []byte {
 | |
| 	// hashed with the following command:
 | |
| 	// caddy hash-password --plaintext "antitiming" --algorithm "argon2id"
 | |
| 	return []byte("$argon2id$v=19$m=47104,t=1,p=1$P2nzckEdTZ3bxCiBCkRTyA$xQL3Z32eo5jKl7u5tcIsnEKObYiyNZQQf5/4sAau6Pg")
 | |
| }
 | |
| 
 | |
| // Interface guards
 | |
| var (
 | |
| 	_ Comparer = (*Argon2idHash)(nil)
 | |
| 	_ Hasher   = (*Argon2idHash)(nil)
 | |
| )
 | |
| 
 | |
| func generateSalt(length int) ([]byte, error) {
 | |
| 	salt := make([]byte, length)
 | |
| 	if _, err := rand.Read(salt); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to generate salt: %w", err)
 | |
| 	}
 | |
| 	return salt, nil
 | |
| }
 |