Просмотр исходного кода

increase test coverage on password hashing funcs

Mike 11 месяцев назад
Родитель
Сommit
94ed0e9470
3 измененных файлов с 564 добавлено и 15 удалено
  1. 13 15
      state/user.go
  2. 274 0
      state/user_test.go
  3. 277 0
      wire/user_test.go

+ 13 - 15
state/user.go

@@ -393,46 +393,44 @@ func (u *User) Age(timeNow func() time.Time) uint16 {
 	}
 }
 
-// ValidateHash checks if md5Hash is identical to one of the password hashes.
+// ValidateHash validates MD5-hashed passwords for BUCP auth. It handles
+// hashes used in early AIM 4.x versions ("weak" hashes) and later AIM 4.x-5.x
+// versions ("strong" hashes).
 func (u *User) ValidateHash(md5Hash []byte) bool {
 	return bytes.Equal(u.StrongMD5Pass, md5Hash) || bytes.Equal(u.WeakMD5Pass, md5Hash)
 }
 
-// ValidateRoastedPass checks if the provided roasted password matches the MD5
-// hash of the user's actual password. A roasted password is a XOR-obfuscated
-// form of the real password, intended to add a simple layer of security.
+// ValidateRoastedPass validates roasted passwords for FLAP auth.
 func (u *User) ValidateRoastedPass(roastedPass []byte) bool {
 	clearPass := wire.RoastOSCARPassword(roastedPass)
-	md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey) // todo remove string conversion
+	md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey)
 	return bytes.Equal(u.WeakMD5Pass, md5Hash)
 }
 
-// ValidateRoastedJavaPass checks if the provided roasted password matches the MD5
-// hash of the user's actual password. A roasted password is a XOR-obfuscated
-// form of the real password, intended to add a simple layer of security. // todo toc description
+// ValidateRoastedJavaPass validates roasted passwords for the Java AIM client FLAP auth.
 func (u *User) ValidateRoastedJavaPass(roastedPass []byte) bool {
 	clearPass := wire.RoastOSCARJavaPassword(roastedPass)
-	md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey) // todo remove string conversion
+	md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey)
 	return bytes.Equal(u.WeakMD5Pass, md5Hash)
 }
 
-// ValidateRoastedTOCPass checks if the provided roasted password matches the MD5
-// hash of the user's actual password. A roasted password is a XOR-obfuscated
-// form of the real password, intended to add a simple layer of security. // todo toc description
+// ValidateRoastedTOCPass validates roasted passwords for TOC auth.
 func (u *User) ValidateRoastedTOCPass(roastedPass []byte) bool {
 	clearPass := wire.RoastTOCPassword(roastedPass)
-	md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey) // todo remove string conversion
+	md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey)
 	return bytes.Equal(u.WeakMD5Pass, md5Hash)
 }
 
+// ValidatePlaintextPass validates plaintext passwords used in Kerberos auth.
 func (u *User) ValidatePlaintextPass(plaintextPass []byte) bool {
-	md5Hash := wire.WeakMD5PasswordHash(string(plaintextPass), u.AuthKey) // todo remove string conversion
+	md5Hash := wire.WeakMD5PasswordHash(string(plaintextPass), u.AuthKey)
 	return bytes.Equal(u.WeakMD5Pass, md5Hash)
 }
 
+// ValidateRoastedKerberosPass validates roasted passwords used in Kerberos auth.
 func (u *User) ValidateRoastedKerberosPass(roastedPass []byte) bool {
 	clearPass := wire.RoastKerberosPassword(roastedPass)
-	md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey) // todo remove string conversion
+	md5Hash := wire.WeakMD5PasswordHash(string(clearPass), u.AuthKey)
 	return bytes.Equal(u.WeakMD5Pass, md5Hash)
 }
 

+ 274 - 0
state/user_test.go

@@ -262,3 +262,277 @@ func TestDisplayScreenName_ValidateICQHandle(t *testing.T) {
 		})
 	}
 }
+
+func TestUser_ValidateRoastedPass(t *testing.T) {
+	tests := []struct {
+		name        string
+		user        User
+		roastedPass []byte
+		expected    bool
+	}{
+		{
+			name: "Valid roasted password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastOSCARPassword([]byte("testPassword")),
+			expected:    true,
+		},
+		{
+			name: "Invalid roasted password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastOSCARPassword([]byte("wrongPassword")),
+			expected:    false,
+		},
+		{
+			name: "Empty roasted password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastOSCARPassword([]byte("")),
+			expected:    false,
+		},
+		{
+			name: "Empty stored password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: []byte{},
+			},
+			roastedPass: wire.RoastOSCARPassword([]byte("testPassword")),
+			expected:    false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := tt.user.ValidateRoastedPass(tt.roastedPass)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestUser_ValidateRoastedJavaPass(t *testing.T) {
+	tests := []struct {
+		name        string
+		user        User
+		roastedPass []byte
+		expected    bool
+	}{
+		{
+			name: "Valid roasted Java password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastOSCARJavaPassword([]byte("testPassword")),
+			expected:    true,
+		},
+		{
+			name: "Invalid roasted Java password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastOSCARJavaPassword([]byte("wrongPassword")),
+			expected:    false,
+		},
+		{
+			name: "Empty roasted Java password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastOSCARJavaPassword([]byte("")),
+			expected:    false,
+		},
+		{
+			name: "Empty stored password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: []byte{},
+			},
+			roastedPass: wire.RoastOSCARJavaPassword([]byte("testPassword")),
+			expected:    false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := tt.user.ValidateRoastedJavaPass(tt.roastedPass)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestUser_ValidateRoastedTOCPass(t *testing.T) {
+	tests := []struct {
+		name        string
+		user        User
+		roastedPass []byte
+		expected    bool
+	}{
+		{
+			name: "Valid roasted TOC password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastTOCPassword([]byte("testPassword")),
+			expected:    true,
+		},
+		{
+			name: "Invalid roasted TOC password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastTOCPassword([]byte("wrongPassword")),
+			expected:    false,
+		},
+		{
+			name: "Empty roasted TOC password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastTOCPassword([]byte("")),
+			expected:    false,
+		},
+		{
+			name: "Empty stored password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: []byte{},
+			},
+			roastedPass: wire.RoastTOCPassword([]byte("testPassword")),
+			expected:    false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := tt.user.ValidateRoastedTOCPass(tt.roastedPass)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestUser_ValidatePlaintextPass(t *testing.T) {
+	tests := []struct {
+		name          string
+		user          User
+		plaintextPass []byte
+		expected      bool
+	}{
+		{
+			name: "Valid plaintext password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			plaintextPass: []byte("testPassword"),
+			expected:      true,
+		},
+		{
+			name: "Invalid plaintext password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			plaintextPass: []byte("wrongPassword"),
+			expected:      false,
+		},
+		{
+			name: "Empty plaintext password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			plaintextPass: []byte(""),
+			expected:      false,
+		},
+		{
+			name: "Empty stored password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: []byte{},
+			},
+			plaintextPass: []byte("testPassword"),
+			expected:      false,
+		},
+		{
+			name: "Password with special characters",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("test@123!", "testAuthKey"),
+			},
+			plaintextPass: []byte("test@123!"),
+			expected:      true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := tt.user.ValidatePlaintextPass(tt.plaintextPass)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestUser_ValidateRoastedKerberosPass(t *testing.T) {
+	tests := []struct {
+		name        string
+		user        User
+		roastedPass []byte
+		expected    bool
+	}{
+		{
+			name: "Valid roasted Kerberos password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastKerberosPassword([]byte("testPassword")),
+			expected:    true,
+		},
+		{
+			name: "Invalid roasted Kerberos password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastKerberosPassword([]byte("wrongPassword")),
+			expected:    false,
+		},
+		{
+			name: "Empty roasted Kerberos password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: wire.WeakMD5PasswordHash("testPassword", "testAuthKey"),
+			},
+			roastedPass: wire.RoastKerberosPassword([]byte("")),
+			expected:    false,
+		},
+		{
+			name: "Empty stored password",
+			user: User{
+				AuthKey:     "testAuthKey",
+				WeakMD5Pass: []byte{},
+			},
+			roastedPass: wire.RoastKerberosPassword([]byte("testPassword")),
+			expected:    false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := tt.user.ValidateRoastedKerberosPass(tt.roastedPass)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}

+ 277 - 0
wire/user_test.go

@@ -0,0 +1,277 @@
+package wire
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestWeakMD5PasswordHash(t *testing.T) {
+	tests := []struct {
+		name     string
+		password string
+		authKey  string
+		want     []byte
+	}{
+		{
+			name:     "empty password and auth key",
+			password: "",
+			authKey:  "",
+			want:     []byte{0x13, 0xfd, 0x0b, 0x9e, 0x89, 0xf4, 0xb8, 0x36, 0xa7, 0x65, 0x8b, 0x9d, 0xca, 0xad, 0x2a, 0xd4},
+		},
+		{
+			name:     "password and auth key",
+			password: "password123",
+			authKey:  "authkey456",
+			want:     []byte{0x04, 0x79, 0x63, 0x82, 0x0d, 0xa7, 0xbb, 0xfe, 0x6a, 0x9b, 0x41, 0xa4, 0x5c, 0x47, 0xcb, 0xcb},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := WeakMD5PasswordHash(tt.password, tt.authKey)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestStrongMD5PasswordHash(t *testing.T) {
+	tests := []struct {
+		name     string
+		password string
+		authKey  string
+		want     []byte
+	}{
+		{
+			name:     "empty password and auth key",
+			password: "",
+			authKey:  "",
+			want:     []byte{0x1f, 0xa2, 0xb6, 0x99, 0x59, 0x84, 0xb0, 0x14, 0x68, 0xa3, 0x7c, 0x77, 0x42, 0x90, 0x0a, 0xc9},
+		},
+		{
+			name:     "password and auth key",
+			password: "password123",
+			authKey:  "authkey456",
+			want:     []byte{0xb9, 0x07, 0x91, 0xcc, 0xcb, 0x5c, 0x57, 0x71, 0xbd, 0xcb, 0xc9, 0x39, 0x82, 0xf7, 0x94, 0x84},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := StrongMD5PasswordHash(tt.password, tt.authKey)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestRoastOSCARPassword(t *testing.T) {
+	tests := []struct {
+		name        string
+		roastedPass []byte
+		want        []byte
+	}{
+		{
+			name:        "empty password",
+			roastedPass: []byte{},
+			want:        []byte{},
+		},
+		{
+			name:        "single byte password",
+			roastedPass: []byte{0xF3},
+			want:        []byte{0x00},
+		},
+		{
+			name:        "multiple bytes password",
+			roastedPass: []byte{0xF3, 0x26, 0x81, 0xC4},
+			want:        []byte{0x00, 0x00, 0x00, 0x00},
+		},
+		{
+			name:        "password longer than roast table",
+			roastedPass: []byte{0xF3, 0x26, 0x81, 0xC4, 0x39, 0x86, 0xDB, 0x92, 0x71, 0xA3, 0xB9, 0xE6, 0x53, 0x7A, 0x95, 0x7C, 0xF3, 0x26},
+			want:        []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
+		},
+		{
+			name:        "non-zero roasted password",
+			roastedPass: []byte{0xE3, 0x16, 0x91, 0xD4},
+			want:        []byte{0x10, 0x30, 0x10, 0x10},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := RoastOSCARPassword(tt.roastedPass)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestRoastKerberosPassword(t *testing.T) {
+	tests := []struct {
+		name        string
+		roastedPass []byte
+		want        []byte
+	}{
+		{
+			name:        "empty password",
+			roastedPass: []byte{},
+			want:        []byte{},
+		},
+		{
+			name:        "single byte password",
+			roastedPass: []byte{0x76},
+			want:        []byte{0x00},
+		},
+		{
+			name:        "multiple bytes password",
+			roastedPass: []byte{0x76, 0x91, 0xc5, 0xe7},
+			want:        []byte{0x00, 0x00, 0x00, 0x00},
+		},
+		{
+			name:        "password longer than roast table",
+			roastedPass: []byte{0x76, 0x91, 0xc5, 0xe7, 0xd0, 0xd9, 0x95, 0xdd, 0x9e, 0x2F, 0xea, 0xd8, 0x6B, 0x21, 0xc2, 0xbc, 0x76, 0x91},
+			want:        []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
+		},
+		{
+			name:        "non-zero roasted password",
+			roastedPass: []byte{0x66, 0x81, 0xd5, 0xf7},
+			want:        []byte{0x10, 0x10, 0x10, 0x10},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := RoastKerberosPassword(tt.roastedPass)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestRoastOSCARJavaPassword(t *testing.T) {
+	tests := []struct {
+		name        string
+		roastedPass []byte
+		want        []byte
+	}{
+		{
+			name:        "empty password",
+			roastedPass: []byte{},
+			want:        []byte{},
+		},
+		{
+			name:        "single byte password",
+			roastedPass: []byte{0xF3},
+			want:        []byte{0x00},
+		},
+		{
+			name:        "multiple bytes password",
+			roastedPass: []byte{0xF3, 0xB3, 0x6C, 0x99},
+			want:        []byte{0x00, 0x00, 0x00, 0x00},
+		},
+		{
+			name:        "password longer than roast table",
+			roastedPass: []byte{0xF3, 0xB3, 0x6C, 0x99, 0x95, 0x3F, 0xAC, 0xB6, 0xC5, 0xFA, 0x6B, 0x63, 0x69, 0x6C, 0xC3, 0x9A, 0xF3, 0xB3},
+			want:        []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
+		},
+		{
+			name:        "non-zero roasted password",
+			roastedPass: []byte{0xE3, 0xA3, 0x5C, 0x89},
+			want:        []byte{0x10, 0x10, 0x30, 0x10},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := RoastOSCARJavaPassword(tt.roastedPass)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestRoastTOCPassword(t *testing.T) {
+	tests := []struct {
+		name        string
+		roastedPass []byte
+		want        []byte
+	}{
+		{
+			name:        "empty password",
+			roastedPass: []byte{},
+			want:        []byte{},
+		},
+		{
+			name:        "single byte password",
+			roastedPass: []byte{0x54}, // 'T'
+			want:        []byte{0x00},
+		},
+		{
+			name:        "multiple bytes password",
+			roastedPass: []byte{0x54, 0x69, 0x63, 0x2F, 0x54, 0x6F, 0x63}, // "Tic/Toc"
+			want:        []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
+		},
+		{
+			name:        "password longer than roast table",
+			roastedPass: []byte{0x54, 0x69, 0x63, 0x2F, 0x54, 0x6F, 0x63, 0x54, 0x69, 0x63, 0x2F, 0x54, 0x6F, 0x63},
+			want:        []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
+		},
+		{
+			name:        "non-zero roasted password",
+			roastedPass: []byte{0x44, 0x59, 0x53, 0x1F},
+			want:        []byte{0x10, 0x30, 0x30, 0x30},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := RoastTOCPassword(tt.roastedPass)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+// Test that roasting and unroasting are reversible operations
+func TestRoastPasswordReversibility(t *testing.T) {
+	tests := []struct {
+		name       string
+		clearPass  []byte
+		roastTable []byte
+		roastFunc  func([]byte) []byte
+	}{
+		{
+			name:       "OSCAR password reversibility",
+			clearPass:  []byte("password123"),
+			roastTable: []byte{0xF3, 0x26, 0x81, 0xC4, 0x39, 0x86, 0xDB, 0x92, 0x71, 0xA3, 0xB9, 0xE6, 0x53, 0x7A, 0x95, 0x7C},
+			roastFunc:  RoastOSCARPassword,
+		},
+		{
+			name:       "Kerberos password reversibility",
+			clearPass:  []byte("password123"),
+			roastTable: []byte{0x76, 0x91, 0xc5, 0xe7, 0xd0, 0xd9, 0x95, 0xdd, 0x9e, 0x2F, 0xea, 0xd8, 0x6B, 0x21, 0xc2, 0xbc},
+			roastFunc:  RoastKerberosPassword,
+		},
+		{
+			name:       "OSCAR Java password reversibility",
+			clearPass:  []byte("password123"),
+			roastTable: []byte{0xF3, 0xB3, 0x6C, 0x99, 0x95, 0x3F, 0xAC, 0xB6, 0xC5, 0xFA, 0x6B, 0x63, 0x69, 0x6C, 0xC3, 0x9A},
+			roastFunc:  RoastOSCARJavaPassword,
+		},
+		{
+			name:       "TOC password reversibility",
+			clearPass:  []byte("password123"),
+			roastTable: []byte("Tic/Toc"),
+			roastFunc:  RoastTOCPassword,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// First, roast the clear password
+			roasted := roastPass(tt.clearPass, tt.roastTable)
+
+			// Then, unroast it using the roast function
+			unroasted := tt.roastFunc(roasted)
+
+			// The result should be the original clear password
+			assert.Equal(t, tt.clearPass, unroasted)
+		})
+	}
+}