package crypto import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "fmt" "strings" "unicode/utf8" "golang.org/x/crypto/argon2" ) const ( MinPasswordLength = 12 argonMemory = 64 * 1024 argonIterations = 3 argonParallelism = 2 argonSaltLength = 16 argonKeyLength = 32 ) // ValidatePassword aplica a política mínima das senhas do painel. func ValidatePassword(password string) error { if utf8.RuneCountInString(password) < MinPasswordLength { return fmt.Errorf("a senha deve ter no mínimo %d caracteres", MinPasswordLength) } return nil } // HashPassword gera um hash Argon2id no formato PHC ($argon2id$v=19$m=...,t=...,p=...$salt$hash). func HashPassword(password string) (string, error) { if err := ValidatePassword(password); err != nil { return "", err } salt := make([]byte, argonSaltLength) if _, err := rand.Read(salt); err != nil { return "", err } hash := argon2.IDKey([]byte(password), salt, argonIterations, argonMemory, argonParallelism, argonKeyLength) return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", argonMemory, argonIterations, argonParallelism, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(hash)), nil } // VerifyPassword compara em tempo constante. Parâmetros fora de limites // razoáveis são recusados, para um hash adulterado não travar o servidor. func VerifyPassword(encoded, password string) bool { parts := strings.Split(encoded, "$") if len(parts) != 6 || parts[1] != "argon2id" || parts[2] != "v=19" { return false } var memory, iterations uint32 var parallelism uint8 if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, ¶llelism); err != nil { return false } if memory < 8*1024 || memory > 256*1024 || iterations < 1 || iterations > 10 || parallelism < 1 || parallelism > 8 { return false } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil || len(salt) < 8 { return false } want, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil || len(want) < 16 || len(want) > 64 { return false } got := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, uint32(len(want))) return subtle.ConstantTimeCompare(got, want) == 1 } // DummyVerify gasta o mesmo tempo de uma verificação real; usado quando o // usuário não existe, para o tempo de resposta não revelar isso. func DummyVerify(password string) { salt := sha256.Sum256([]byte("botpontos-dummy-salt")) _ = argon2.IDKey([]byte(password), salt[:argonSaltLength], argonIterations, argonMemory, argonParallelism, argonKeyLength) } // NewToken gera um token aleatório (enviado ao navegador) e seu hash SHA-256 (gravado no banco). func NewToken() (token string, hash []byte, err error) { raw := make([]byte, 32) if _, err := rand.Read(raw); err != nil { return "", nil, err } token = base64.RawURLEncoding.EncodeToString(raw) return token, HashToken(token), nil } func HashToken(token string) []byte { sum := sha256.Sum256([]byte(token)) return sum[:] } // ValidTokenFormat evita consultar o banco com valores claramente inválidos. func ValidTokenFormat(token string) bool { raw, err := base64.RawURLEncoding.DecodeString(token) return err == nil && len(raw) == 32 } // TokenMatches compara um token recebido com o hash gravado, em tempo constante. func TokenMatches(token string, hash []byte) bool { return subtle.ConstantTimeCompare(HashToken(token), hash) == 1 }