4
0
Эх сурвалжийг харах

support md5 password hash for AIM 3.5-4.7

There are two kinds of password hashing for BUCP auth.

The first type is what we'll call the the "weak" password hash.
It's new to this code base and unlocks support for AIM 3.5-4.7

  md5(auth key + password + "AOL Instant Messenger (SM)")

The second type is what we'll call the "strong" password hash, and
already existed before this commit:

  md5(auth key + md5(password) + "AOL Instant Messenger (SM)")

Now when a new account is created, both types of hashes are generated
for each password. At login time, the auth service checks both hashes
and succeeds if the hash provided by the client matches one of these
hashes.

Unfortunately, this requires server operators to reset the passwords for
all users in order to generate the weak password hash, as it's not
possible to derive the weak password hash from the existing strong
password hash.
Mike 2 жил өмнө
parent
commit
664e09f8a3

+ 12 - 3
foodgroup/auth.go

@@ -162,13 +162,22 @@ func (s AuthService) BUCPLoginRequest(bodyIn wire.SNAC_0x17_0x02_BUCPLoginReques
 
 	u, err := s.userManager.User(screenName)
 	switch {
+	// runtime error
 	case err != nil:
 		return wire.SNACMessage{}, err
-	case u != nil && bytes.Equal(u.PassHash, md5Hash):
-		// password check succeeded
+	// user exists, check password hashes.
+	// check both strong password hash (for AIM 4.8+) and weak password hash
+	// (for AIM < 4.8).
+	// in the future, this could check the appropriate hash based on the client
+	// version indicated in the request metadata, but more testing needs to be
+	// done first to make sure versioning metadata is consistent across all AIM
+	// clients, including 3rd-party implementations, lest we create edge cases
+	// that break login for some clients.
+	case u != nil && (bytes.Equal(u.StrongMD5Pass, md5Hash) || bytes.Equal(u.WeakMD5Pass, md5Hash)):
 		loginOK = true
+	// authentication check is disabled, allow unconditional login. create new
+	// user if the account doesn't already exist.
 	case s.config.DisableAuth:
-		// login failed but let them in anyway
 		user, err := newUserFn(screenName)
 		if err != nil {
 			return wire.SNACMessage{}, err

+ 3 - 3
foodgroup/auth_test.go

@@ -49,7 +49,7 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 			inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.TLVPasswordHash, user.PassHash),
+						wire.NewTLV(wire.TLVPasswordHash, user.StrongMD5Pass),
 						wire.NewTLV(wire.TLVScreenName, user.ScreenName),
 					},
 				},
@@ -99,7 +99,7 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 			inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.TLVPasswordHash, user.PassHash),
+						wire.NewTLV(wire.TLVPasswordHash, user.StrongMD5Pass),
 						wire.NewTLV(wire.TLVScreenName, user.ScreenName),
 					},
 				},
@@ -311,7 +311,7 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 			inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
-						wire.NewTLV(wire.TLVPasswordHash, user.PassHash),
+						wire.NewTLV(wire.TLVPasswordHash, user.StrongMD5Pass),
 						wire.NewTLV(wire.TLVScreenName, user.ScreenName),
 					},
 				},

+ 15 - 0
state/migrations/0002_hash_columns.down.sql

@@ -0,0 +1,15 @@
+CREATE TEMPORARY TABLE user_backup
+(
+    screenName VARCHAR(16) PRIMARY KEY,
+    authKey    TEXT,
+    passHash   TEXT
+);
+
+INSERT INTO user_backup (screenName, authKey, passHash)
+SELECT screenName, authKey, strongMD5Pass
+FROM user;
+
+DROP TABLE user;
+
+ALTER TABLE user_backup
+    RENAME TO user;

+ 12 - 0
state/migrations/0002_hash_columns.up.sql

@@ -0,0 +1,12 @@
+ALTER TABLE user
+    RENAME COLUMN passHash TO strongMD5Pass;
+
+ALTER TABLE user
+    ADD COLUMN weakMD5Pass TEXT;
+
+-- The cleartext passwords don't exist, so it's not possible to create weak MD5
+-- hashes. Fill in the values with a placeholder value. The administrator will
+-- require everyone to reset their passwords if they want to log in with AIM
+-- 3.5 thru AIM4.7
+UPDATE user
+SET weakMD5Pass = strongMD5Pass;

+ 34 - 10
state/user_store.go

@@ -38,14 +38,37 @@ const (
 
 // User represents an instant messaging user.
 type User struct {
-	ScreenName string `json:"screen_name"`
-	AuthKey    string `json:"-"`
-	PassHash   []byte `json:"-"`
+	ScreenName    string `json:"screen_name"`
+	AuthKey       string `json:"-"`
+	StrongMD5Pass []byte `json:"-"`
+	WeakMD5Pass   []byte `json:"-"`
 }
 
 // HashPassword creates a password hash using the MD5 digest algorithm. The
-// hash is stored in the User.PassHash field.
+// hash is stored in the User.StrongMD5Pass field.
 func (u *User) HashPassword(passwd string) error {
+	if err := u.weakPassword(passwd); err != nil {
+		return err
+	}
+	return u.strongPassword(passwd)
+}
+
+func (u *User) weakPassword(passwd string) error {
+	hash := md5.New()
+	if _, err := io.WriteString(hash, u.AuthKey); err != nil {
+		return err
+	}
+	if _, err := io.WriteString(hash, passwd); err != nil {
+		return err
+	}
+	if _, err := io.WriteString(hash, "AOL Instant Messenger (SM)"); err != nil {
+		return err
+	}
+	u.WeakMD5Pass = hash.Sum(nil)
+	return nil
+}
+
+func (u *User) strongPassword(passwd string) error {
 	top := md5.New()
 	if _, err := io.WriteString(top, passwd); err != nil {
 		return err
@@ -60,7 +83,7 @@ func (u *User) HashPassword(passwd string) error {
 	if _, err := io.WriteString(bottom, "AOL Instant Messenger (SM)"); err != nil {
 		return err
 	}
-	u.PassHash = bottom.Sum(nil)
+	u.StrongMD5Pass = bottom.Sum(nil)
 	return nil
 }
 
@@ -143,12 +166,13 @@ func (f SQLiteUserStore) User(screenName string) (*User, error) {
 		SELECT
 			screenName, 
 			authKey, 
-			passHash
+			weakMD5Pass,
+			strongMD5Pass
 		FROM user
 		WHERE screenName = ?
 	`
 	u := &User{}
-	err := f.db.QueryRow(q, screenName).Scan(&u.ScreenName, &u.AuthKey, &u.PassHash)
+	err := f.db.QueryRow(q, screenName).Scan(&u.ScreenName, &u.AuthKey, &u.WeakMD5Pass, &u.StrongMD5Pass)
 	if errors.Is(err, sql.ErrNoRows) {
 		return nil, nil
 	}
@@ -159,11 +183,11 @@ func (f SQLiteUserStore) User(screenName string) (*User, error) {
 // the user already exists.
 func (f SQLiteUserStore) InsertUser(u User) error {
 	q := `
-		INSERT INTO user (screenName, authKey, passHash)
-		VALUES (?, ?, ?)
+		INSERT INTO user (screenName, authKey, weakMD5Pass, strongMD5Pass)
+		VALUES (?, ?, ?, ?)
 		ON CONFLICT DO NOTHING
 	`
-	_, err := f.db.Exec(q, u.ScreenName, u.AuthKey, u.PassHash)
+	_, err := f.db.Exec(q, u.ScreenName, u.AuthKey, u.WeakMD5Pass, u.StrongMD5Pass)
 	return err
 }
 

+ 5 - 5
state/user_store_test.go

@@ -354,12 +354,12 @@ func TestGetUser(t *testing.T) {
 	assert.NoError(t, err)
 
 	expectUser := &User{
-		ScreenName: "testscreenname",
-		AuthKey:    "theauthkey",
-		PassHash:   []byte("thepasshash"),
+		ScreenName:    "testscreenname",
+		AuthKey:       "theauthkey",
+		StrongMD5Pass: []byte("thepasshash"),
 	}
-	_, err = f.db.Exec(`INSERT INTO user (ScreenName, authKey, passHash) VALUES(?, ?, ?)`,
-		expectUser.ScreenName, expectUser.AuthKey, expectUser.PassHash)
+	_, err = f.db.Exec(`INSERT INTO user (ScreenName, authKey, strongMD5Pass) VALUES(?, ?, ?)`,
+		expectUser.ScreenName, expectUser.AuthKey, expectUser.StrongMD5Pass)
 	if err != nil {
 		t.Fatalf("failed to insert user: %s", err.Error())
 	}