caddy/modules/caddyhttp/caddyauth/argon2id.go
GreyXor 13a4ec7597
basicauth: Implement argon2id (#7186)
* 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
2025-10-06 17:27:06 -06:00

188 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
}