Implement owner setup and browser authentication sessions
This commit is contained in:
parent
077cf7601a
commit
715423ab8e
21 changed files with 2185 additions and 14 deletions
127
internal/adapters/authn/password.go
Normal file
127
internal/adapters/authn/password.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Package authn implements password hashing and high-entropy credential generation.
|
||||
package authn
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
var ErrInvalidPasswordHash = errors.New("invalid password hash")
|
||||
|
||||
type Argon2Parameters struct {
|
||||
MemoryKiB uint32
|
||||
Time uint32
|
||||
Threads uint8
|
||||
SaltBytes uint32
|
||||
KeyBytes uint32
|
||||
}
|
||||
|
||||
type PasswordHasher struct {
|
||||
parameters Argon2Parameters
|
||||
}
|
||||
|
||||
func NewPasswordHasher() PasswordHasher {
|
||||
return PasswordHasher{parameters: Argon2Parameters{
|
||||
MemoryKiB: 64 * 1024,
|
||||
Time: 3,
|
||||
Threads: 4,
|
||||
SaltBytes: 16,
|
||||
KeyBytes: 32,
|
||||
}}
|
||||
}
|
||||
|
||||
func newPasswordHasher(parameters Argon2Parameters) PasswordHasher {
|
||||
return PasswordHasher{parameters: parameters}
|
||||
}
|
||||
|
||||
func (h PasswordHasher) Hash(password string) (string, error) {
|
||||
salt := make([]byte, h.parameters.SaltBytes)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", fmt.Errorf("generate password salt: %w", err)
|
||||
}
|
||||
derived := argon2.IDKey([]byte(password), salt, h.parameters.Time, h.parameters.MemoryKiB, h.parameters.Threads, h.parameters.KeyBytes)
|
||||
return fmt.Sprintf(
|
||||
"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
argon2.Version,
|
||||
h.parameters.MemoryKiB,
|
||||
h.parameters.Time,
|
||||
h.parameters.Threads,
|
||||
base64.RawStdEncoding.EncodeToString(salt),
|
||||
base64.RawStdEncoding.EncodeToString(derived),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (h PasswordHasher) Verify(password, encoded string) (bool, error) {
|
||||
parameters, salt, expected, err := parsePasswordHash(encoded)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
actual := argon2.IDKey([]byte(password), salt, parameters.Time, parameters.MemoryKiB, parameters.Threads, uint32(len(expected)))
|
||||
return subtle.ConstantTimeCompare(actual, expected) == 1, nil
|
||||
}
|
||||
|
||||
func parsePasswordHash(encoded string) (Argon2Parameters, []byte, []byte, error) {
|
||||
parts := strings.Split(encoded, "$")
|
||||
if len(parts) != 6 || parts[0] != "" || parts[1] != "argon2id" {
|
||||
return Argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
||||
}
|
||||
|
||||
version, err := parseNamedUint(parts[2], "v", 8)
|
||||
if err != nil || version != argon2.Version {
|
||||
return Argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
||||
}
|
||||
parameters, err := parseArgon2Parameters(parts[3])
|
||||
if err != nil {
|
||||
return Argon2Parameters{}, nil, nil, err
|
||||
}
|
||||
salt, err := base64.RawStdEncoding.Strict().DecodeString(parts[4])
|
||||
if err != nil || len(salt) < 16 || len(salt) > 64 {
|
||||
return Argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
||||
}
|
||||
derived, err := base64.RawStdEncoding.Strict().DecodeString(parts[5])
|
||||
if err != nil || len(derived) < 16 || len(derived) > 64 {
|
||||
return Argon2Parameters{}, nil, nil, ErrInvalidPasswordHash
|
||||
}
|
||||
parameters.SaltBytes = uint32(len(salt))
|
||||
parameters.KeyBytes = uint32(len(derived))
|
||||
return parameters, salt, derived, nil
|
||||
}
|
||||
|
||||
func parseArgon2Parameters(value string) (Argon2Parameters, error) {
|
||||
parts := strings.Split(value, ",")
|
||||
if len(parts) != 3 {
|
||||
return Argon2Parameters{}, ErrInvalidPasswordHash
|
||||
}
|
||||
memory, err := parseNamedUint(parts[0], "m", 32)
|
||||
if err != nil || memory < 8*1024 || memory > 1024*1024 {
|
||||
return Argon2Parameters{}, ErrInvalidPasswordHash
|
||||
}
|
||||
timeCost, err := parseNamedUint(parts[1], "t", 32)
|
||||
if err != nil || timeCost == 0 || timeCost > 10 {
|
||||
return Argon2Parameters{}, ErrInvalidPasswordHash
|
||||
}
|
||||
threads, err := parseNamedUint(parts[2], "p", 8)
|
||||
if err != nil || threads == 0 || threads > 16 {
|
||||
return Argon2Parameters{}, ErrInvalidPasswordHash
|
||||
}
|
||||
return Argon2Parameters{MemoryKiB: uint32(memory), Time: uint32(timeCost), Threads: uint8(threads)}, nil
|
||||
}
|
||||
|
||||
func parseNamedUint(value, name string, bitSize int) (uint64, error) {
|
||||
prefix := name + "="
|
||||
if !strings.HasPrefix(value, prefix) {
|
||||
return 0, ErrInvalidPasswordHash
|
||||
}
|
||||
parsed, err := strconv.ParseUint(strings.TrimPrefix(value, prefix), 10, bitSize)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidPasswordHash
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue