mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00
Feature: Auth (#4)
* feat(api): add auth * feat(web): add auth and refactor * refactor(web): baseurl * feat: add autobrrctl cli for user creation * build: move static assets * refactor(web): auth guard and routing * refactor: rename var * fix: remove subrouter * build: update default config
This commit is contained in:
parent
2e8d0950c1
commit
40b855bf39
56 changed files with 1208 additions and 257 deletions
172
pkg/argon2id/argon2id.go
Normal file
172
pkg/argon2id/argon2id.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package argon2id
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidHash in returned by ComparePasswordAndHash if the provided
|
||||
// hash isn't in the expected format.
|
||||
ErrInvalidHash = errors.New("argon2id: hash is not in the correct format")
|
||||
|
||||
// ErrIncompatibleVersion in returned by ComparePasswordAndHash if the
|
||||
// provided hash was created using a different version of Argon2.
|
||||
ErrIncompatibleVersion = errors.New("argon2id: incompatible version of argon2")
|
||||
)
|
||||
|
||||
// DefaultParams provides some sane default parameters for hashing passwords.
|
||||
//
|
||||
// Follows recommendations given by the Argon2 RFC:
|
||||
// "The Argon2id variant with t=1 and maximum available memory is RECOMMENDED as a
|
||||
// default setting for all environments. This setting is secure against side-channel
|
||||
// attacks and maximizes adversarial costs on dedicated bruteforce hardware.""
|
||||
//
|
||||
// The default parameters should generally be used for development/testing purposes
|
||||
// only. Custom parameters should be set for production applications depending on
|
||||
// available memory/CPU resources and business requirements.
|
||||
var DefaultParams = &Params{
|
||||
Memory: 64 * 1024,
|
||||
Iterations: 1,
|
||||
Parallelism: 2,
|
||||
SaltLength: 16,
|
||||
KeyLength: 32,
|
||||
}
|
||||
|
||||
// Params describes the input parameters used by the Argon2id algorithm. The
|
||||
// Memory and Iterations parameters control the computational cost of hashing
|
||||
// the password. The higher these figures are, the greater the cost of generating
|
||||
// the hash and the longer the runtime. It also follows that the greater the cost
|
||||
// will be for any attacker trying to guess the password. If the code is running
|
||||
// on a machine with multiple cores, then you can decrease the runtime without
|
||||
// reducing the cost by increasing the Parallelism parameter. This controls the
|
||||
// number of threads that the work is spread across. Important note: Changing the
|
||||
// value of the Parallelism parameter changes the hash output.
|
||||
//
|
||||
// For guidance and an outline process for choosing appropriate parameters see
|
||||
// https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4
|
||||
type Params struct {
|
||||
// The amount of memory used by the algorithm (in kibibytes).
|
||||
Memory uint32
|
||||
|
||||
// The number of iterations over the memory.
|
||||
Iterations uint32
|
||||
|
||||
// The number of threads (or lanes) used by the algorithm.
|
||||
// Recommended value is between 1 and runtime.NumCPU().
|
||||
Parallelism uint8
|
||||
|
||||
// Length of the random salt. 16 bytes is recommended for password hashing.
|
||||
SaltLength uint32
|
||||
|
||||
// Length of the generated key. 16 bytes or more is recommended.
|
||||
KeyLength uint32
|
||||
}
|
||||
|
||||
// CreateHash returns a Argon2id hash of a plain-text password using the
|
||||
// provided algorithm parameters. The returned hash follows the format used by
|
||||
// the Argon2 reference C implementation and contains the base64-encoded Argon2id d
|
||||
// derived key prefixed by the salt and parameters. It looks like this:
|
||||
//
|
||||
// $argon2id$v=19$m=65536,t=3,p=2$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
|
||||
//
|
||||
func CreateHash(password string, params *Params) (hash string, err error) {
|
||||
salt, err := generateRandomBytes(params.SaltLength)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength)
|
||||
|
||||
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
||||
b64Key := base64.RawStdEncoding.EncodeToString(key)
|
||||
|
||||
hash = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, params.Memory, params.Iterations, params.Parallelism, b64Salt, b64Key)
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// ComparePasswordAndHash performs a constant-time comparison between a
|
||||
// plain-text password and Argon2id hash, using the parameters and salt
|
||||
// contained in the hash. It returns true if they match, otherwise it returns
|
||||
// false.
|
||||
func ComparePasswordAndHash(password, hash string) (match bool, err error) {
|
||||
match, _, err = CheckHash(password, hash)
|
||||
return match, err
|
||||
}
|
||||
|
||||
// CheckHash is like ComparePasswordAndHash, except it also returns the params that the hash was
|
||||
// created with. This can be useful if you want to update your hash params over time (which you
|
||||
// should).
|
||||
func CheckHash(password, hash string) (match bool, params *Params, err error) {
|
||||
params, salt, key, err := DecodeHash(hash)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
otherKey := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength)
|
||||
|
||||
keyLen := int32(len(key))
|
||||
otherKeyLen := int32(len(otherKey))
|
||||
|
||||
if subtle.ConstantTimeEq(keyLen, otherKeyLen) == 0 {
|
||||
return false, params, nil
|
||||
}
|
||||
if subtle.ConstantTimeCompare(key, otherKey) == 1 {
|
||||
return true, params, nil
|
||||
}
|
||||
return false, params, nil
|
||||
}
|
||||
|
||||
func generateRandomBytes(n uint32) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// DecodeHash expects a hash created from this package, and parses it to return the params used to
|
||||
// create it, as well as the salt and key (password hash).
|
||||
func DecodeHash(hash string) (params *Params, salt, key []byte, err error) {
|
||||
vals := strings.Split(hash, "$")
|
||||
if len(vals) != 6 {
|
||||
return nil, nil, nil, ErrInvalidHash
|
||||
}
|
||||
|
||||
var version int
|
||||
_, err = fmt.Sscanf(vals[2], "v=%d", &version)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if version != argon2.Version {
|
||||
return nil, nil, nil, ErrIncompatibleVersion
|
||||
}
|
||||
|
||||
params = &Params{}
|
||||
_, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", ¶ms.Memory, ¶ms.Iterations, ¶ms.Parallelism)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4])
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
params.SaltLength = uint32(len(salt))
|
||||
|
||||
key, err = base64.RawStdEncoding.Strict().DecodeString(vals[5])
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
params.KeyLength = uint32(len(key))
|
||||
|
||||
return params, salt, key, nil
|
||||
}
|
111
pkg/argon2id/argon2id_test.go
Normal file
111
pkg/argon2id/argon2id_test.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package argon2id
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateHash(t *testing.T) {
|
||||
hashRX, err := regexp.Compile(`^\$argon2id\$v=19\$m=65536,t=1,p=2\$[A-Za-z0-9+/]{22}\$[A-Za-z0-9+/]{43}$`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hash1, err := CreateHash("pa$$word", DefaultParams)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !hashRX.MatchString(hash1) {
|
||||
t.Errorf("hash %q not in correct format", hash1)
|
||||
}
|
||||
|
||||
hash2, err := CreateHash("pa$$word", DefaultParams)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if strings.Compare(hash1, hash2) == 0 {
|
||||
t.Error("hashes must be unique")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComparePasswordAndHash(t *testing.T) {
|
||||
hash, err := CreateHash("pa$$word", DefaultParams)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
match, err := ComparePasswordAndHash("pa$$word", hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !match {
|
||||
t.Error("expected password and hash to match")
|
||||
}
|
||||
|
||||
match, err = ComparePasswordAndHash("otherPa$$word", hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if match {
|
||||
t.Error("expected password and hash to not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeHash(t *testing.T) {
|
||||
hash, err := CreateHash("pa$$word", DefaultParams)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
params, _, _, err := DecodeHash(hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if *params != *DefaultParams {
|
||||
t.Fatalf("expected %#v got %#v", *DefaultParams, *params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHash(t *testing.T) {
|
||||
hash, err := CreateHash("pa$$word", DefaultParams)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ok, params, err := CheckHash("pa$$word", hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("expected password to match")
|
||||
}
|
||||
if *params != *DefaultParams {
|
||||
t.Fatalf("expected %#v got %#v", *DefaultParams, *params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictDecoding(t *testing.T) {
|
||||
// "bug" valid hash: $argon2id$v=19$m=65536,t=1,p=2$UDk0zEuIzbt0x3bwkf8Bgw$ihSfHWUJpTgDvNWiojrgcN4E0pJdUVmqCEdRZesx9tE
|
||||
ok, _, err := CheckHash("bug", "$argon2id$v=19$m=65536,t=1,p=2$UDk0zEuIzbt0x3bwkf8Bgw$ihSfHWUJpTgDvNWiojrgcN4E0pJdUVmqCEdRZesx9tE")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("expected password to match")
|
||||
}
|
||||
|
||||
// changed one last character of the hash
|
||||
ok, _, err = CheckHash("bug", "$argon2id$v=19$m=65536,t=1,p=2$UDk0zEuIzbt0x3bwkf8Bgw$ihSfHWUJpTgDvNWiojrgcN4E0pJdUVmqCEdRZesx9tF")
|
||||
if err == nil {
|
||||
t.Fatal("Hash validation should fail")
|
||||
}
|
||||
|
||||
if ok {
|
||||
t.Fatal("Hash validation should fail")
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue