[management, reverse proxy] Add reverse proxy feature (#5291)

* implement reverse proxy


---------

Co-authored-by: Alisdair MacLeod <git@alisdairmacleod.co.uk>
Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
Co-authored-by: Viktor Liu <viktor@netbird.io>
Co-authored-by: Diego Noguês <diego.sure@gmail.com>
Co-authored-by: Diego Noguês <49420+diegocn@users.noreply.github.com>
Co-authored-by: Bethuel Mmbaga <bethuelmbaga12@gmail.com>
Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com>
Co-authored-by: Ashley Mensah <ashleyamo982@gmail.com>
This commit is contained in:
Pascal Fischer
2026-02-13 19:37:43 +01:00
committed by GitHub
co-authored by Alisdair MacLeod mlsmaycon Eduard Gert Viktor Liu Diego Noguês Diego Noguês Bethuel Mmbaga Zoltan Papp Ashley Mensah
parent edce11b34d
commit f53155562f
225 changed files with 35513 additions and 235 deletions
+136
View File
@@ -0,0 +1,136 @@
package argon2id
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
const (
argon2Memory = 19456
argon2Iterations = 2
argon2Parallelism = 1
argon2SaltLength = 16
argon2KeyLength = 32
)
var (
// ErrInvalidHash is returned when the hash string format is invalid
ErrInvalidHash = errors.New("invalid hash format")
// ErrIncompatibleVersion is returned when the Argon2 version is not supported
ErrIncompatibleVersion = errors.New("incompatible argon2 version")
// ErrMismatchedHashAndPassword is returned when password verification fails
ErrMismatchedHashAndPassword = errors.New("password does not match hash")
)
func Hash(secret string) (string, error) {
salt := make([]byte, argon2SaltLength)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("failed to generate salt: %w", err)
}
hash := argon2.IDKey(
[]byte(secret),
salt,
argon2Iterations,
argon2Memory,
argon2Parallelism,
argon2KeyLength,
)
encodedSalt := base64.RawStdEncoding.EncodeToString(salt)
encodedHash := base64.RawStdEncoding.EncodeToString(hash)
return fmt.Sprintf(
"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version,
argon2Memory,
argon2Iterations,
argon2Parallelism,
encodedSalt,
encodedHash,
), nil
}
func Verify(secret, encodedHash string) error {
params, salt, hash, err := decodeHash(encodedHash)
if err != nil {
return err
}
computedHash := argon2.IDKey(
[]byte(secret),
salt,
params.iterations,
params.memory,
params.parallelism,
params.keyLength,
)
if subtle.ConstantTimeCompare(hash, computedHash) == 1 {
return nil
}
return ErrMismatchedHashAndPassword
}
type hashParams struct {
memory uint32
iterations uint32
parallelism uint8
keyLength uint32
version int
}
func decodeHash(encodedHash string) (*hashParams, []byte, []byte, error) {
parts := strings.Split(encodedHash, "$")
if len(parts) != 6 {
return nil, nil, nil, ErrInvalidHash
}
if parts[1] != "argon2id" {
return nil, nil, nil, ErrInvalidHash
}
var version int
if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil {
return nil, nil, nil, fmt.Errorf("%w: invalid version: %v", ErrInvalidHash, err)
}
if version != argon2.Version {
return nil, nil, nil, ErrIncompatibleVersion
}
var memory, iterations uint32
var parallelism uint8
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, &parallelism); err != nil {
return nil, nil, nil, fmt.Errorf("%w: invalid parameters: %v", ErrInvalidHash, err)
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return nil, nil, nil, fmt.Errorf("%w: invalid salt encoding: %v", ErrInvalidHash, err)
}
hash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return nil, nil, nil, fmt.Errorf("%w: invalid hash encoding: %v", ErrInvalidHash, err)
}
params := &hashParams{
memory: memory,
iterations: iterations,
parallelism: parallelism,
keyLength: uint32(len(hash)),
version: version,
}
return params, salt, hash, nil
}
+327
View File
@@ -0,0 +1,327 @@
package argon2id
import (
"errors"
"strings"
"testing"
"golang.org/x/crypto/argon2"
)
func TestHash(t *testing.T) {
tests := []struct {
name string
secret string
}{
{
name: "simple password",
secret: "password123",
},
{
name: "complex password with special chars",
secret: "P@ssw0rd!#$%^&*()",
},
{
name: "long password",
secret: strings.Repeat("a", 100),
},
{
name: "empty password",
secret: "",
},
{
name: "unicode password",
secret: "пароль密码🔐",
},
{
name: "numeric PIN",
secret: "123456",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hash, err := Hash(tt.secret)
if err != nil {
t.Fatalf("Hash() error = %v", err)
}
// Verify hash format
if !strings.HasPrefix(hash, "$argon2id$") {
t.Errorf("Hash() = %v, want hash starting with $argon2id$", hash)
}
// Verify hash has correct number of components
parts := strings.Split(hash, "$")
if len(parts) != 6 {
t.Errorf("Hash() has %d parts, want 6", len(parts))
}
// Verify version is present
if !strings.HasPrefix(hash, "$argon2id$v=") {
t.Errorf("Hash() missing version, got %v", hash)
}
// Verify each hash is unique (different salt)
hash2, err := Hash(tt.secret)
if err != nil {
t.Fatalf("Hash() second call error = %v", err)
}
if hash == hash2 {
t.Error("Hash() produces identical hashes for same input (salt not random)")
}
})
}
}
func TestVerify(t *testing.T) {
tests := []struct {
name string
secret string
wantError error
}{
{
name: "valid password",
secret: "correctPassword",
wantError: nil,
},
{
name: "valid PIN",
secret: "1234",
wantError: nil,
},
{
name: "empty secret",
secret: "",
wantError: nil,
},
{
name: "unicode secret",
secret: "密码🔐",
wantError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Generate hash
hash, err := Hash(tt.secret)
if err != nil {
t.Fatalf("Hash() error = %v", err)
}
// Verify correct secret
err = Verify(tt.secret, hash)
if !errors.Is(err, tt.wantError) {
t.Errorf("Verify() error = %v, wantError %v", err, tt.wantError)
}
})
}
}
func TestVerifyIncorrectPassword(t *testing.T) {
secret := "correctPassword"
wrongSecret := "wrongPassword"
hash, err := Hash(secret)
if err != nil {
t.Fatalf("Hash() error = %v", err)
}
err = Verify(wrongSecret, hash)
if !errors.Is(err, ErrMismatchedHashAndPassword) {
t.Errorf("Verify() error = %v, want %v", err, ErrMismatchedHashAndPassword)
}
}
func TestVerifyInvalidHashFormat(t *testing.T) {
tests := []struct {
name string
invalidHash string
expectedError error
}{
{
name: "empty hash",
invalidHash: "",
expectedError: ErrInvalidHash,
},
{
name: "wrong algorithm",
invalidHash: "$bcrypt$v=19$m=19456,t=2,p=1$c2FsdA$aGFzaA",
expectedError: ErrInvalidHash,
},
{
name: "missing parts",
invalidHash: "$argon2id$v=19$m=19456",
expectedError: ErrInvalidHash,
},
{
name: "too many parts",
invalidHash: "$argon2id$v=19$m=19456,t=2,p=1$salt$hash$extra",
expectedError: ErrInvalidHash,
},
{
name: "invalid version format",
invalidHash: "$argon2id$vXX$m=19456,t=2,p=1$c2FsdA$aGFzaA",
expectedError: ErrInvalidHash,
},
{
name: "invalid parameters format",
invalidHash: "$argon2id$v=19$mXX,tYY,pZZ$c2FsdA$aGFzaA",
expectedError: ErrInvalidHash,
},
{
name: "invalid salt base64",
invalidHash: "$argon2id$v=19$m=19456,t=2,p=1$not-valid-base64!@#$aGFzaA",
expectedError: ErrInvalidHash,
},
{
name: "invalid hash base64",
invalidHash: "$argon2id$v=19$m=19456,t=2,p=1$c2FsdA$not-valid-base64!@#",
expectedError: ErrInvalidHash,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Verify("password", tt.invalidHash)
if err == nil {
t.Errorf("Verify() expected error, got nil")
return
}
if !errors.Is(err, tt.expectedError) && !strings.Contains(err.Error(), tt.expectedError.Error()) {
t.Errorf("Verify() error = %v, want error containing %v", err, tt.expectedError)
}
})
}
}
func TestVerifyIncompatibleVersion(t *testing.T) {
// Manually craft a hash with wrong version
invalidVersionHash := "$argon2id$v=18$m=19456,t=2,p=1$c2FsdDEyMzQ1Njc4OTA$aGFzaDEyMzQ1Njc4OTBhYmNkZWZnaGlqa2xtbm9w"
err := Verify("password", invalidVersionHash)
if !errors.Is(err, ErrIncompatibleVersion) {
t.Errorf("Verify() error = %v, want %v", err, ErrIncompatibleVersion)
}
}
func TestHashDeterminism(t *testing.T) {
// Ensure different hashes for same password (random salt)
password := "testPassword"
hashes := make(map[string]bool)
for i := 0; i < 10; i++ {
hash, err := Hash(password)
if err != nil {
t.Fatalf("Hash() error = %v", err)
}
if hashes[hash] {
t.Error("Hash() produced duplicate hash (salt generation may be broken)")
}
hashes[hash] = true
}
if len(hashes) != 10 {
t.Errorf("Expected 10 unique hashes, got %d", len(hashes))
}
}
func TestOWASPCompliance(t *testing.T) {
// Test that generated hashes use OWASP-recommended parameters
secret := "testPassword"
hash, err := Hash(secret)
if err != nil {
t.Fatalf("Hash() error = %v", err)
}
params, _, _, err := decodeHash(hash)
if err != nil {
t.Fatalf("decodeHash() error = %v", err)
}
// Verify OWASP minimum baseline parameters
if params.memory != 19456 {
t.Errorf("memory = %d, want 19456 (OWASP baseline)", params.memory)
}
if params.iterations != 2 {
t.Errorf("iterations = %d, want 2 (OWASP baseline)", params.iterations)
}
if params.parallelism != 1 {
t.Errorf("parallelism = %d, want 1 (OWASP baseline)", params.parallelism)
}
if params.keyLength != 32 {
t.Errorf("keyLength = %d, want 32", params.keyLength)
}
if params.version != argon2.Version {
t.Errorf("version = %d, want %d", params.version, argon2.Version)
}
}
func TestConstantTimeComparison(t *testing.T) {
// This test verifies that Verify() is using constant-time comparison
// by ensuring it doesn't fail differently for similar vs different hashes
secret := "password123"
wrongSecret := "password124" // One character different
hash, err := Hash(secret)
if err != nil {
t.Fatalf("Hash() error = %v", err)
}
// Both wrong passwords should return the same error
err1 := Verify(wrongSecret, hash)
err2 := Verify("completelydifferent", hash)
if !errors.Is(err1, ErrMismatchedHashAndPassword) {
t.Errorf("Verify() error = %v, want %v", err1, ErrMismatchedHashAndPassword)
}
if !errors.Is(err2, ErrMismatchedHashAndPassword) {
t.Errorf("Verify() error = %v, want %v", err2, ErrMismatchedHashAndPassword)
}
// Errors should be identical (same error type and message)
if err1.Error() != err2.Error() {
t.Error("Verify() returns different errors for different wrong passwords (potential timing attack)")
}
}
func TestCaseSensitivity(t *testing.T) {
// Passwords should be case-sensitive
secret := "Password123"
wrongSecret := "password123"
hash, err := Hash(secret)
if err != nil {
t.Fatalf("Hash() error = %v", err)
}
// Correct password should verify
if err := Verify(secret, hash); err != nil {
t.Errorf("Verify() with correct password error = %v, want nil", err)
}
// Wrong case should not verify
if err := Verify(wrongSecret, hash); !errors.Is(err, ErrMismatchedHashAndPassword) {
t.Errorf("Verify() with wrong case error = %v, want %v", err, ErrMismatchedHashAndPassword)
}
}
// Benchmark tests
func BenchmarkHash(b *testing.B) {
secret := "benchmarkPassword123"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = Hash(secret)
}
}
func BenchmarkVerify(b *testing.B) {
secret := "benchmarkPassword123"
hash, _ := Hash(secret)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Verify(secret, hash)
}
}