Files

245 lines
7.0 KiB
Go
Raw Permalink Normal View History

package crypto
import (
"encoding/hex"
"errors"
"strings"
"testing"
)
func testHasher(t *testing.T) *PasswordHasher {
t.Helper()
// Параметры снижены: тесты не должны тратить секунды на каждый хэш.
hasher, err := NewPasswordHasher([]byte("unit-test-pepper"), Argon2Params{
Memory: 1024, Iterations: 1, Parallelism: 1, SaltLength: 16, KeyLength: 32,
})
if err != nil {
t.Fatalf("NewPasswordHasher: %v", err)
}
return hasher
}
func TestPasswordHashAndVerify(t *testing.T) {
hasher := testHasher(t)
hash, err := hasher.Hash("correct horse battery staple")
if err != nil {
t.Fatalf("Hash: %v", err)
}
if !strings.HasPrefix(hash, "argon2id$") {
t.Fatalf("unexpected hash format: %s", hash)
}
if err := hasher.Verify("correct horse battery staple", hash); err != nil {
t.Fatalf("Verify: %v", err)
}
if err := hasher.Verify("wrong password", hash); !errors.Is(err, ErrPasswordMismatch) {
t.Fatalf("Verify with wrong password = %v, want ErrPasswordMismatch", err)
}
}
func TestPasswordHashIsSalted(t *testing.T) {
hasher := testHasher(t)
first, err := hasher.Hash("same-password-value")
if err != nil {
t.Fatalf("Hash: %v", err)
}
second, err := hasher.Hash("same-password-value")
if err != nil {
t.Fatalf("Hash: %v", err)
}
if first == second {
t.Fatal("hashes of the same password must differ (unique salt)")
}
}
func TestPepperIsRequired(t *testing.T) {
if _, err := NewPasswordHasher(nil, Argon2Params{}); !errors.Is(err, ErrEmptyPepper) {
t.Fatalf("NewPasswordHasher without pepper = %v, want ErrEmptyPepper", err)
}
}
func TestPepperChangesHash(t *testing.T) {
params := Argon2Params{Memory: 1024, Iterations: 1, Parallelism: 1, SaltLength: 16, KeyLength: 32}
first, err := NewPasswordHasher([]byte("pepper-one"), params)
if err != nil {
t.Fatalf("NewPasswordHasher: %v", err)
}
second, err := NewPasswordHasher([]byte("pepper-two"), params)
if err != nil {
t.Fatalf("NewPasswordHasher: %v", err)
}
hash, err := first.Hash("password-value-1")
if err != nil {
t.Fatalf("Hash: %v", err)
}
if err := second.Verify("password-value-1", hash); !errors.Is(err, ErrPasswordMismatch) {
t.Fatal("hash must not verify with a different pepper")
}
}
func TestNeedsRehash(t *testing.T) {
weak := Argon2Params{Memory: 1024, Iterations: 1, Parallelism: 1, SaltLength: 16, KeyLength: 32}
hasher, err := NewPasswordHasher([]byte("pepper"), weak)
if err != nil {
t.Fatalf("NewPasswordHasher: %v", err)
}
hash, err := hasher.Hash("password-value-1")
if err != nil {
t.Fatalf("Hash: %v", err)
}
if hasher.NeedsRehash(hash) {
t.Fatal("hash should match the hasher parameters")
}
strong := weak
strong.Memory = 2048
upgraded, err := NewPasswordHasher([]byte("pepper"), strong)
if err != nil {
t.Fatalf("NewPasswordHasher: %v", err)
}
if !upgraded.NeedsRehash(hash) {
t.Fatal("hash with weaker parameters must need a rehash")
}
if !hasher.NeedsRehash("garbage") {
t.Fatal("unparsable hash must need a rehash")
}
}
func TestMasterKeyEncryptDecrypt(t *testing.T) {
key, err := ParseMasterKey(strings.Repeat("ab", 32))
if err != nil {
t.Fatalf("ParseMasterKey: %v", err)
}
sealed, err := key.Encrypt("user@example.com")
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
if !strings.HasPrefix(sealed, "v1:") {
t.Fatalf("ciphertext must be versioned, got %s", sealed)
}
opened, err := key.Decrypt(sealed)
if err != nil {
t.Fatalf("Decrypt: %v", err)
}
if opened != "user@example.com" {
t.Fatalf("Decrypt = %q", opened)
}
}
func TestMasterKeyRejectsForeignCiphertext(t *testing.T) {
first, _ := ParseMasterKey(strings.Repeat("ab", 32))
second, _ := ParseMasterKey(strings.Repeat("cd", 32))
sealed, err := first.Encrypt("secret-value")
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
if _, err := second.Decrypt(sealed); err == nil {
t.Fatal("another key must not decrypt the value")
}
if _, err := first.Decrypt("plain-value"); !errors.Is(err, ErrInvalidCiphertext) {
t.Fatalf("Decrypt(plain) = %v, want ErrInvalidCiphertext", err)
}
if _, err := first.Decrypt("v2:AAAA"); !errors.Is(err, ErrInvalidCiphertext) {
t.Fatal("unknown key version must be rejected")
}
}
func TestMasterKeyAcceptsHexBase64AndRaw(t *testing.T) {
raw := make([]byte, 32)
for i := range raw {
raw[i] = byte(i)
}
hexForm := hex.EncodeToString(raw)
for name, value := range map[string]string{
"hex": hexForm,
"raw": string(raw),
} {
key, err := ParseMasterKey(value)
if err != nil {
t.Fatalf("%s: ParseMasterKey: %v", name, err)
}
sealed, err := key.Encrypt("value")
if err != nil {
t.Fatalf("%s: Encrypt: %v", name, err)
}
if _, err := key.Decrypt(sealed); err != nil {
t.Fatalf("%s: Decrypt: %v", name, err)
}
}
if _, err := ParseMasterKey("too-short"); !errors.Is(err, ErrInvalidMasterKey) {
t.Fatalf("ParseMasterKey(short) = %v, want ErrInvalidMasterKey", err)
}
}
func TestBlindIndexIsStableAndUnique(t *testing.T) {
key, _ := ParseMasterKey(strings.Repeat("11", 32))
other, _ := ParseMasterKey(strings.Repeat("22", 32))
first := key.BlindIndex(NormalizeEmail(" User@Example.COM "))
same := key.BlindIndex(NormalizeEmail("user@example.com"))
another := key.BlindIndex(NormalizeEmail("second@example.com"))
if first != same {
t.Fatal("blind index must be stable for the same normalized value")
}
if first == another {
t.Fatal("blind index must differ for different values")
}
if first == other.BlindIndex(NormalizeEmail("user@example.com")) {
t.Fatal("blind index must depend on the key")
}
if len(first) != 64 {
t.Fatalf("blind index length = %d, want 64 hex chars", len(first))
}
}
func TestSessionTokenHashing(t *testing.T) {
token, hash, err := NewSessionToken()
if err != nil {
t.Fatalf("NewSessionToken: %v", err)
}
if token == "" || hash == "" {
t.Fatal("token and hash must not be empty")
}
if token == hash {
t.Fatal("token must not be stored as is")
}
if HashToken(token) != hash {
t.Fatal("HashToken must reproduce the stored hash")
}
other, _, err := NewSessionToken()
if err != nil {
t.Fatalf("NewSessionToken: %v", err)
}
if other == token {
t.Fatal("tokens must be unique")
}
}
func TestRecoveryCodeFormat(t *testing.T) {
code, err := NewRecoveryCode()
if err != nil {
t.Fatalf("NewRecoveryCode: %v", err)
}
if len(code) != 14 || strings.Count(code, "-") != 2 {
t.Fatalf("recovery code %q has unexpected format", code)
}
}
func TestValidatePasswordAndUsername(t *testing.T) {
if err := ValidatePassword("short"); err == nil {
t.Fatal("short password must be rejected")
}
if err := ValidatePassword("long-enough-password"); err != nil {
t.Fatalf("valid password rejected: %v", err)
}
for _, username := range []string{"ab", "user.name", "User_123"} {
if err := ValidateUsername(username); err != nil {
t.Fatalf("valid username %q rejected: %v", username, err)
}
}
for _, username := range []string{"a", "with space", "кириллица", strings.Repeat("x", 33)} {
if err := ValidateUsername(username); err == nil {
t.Fatalf("invalid username %q accepted", username)
}
}
}