mirror of
				https://github.com/owncast/owncast.git
				synced 2025-11-01 02:44:31 +08:00 
			
		
		
		
	sha-512 hash passwords longer than 72 bytes (#4331)
* sha-512 hash passwords longer than 72 bytes * rename compress_hashing to go conventions * add api test for long passwords * fix typo * chore(test): add unit test for password hashing --------- Co-authored-by: Gabe Kangas <gabek@real-ity.com>
This commit is contained in:
		| @ -1,5 +1,6 @@ | ||||
| var request = require('supertest'); | ||||
| var bcrypt = require('bcrypt'); | ||||
| var shajs = require("sha.js"); | ||||
|  | ||||
| const sendAdminRequest = require('./lib/admin').sendAdminRequest; | ||||
| const failAdminRequest = require('./lib/admin').failAdminRequest; | ||||
| @ -356,6 +357,35 @@ test('reset admin password', async () => { | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| let newAdminPasswordLong = randomString(80); | ||||
|  | ||||
| test('change admin password >72 bytes', async () => { | ||||
| 	await sendAdminRequest('config/adminpass', newAdminPasswordLong); | ||||
| }); | ||||
|  | ||||
| test('verify admin password change (>72 bytes)', async () => { | ||||
| 	const res = await getAdminResponse( | ||||
| 		'serverconfig', | ||||
| 		(adminPassword = newAdminPasswordLong) | ||||
| 	); | ||||
|  | ||||
| 	bcrypt.compare( | ||||
| 		shajs('SHA512').update(newAdminPasswordLong).digest(), | ||||
| 		res.body.adminPassword, | ||||
| 		function (err, result) { | ||||
| 			expect(result).toBe(true); | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| test('reset admin password (>72)', async () => { | ||||
| 	await sendAdminRequest( | ||||
| 		'config/adminpass', | ||||
| 		defaultAdminPassword, | ||||
| 		(adminPassword = newAdminPasswordLong) | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| test('set override websocket host', async () => { | ||||
| 	await sendAdminRequest('config/sockethostoverride', overriddenWebsocketHost); | ||||
| }); | ||||
|  | ||||
| @ -14,6 +14,7 @@ | ||||
| 		"bcrypt": "^5.1.1", | ||||
| 		"crypto-random": "^2.0.1", | ||||
| 		"jsonfile": "^6.1.0", | ||||
| 		"sha.js": "^2.4.12", | ||||
| 		"supertest": "^6.3.2", | ||||
| 		"websocket": "^1.0.32" | ||||
| 	}, | ||||
|  | ||||
| @ -1,15 +1,28 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"crypto/sha512" | ||||
|  | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
|  | ||||
| func HashPassword(password string) (string, error) { | ||||
| 	password_bytes := compressPassword([]byte(password)) | ||||
| 	// 0 will use the default cost of 10 instead | ||||
| 	hash, err := bcrypt.GenerateFromPassword([]byte(password), 0) | ||||
| 	hash, err := bcrypt.GenerateFromPassword(password_bytes, 0) | ||||
| 	return string(hash), err | ||||
| } | ||||
|  | ||||
| func ComparseHash(hash string, password string) error { | ||||
| 	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) | ||||
| func CompareHash(hash string, password string) error { | ||||
| 	password_bytes := compressPassword([]byte(password)) | ||||
| 	return bcrypt.CompareHashAndPassword([]byte(hash), password_bytes) | ||||
| } | ||||
|  | ||||
| // Takes a password and computes a sha-512 hash of it if it is longer than 72 bytes, guaranteeing it is less than 72 bytes long. | ||||
| func compressPassword(password []byte) []byte { | ||||
| 	if len(password) > 72 { | ||||
| 		sha512_hashed := sha512.Sum512(password) | ||||
| 		password = sha512_hashed[:] | ||||
| 	} | ||||
| 	return password | ||||
| } | ||||
|  | ||||
							
								
								
									
										320
									
								
								utils/hashing_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								utils/hashing_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,320 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
|  | ||||
| func TestHashPassword_ShortPassword(t *testing.T) { | ||||
| 	password := "short" | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("HashPassword failed for short password: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if hash == "" { | ||||
| 		t.Error("HashPassword returned empty hash for short password") | ||||
| 	} | ||||
|  | ||||
| 	if hash == password { | ||||
| 		t.Error("HashPassword returned unhashed password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHashPassword_72BytePassword(t *testing.T) { | ||||
| 	// Test with exactly 72 bytes (bcrypt's limit) | ||||
| 	password := strings.Repeat("a", 72) | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("HashPassword failed for 72-byte password: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if hash == "" { | ||||
| 		t.Error("HashPassword returned empty hash for 72-byte password") | ||||
| 	} | ||||
|  | ||||
| 	if hash == password { | ||||
| 		t.Error("HashPassword returned unhashed password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHashPassword_LongPassword(t *testing.T) { | ||||
| 	// Test with password longer than 72 bytes | ||||
| 	password := strings.Repeat("a", 100) | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("HashPassword failed for long password: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if hash == "" { | ||||
| 		t.Error("HashPassword returned empty hash for long password") | ||||
| 	} | ||||
|  | ||||
| 	if hash == password { | ||||
| 		t.Error("HashPassword returned unhashed password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHashPassword_VeryLongPassword(t *testing.T) { | ||||
| 	// Test with very long password (200 bytes) | ||||
| 	password := strings.Repeat("x", 200) | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("HashPassword failed for very long password: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if hash == "" { | ||||
| 		t.Error("HashPassword returned empty hash for very long password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCompareHash_ShortPassword_Success(t *testing.T) { | ||||
| 	password := "testpassword123" | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("HashPassword failed: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = CompareHash(hash, password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("CompareHash failed to match correct short password: %v", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCompareHash_ShortPassword_Failure(t *testing.T) { | ||||
| 	password := "testpassword123" | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("HashPassword failed: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = CompareHash(hash, "wrongpassword") | ||||
| 	if err == nil { | ||||
| 		t.Error("CompareHash incorrectly matched wrong password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCompareHash_LongPassword_Success(t *testing.T) { | ||||
| 	// Test with password longer than 72 bytes | ||||
| 	password := strings.Repeat("a", 100) | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("HashPassword failed for long password: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = CompareHash(hash, password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("CompareHash failed to match correct long password: %v", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCompareHash_LongPassword_Failure(t *testing.T) { | ||||
| 	// Test with password longer than 72 bytes | ||||
| 	password := strings.Repeat("a", 100) | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("HashPassword failed: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Wrong password also longer than 72 bytes | ||||
| 	wrongPassword := strings.Repeat("b", 100) | ||||
| 	err = CompareHash(hash, wrongPassword) | ||||
| 	if err == nil { | ||||
| 		t.Error("CompareHash incorrectly matched wrong long password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCompareHash_LongPassword_SlightDifference(t *testing.T) { | ||||
| 	// Test that even a slight difference in long passwords is detected | ||||
| 	password := strings.Repeat("a", 100) | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("HashPassword failed: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Same length but one character different | ||||
| 	wrongPassword := strings.Repeat("a", 99) + "b" | ||||
| 	err = CompareHash(hash, wrongPassword) | ||||
| 	if err == nil { | ||||
| 		t.Error("CompareHash incorrectly matched password with slight difference") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCompareHash_73BytePassword(t *testing.T) { | ||||
| 	// Test edge case: 73 bytes (just over bcrypt's limit) | ||||
| 	password := strings.Repeat("a", 73) | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("HashPassword failed for 73-byte password: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = CompareHash(hash, password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("CompareHash failed to match correct 73-byte password: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Ensure wrong password doesn't match | ||||
| 	wrongPassword := strings.Repeat("b", 73) | ||||
| 	err = CompareHash(hash, wrongPassword) | ||||
| 	if err == nil { | ||||
| 		t.Error("CompareHash incorrectly matched wrong 73-byte password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCompressPassword_ShortPassword(t *testing.T) { | ||||
| 	password := []byte("short") | ||||
| 	compressed := compressPassword(password) | ||||
|  | ||||
| 	if len(compressed) != len(password) { | ||||
| 		t.Error("compressPassword should not modify passwords shorter than 72 bytes") | ||||
| 	} | ||||
|  | ||||
| 	if string(compressed) != string(password) { | ||||
| 		t.Error("compressPassword modified short password content") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCompressPassword_72BytePassword(t *testing.T) { | ||||
| 	password := []byte(strings.Repeat("a", 72)) | ||||
| 	compressed := compressPassword(password) | ||||
|  | ||||
| 	if len(compressed) != 72 { | ||||
| 		t.Error("compressPassword should not modify passwords exactly 72 bytes") | ||||
| 	} | ||||
|  | ||||
| 	if string(compressed) != string(password) { | ||||
| 		t.Error("compressPassword modified 72-byte password content") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCompressPassword_LongPassword(t *testing.T) { | ||||
| 	password := []byte(strings.Repeat("a", 100)) | ||||
| 	compressed := compressPassword(password) | ||||
|  | ||||
| 	// SHA-512 produces 64 bytes | ||||
| 	if len(compressed) != 64 { | ||||
| 		t.Errorf("compressPassword should produce 64-byte hash, got %d bytes", len(compressed)) | ||||
| 	} | ||||
|  | ||||
| 	// Ensure it's different from original | ||||
| 	if string(compressed) == string(password) { | ||||
| 		t.Error("compressPassword did not hash long password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCompressPassword_DifferentLongPasswords(t *testing.T) { | ||||
| 	password1 := []byte(strings.Repeat("a", 100)) | ||||
| 	password2 := []byte(strings.Repeat("b", 100)) | ||||
|  | ||||
| 	compressed1 := compressPassword(password1) | ||||
| 	compressed2 := compressPassword(password2) | ||||
|  | ||||
| 	if string(compressed1) == string(compressed2) { | ||||
| 		t.Error("compressPassword produced same hash for different long passwords") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHashPassword_UniqueHashes(t *testing.T) { | ||||
| 	password := "testpassword" | ||||
|  | ||||
| 	// Generate two hashes of the same password | ||||
| 	hash1, err1 := HashPassword(password) | ||||
| 	hash2, err2 := HashPassword(password) | ||||
|  | ||||
| 	if err1 != nil || err2 != nil { | ||||
| 		t.Fatalf("HashPassword failed: %v, %v", err1, err2) | ||||
| 	} | ||||
|  | ||||
| 	// bcrypt includes a salt, so hashes should be different | ||||
| 	if hash1 == hash2 { | ||||
| 		t.Error("HashPassword produced identical hashes (should use salt)") | ||||
| 	} | ||||
|  | ||||
| 	// But both should validate against the original password | ||||
| 	if err := CompareHash(hash1, password); err != nil { | ||||
| 		t.Error("First hash does not validate against password") | ||||
| 	} | ||||
|  | ||||
| 	if err := CompareHash(hash2, password); err != nil { | ||||
| 		t.Error("Second hash does not validate against password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHashPassword_EmptyPassword(t *testing.T) { | ||||
| 	password := "" | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("HashPassword failed for empty password: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = CompareHash(hash, password) | ||||
| 	if err != nil { | ||||
| 		t.Error("CompareHash failed to match empty password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHashPassword_SpecialCharacters(t *testing.T) { | ||||
| 	password := "p@ssw0rd!#$%^&*()" | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("HashPassword failed for password with special characters: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = CompareHash(hash, password) | ||||
| 	if err != nil { | ||||
| 		t.Error("CompareHash failed to match password with special characters") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHashPassword_UnicodeCharacters(t *testing.T) { | ||||
| 	password := "пароль密码🔒" | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("HashPassword failed for password with unicode characters: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = CompareHash(hash, password) | ||||
| 	if err != nil { | ||||
| 		t.Error("CompareHash failed to match password with unicode characters") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHashPassword_LongUnicodePassword(t *testing.T) { | ||||
| 	// Create a long password with unicode characters (over 72 bytes) | ||||
| 	password := strings.Repeat("密码🔒", 30) // Each character is multiple bytes | ||||
|  | ||||
| 	if len([]byte(password)) <= 72 { | ||||
| 		t.Skip("Unicode password not long enough for test") | ||||
| 	} | ||||
|  | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("HashPassword failed for long unicode password: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = CompareHash(hash, password) | ||||
| 	if err != nil { | ||||
| 		t.Error("CompareHash failed to match long unicode password") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHashPassword_BcryptCostIsDefault(t *testing.T) { | ||||
| 	password := "testpassword" | ||||
| 	hash, err := HashPassword(password) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("HashPassword failed: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Extract the cost from the hash | ||||
| 	cost, err := bcrypt.Cost([]byte(hash)) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Failed to extract cost from hash: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// bcrypt.DefaultCost is 10 | ||||
| 	if cost != bcrypt.DefaultCost { | ||||
| 		t.Errorf("Expected cost %d, got %d", bcrypt.DefaultCost, cost) | ||||
| 	} | ||||
| } | ||||
| @ -43,7 +43,7 @@ func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc { | ||||
| 		user, pass, ok := r.BasicAuth() | ||||
|  | ||||
| 		// Failed | ||||
| 		if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || utils.ComparseHash(password, pass) != nil { | ||||
| 		if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || utils.CompareHash(password, pass) != nil { | ||||
| 			w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) | ||||
| 			http.Error(w, "Unauthorized", http.StatusUnauthorized) | ||||
| 			log.Debugln("Failed admin authentication") | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 mahmed2000
					mahmed2000