Argon2를 이용한 비밀번호 암호화 및 검증

암호화와 검증의 중요성

암호화는 사용자의 개인정보 보호에 핵심적인 역할을 합니다. 특히 비밀번호와 같은 민감한 정보는 안전하게 저장해야 하며, Argon2 알고리즘은 이러한 정보를 안전하게 암호화하는 데 현재 널리 사용되는 방법 중 하나입니다.

Argon2 알고리즘 소개

Argon2는 메모리와 시간 비용을 조절할 수 있는 강력한 암호화 알고리즘입니다. 이는 고도의 커스터마이징이 가능하며, 병렬 처리, 메모리 양, 해시 길이, 솔트 길이를 조절할 수 있습니다.

Argon2 사용 방법

비밀번호 해시 생성
// 비밀번호 해시 생성 
public static string HashPassword(string password) 
{     
	using (var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)))     
	{         
		argon2.Salt = CreateSalt();         
		// ... 나머지 설정         
		return Convert.ToBase64String(argon2.GetBytes(128));     
	} 
}
솔트 생성
// 솔트 생성 
private static byte[] CreateSalt() 
{     
	var buffer = new byte[16];     
	using (var rng = new RNGCryptoServiceProvider())     
	{         
		rng.GetBytes(buffer);
	 }   
	return buffer; 
}  
비밀번호 검증
// 비밀번호 검증 
public static bool VerifyPassword(string inputPassword, string storedHash) 
{     
	// ... 추출과 검증 로직 
}

비밀번호 해시 생성 과정

  1. 솔트 생성: 각 비밀번호에 대해 유일한 솔트를 생성합니다. 이는 같은 비밀번호라도 다른 해시 값을 생성하게 해줍니다.
  2. Argon2 인스턴스 구성: 메모리 크기, 반복 횟수, 병렬 처리 수준을 설정합니다.
  3. 비밀번호 해시: 비밀번호와 솔트를 결합하여 해시를 생성합니다.
  4. 저장: 해시와 솔트를 데이터베이스에 안전하게 저장합니다.

비밀번호 검증 과정

  1. 솔트 추출: 데이터베이스에서 사용자의 해시를 가져와서 솔트를 추출합니다.
  2. 새 해시 생성: 입력된 비밀번호와 솔트를 사용하여 새로운 해시를 생성합니다.
  3. 비교: 새로 생성된 해시와 데이터베이스에 저장된 해시를 비교합니다.

실수했던 코드

public static bool VerifyPassword(string inputPassword, string storedHash) 
{ 
	byte[] hashBytes = Convert.FromBase64String(storedHash); 
	 // Salt는 Hash에서 추출하거나 별도로 저장해야 합니다. 
	 // 예를 들어, 첫 16바이트가 Salt라고 가정합니다. 
	 byte[] salt = hashBytes.Take(16).ToArray(); 
	 // Hash 부분을 추출합니다. 
	 byte[] storedHashBytes = hashBytes.Skip(16).ToArray(); 
	 using (var argon2 = new Argon2id(Encoding.UTF8.GetBytes(inputPassword))) 
	 { 
		 argon2.Salt = salt; 
		 argon2.DegreeOfParallelism = 8; 
		 argon2.MemorySize = 65536; 
		 argon2.Iterations = 4; 
		 // 입력된 비밀번호로부터 생성된 해시 
		 byte[] newHashBytes = argon2.GetBytes(storedHashBytes.Length); 
		 // 저장된 해시와 입력된 비밀번호의 해시를 비교 
		 return newHashBytes.SequenceEqual(storedHashBytes); 
	 } 
 }

private static string HashPassword(string password) 
{ 
	// Argon2id 인스턴스 생성 
	using (var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password))) 
	{ 
		argon2.Salt = CreateSalt(); 
		argon2.DegreeOfParallelism = 8; 
		// 병렬 처리에 사용할 스레드의 수 
		argon2.MemorySize = 65536; 
		// 사용할 메모리 양(KB) 
		argon2.Iterations = 4; 
		// 해시 반복 횟수 
		argon2.AssociatedData = null; 
		argon2.KnownSecret = null; 
		// 해시 계산 
		return Convert.ToBase64String(argon2.GetBytes(128)); 
		// 해시된 비밀번호 반환 
	} 
} 

private static byte[] CreateSalt() 
{ 
	var buffer = new byte[16]; 
	using (var rng = new RNGCryptoServiceProvider()) 
	{ 
		rng.GetBytes(buffer); 
	} 
	return buffer; 
}

VerifyPassword에서 Hash 검증 부분이 제대로 작동하지 않아
비밀번호에 대한 인증이 실패하는 상황

확인 해야 하는 부분
  1. 저장된 Hash (storedHash)가 실제로 Base64 인코딩 된 값인지
  2. 저장된 hash에서 salt를 정확히 추출하고 있는지
  3. GetBytes 메소드로 생성하는 해시의 길이가 저장된 해시와 동일한지 확인
  • 문제가 되는 부분
private static string HashPassword(string password) 
{ 
		// 해시 반복 횟수 
		argon2.AssociatedData = null; 
		argon2.KnownSecret = null; 
		// 해시 계산 
		return Convert.ToBase64String(argon2.GetBytes(128)); 
		// 해시된 비밀번호 반환 
	} 
} 

변경 사항

해시 생성 후 Base64로 인코딩 할 수 있도록 수정

수정된 코드

private static string HashPassword(string password)
{
	// 해시 생성
	byte[] hashBytes = argon2.GetBytes(128);

	// 해시 앞에 솔트를 붙여서 전체를 Base64 인코딩으로 변환
	byte[] hashWithSaltBytes = new byte[salt.Length + hashBytes.Length];
	Array.Copy(salt, 0, hashWithSaltBytes, 0, salt.Length);
	Array.Copy(hashBytes, 0, hashWithSaltBytes, salt.Length, hashBytes.Length);

	return Convert.ToBase64String(hashWithSaltBytes);
}
private static string HashPassword(string password)
{
    using (var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)))
    {
        byte[] salt = CreateSalt();
        argon2.Salt = salt;
        argon2.DegreeOfParallelism = 8; 
        argon2.MemorySize = 65536;
        argon2.Iterations = 4;

        // 해시 생성
        byte[] hashBytes = argon2.GetBytes(128);

        // 해시 앞에 솔트를 붙여서 전체를 Base64 인코딩으로 변환
        byte[] hashWithSaltBytes = new byte[salt.Length + hashBytes.Length];
        Array.Copy(salt, 0, hashWithSaltBytes, 0, salt.Length);
        Array.Copy(hashBytes, 0, hashWithSaltBytes, salt.Length, hashBytes.Length);

        return Convert.ToBase64String(hashWithSaltBytes);
    }
}

변경된 방식의 설명

첫 번째 메소드

첫 번째 코드에서는 솔트가 생성되어 Argon2 인스턴스에 할당되지만, 메소드가 해시만 반환하고 솔트는 결과값에 어떤 방식으로도 포함되지 않습니다.

argon2.Salt = CreateSalt();
// ...
// 이 라인은 해시만 가져오고, 반환값에 솔트는 포함되지 않습니다.
return Convert.ToBase64String(argon2.GetBytes(128)); // 해시된 비밀번호 반환

원본 해시를 생성할 때 사용된 정확한 솔트가 필요
비밀번호 검증시 이 salt 가 Hash와 함께 저장되지 않는다면, 사용자의 비밀번호를 검증하기 위해
동일한 Hash를 재생산할 방법이 없어져 인증목적으로 쓸모가 없게 됨

두 번째 메소드

솔트가 생성되고 해시 앞에 추가된 후 전체 연결된 바이트 배열이 Base64 문자열로 변환
출력값에 Hash와 Salt가 모두 포함 된

byte[] salt = CreateSalt();
argon2.Salt = salt;
// ...
byte[] hashWithSaltBytes = new byte[salt.Length + hashBytes.Length];
Array.Copy(salt, 0, hashWithSaltBytes, 0, salt.Length);
Array.Copy(hashBytes, 0, hashWithSaltBytes, salt.Length, hashBytes.Length);

return Convert.ToBase64String(hashWithSaltBytes);
  1. 저장: 반환된 문자열에는 해시와 솔트가 모두 포함되어 있습니다. 이는 단일 문자열을 데이터베이스에 저장할 수 있으며 나중에 비밀번호를 검증할 때 필요한 모든 것을 갖추게 됩니다.

  2. 검증: 비밀번호를 검증할 필요가 있을 때, 이 문자열을 디코드하여 솔트와 해시를 분리하여 추출하고, 해당 솔트를 사용하여 비밀번호 시도를 해시할 수 있습니다. 새로운 해시가 저장된 해시와 일치하면 비밀번호가 맞는 것입니다.

해시와 함께 솔트를 저장하는 것은 비밀번호 검증 과정이 의도한 대로 작동할 수 있도록 하는 일반적인 관행입니다. 원본 솔트에 접근할 수 없다면, 동일한 비밀번호라도 다른 솔트로 해시하면 다른 해시가 생성되기 때문에 비밀번호가 맞는지 검증할 수 없습니다.

결론적으로, 비밀번호 검증 과정이 의도한 대로 작동할 수 있도록 하려면 두 번째 방법이 올바른 방법입니다. 첫 번째 메소드는 나중에 검증을 위해 필요한 솔트를 해시와 함께 포함하지 않아서 적절하지 않습니다.

암호화로 저장된 Hash Password
[  
	{  
		"id": "Master",  
		"pw": "uXSnqOiXKs3va9nRneO9lhRX5f6Xf7PkXlUKwZJJWd55tElTSAav/BwUGcKWlLf
			   j1OEIXVWHqXVSzna+OBovhFyYcVYn8bP3MD59Ni9P+GSLnewNLtwZ0HNxyUvkGC
			   9A/sYhTK6BUto50+peZFYGZiu6GnOU6rQledkgJbOfra
			   T5zM8F6QOoDy9Qw1PFYkhT",  
		"level": 1,  
		"exp": 0,  
		"job": 5,  
		"damage": 10,  
		"defence": 5,  
		"hp": 100,  
		"gold": 1500,  
		"inventory": 
		{  
			"무쇠갑옷": {  "equipment": false  },  
			"낡은 검": {  "equipment": false  }  
		}  
	}  
]