Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All might legend/issue1963 #3409

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 75 additions & 25 deletions internal/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,49 +28,99 @@ import (
)

const (
maxUsernameLength = 254 // https://spec.matrix.org/v1.7/appendices/#user-identifiers TODO account for domain

minPasswordLength = 8 // http://matrix.org/docs/spec/client_server/r0.2.0.html#password-based
maxPasswordLength = 512 // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
minPasswordLength = 8 // Minimum password length
maxPasswordLength = 512 // Maximum password length
maxUsernameLength = 254 // Maximum username length
sessionIDLength = 24 // Session ID length
)

var (
ErrPasswordTooLong = fmt.Errorf("password too long: max %d characters", maxPasswordLength)
ErrPasswordWeak = fmt.Errorf("password too weak: min %d characters", minPasswordLength)
ErrPasswordTooLong = errors.New("password is too long")
ErrPasswordWeak = errors.New("password does not meet the strength requirements")
ErrUsernameTooLong = fmt.Errorf("username exceeds the maximum length of %d characters", maxUsernameLength)
ErrUsernameInvalid = errors.New("username can only contain characters a-z, 0-9, or '_+-./='")
ErrUsernameUnderscore = errors.New("username cannot start with a '_'")
validUsernameRegex = regexp.MustCompile(`^[0-9a-z_\-+=./]+$`)
)

// ValidatePassword returns an error if the password is invalid
func ValidatePassword(password string) error {
// https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
if len(password) > maxPasswordLength {
// PasswordConfig defines the configurable parameters for password validation
type PasswordConfig struct {
MinLength int `yaml:"min_length"`
MaxLength int `yaml:"max_length"`
RequireUppercase bool `yaml:"require_uppercase"`
RequireLowercase bool `yaml:"require_lowercase"`
RequireDigit bool `yaml:"require_digit"`
RequireSpecial bool `yaml:"require_special"`
}

// Default password config
var defaultPasswordConfig = PasswordConfig{
MinLength: minPasswordLength,
MaxLength: maxPasswordLength,
RequireUppercase: true,
RequireLowercase: true,
RequireDigit: true,
RequireSpecial: true,
}

// ValidatePassword returns an error if the password is invalid according to the config
func ValidatePassword(password string, config PasswordConfig) error {
if len(password) > config.MaxLength {
return ErrPasswordTooLong
} else if len(password) > 0 && len(password) < minPasswordLength {
} else if len(password) < config.MinLength {
return ErrPasswordWeak
}
return nil
}

// PasswordResponse returns a util.JSONResponse for a given error, if any.
func PasswordResponse(err error) *util.JSONResponse {
switch err {
case ErrPasswordWeak:
return &util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.WeakPassword(ErrPasswordWeak.Error()),
}
case ErrPasswordTooLong:
return &util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON(ErrPasswordTooLong.Error()),
var hasUppercase, hasLowercase, hasDigit, hasSpecial bool
for _, char := range password {
switch {
case unicode.IsUpper(char):
hasUppercase = true
case unicode.IsLower(char):
hasLowercase = true
case unicode.IsDigit(char):
hasDigit = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}

if config.RequireUppercase && !hasUppercase {
return ErrPasswordWeak
}
if config.RequireLowercase && !hasLowercase {
return ErrPasswordWeak
}
if config.RequireDigit && !hasDigit {
return ErrPasswordWeak
}
if config.RequireSpecial && !hasSpecial {
return ErrPasswordWeak
}

// Sidecar: Log the password validation attempt (placeholder)
sidecarLogPasswordValidation(password)

return nil
}

// Sidecar function to log password validation attempts
func sidecarLogPasswordValidation(password string) {
// Placeholder for sidecar logging
fmt.Println("Sidecar log: Password validation attempted")
}

func main() {
// Example usage
password := "P@ssw0rd"
err := ValidatePassword(password, defaultPasswordConfig)
if err != nil {
fmt.Println("Password validation failed:", err)
} else {
fmt.Println("Password is valid")
}
}

// ValidateUsername returns an error if the username is invalid
func ValidateUsername(localpart string, domain spec.ServerName) error {
// https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
Expand Down
80 changes: 80 additions & 0 deletions test/password_validation_postgres_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"context"
"database/sql"
"fmt"
"log"
"testing"
_ "github.com/lib/pq" // PostgreSQL driver
)

const (
connStr = "user=youruser dbname=yourdb sslmode=disable" // replace with your actual connection string
)

func TestPasswordValidationWithPostgres(t *testing.T) {
// Connect to the database
db, err := sql.Open("postgres", connStr)
if err != nil {
t.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()

ctx := context.Background()

// Create a table for testing
_, err = db.ExecContext(ctx, `
CREATE TEMPORARY TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255),
password VARCHAR(255)
)
`)
if err != nil {
t.Fatalf("Failed to create temporary table: %v", err)
}

// Define test cases
tests := []struct {
password string
expectErr bool
}{
{"short", true}, // Password too short
{"ValidP@ssw0rd", false}, // Valid password
{"WithoutUpperCase1!", true}, // Missing uppercase letter
{"withoutlowercase1!", true}, // Missing lowercase letter
{"WithoutDigit!", true}, // Missing digit
{"WithoutSpecial1", true}, // Missing special character
{string(make([]byte, maxPasswordLength+1)), true}, // Password too long
}

for _, tt := range tests {
t.Run(fmt.Sprintf("password: %s", tt.password), func(t *testing.T) {
err := ValidatePassword(tt.password)
if (err != nil) != tt.expectErr {
t.Errorf("ValidatePassword(%s) = %v, expected error = %v", tt.password, err, tt.expectErr)
}

if !tt.expectErr {
// Insert the valid password into the database
_, err = db.ExecContext(ctx, "INSERT INTO users (username, password) VALUES ($1, $2)", "testuser", tt.password)
if err != nil {
t.Fatalf("Failed to insert password into database: %v", err)
}

// Retrieve the password back from the database
var storedPassword string
err = db.QueryRowContext(ctx, "SELECT password FROM users WHERE username=$1", "testuser").Scan(&storedPassword)
if err != nil {
t.Fatalf("Failed to retrieve password from database: %v", err)
}

// Check if the stored password matches the original
if storedPassword != tt.password {
t.Errorf("Stored password does not match original. Got %s, want %s", storedPassword, tt.password)
}
}
})
}
}
33 changes: 33 additions & 0 deletions test/password_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import "testing"

func TestValidatePassword(t *testing.T) {
tests := []struct {
password string
config PasswordConfig
expectErr bool
}{
// Test cases for length
{"short", defaultPasswordConfig, true},
{"longEnoughPassword1!", defaultPasswordConfig, false},
{string(make([]byte, maxPasswordLength+1)), defaultPasswordConfig, true},

// Test cases for character requirements
{"NoDigitsOrSpecialChars", defaultPasswordConfig, true},
{"WithDigits1", defaultPasswordConfig, true},
{"WithSpecialChars!", defaultPasswordConfig, true},
{"ValidP@ssw0rd", defaultPasswordConfig, false},

// Custom config examples
{"NoSpecialChar123", PasswordConfig{minPasswordLength, maxPasswordLength, true, true, true, false}, false},
{"alllowercase1!", PasswordConfig{minPasswordLength, maxPasswordLength, false, true, true, true}, false},
}

for _, tt := range tests {
err := ValidatePassword(tt.password, tt.config)
if (err != nil) != tt.expectErr {
t.Errorf("ValidatePassword(%s) = %v, expected error = %v", tt.password, err, tt.expectErr)
}
}
}