mirror of
https://github.com/caddyserver/caddy.git
synced 2025-10-19 15:53:17 +00:00
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
|
||
|
}
|