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)
{
// ... 추출과 검증 로직
}
비밀번호 해시 생성 과정
- 솔트 생성: 각 비밀번호에 대해 유일한 솔트를 생성합니다. 이는 같은 비밀번호라도 다른 해시 값을 생성하게 해줍니다.
- Argon2 인스턴스 구성: 메모리 크기, 반복 횟수, 병렬 처리 수준을 설정합니다.
- 비밀번호 해시: 비밀번호와 솔트를 결합하여 해시를 생성합니다.
- 저장: 해시와 솔트를 데이터베이스에 안전하게 저장합니다.
비밀번호 검증 과정
- 솔트 추출: 데이터베이스에서 사용자의 해시를 가져와서 솔트를 추출합니다.
- 새 해시 생성: 입력된 비밀번호와 솔트를 사용하여 새로운 해시를 생성합니다.
- 비교: 새로 생성된 해시와 데이터베이스에 저장된 해시를 비교합니다.
실수했던 코드
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 검증 부분이 제대로 작동하지 않아
비밀번호에 대한 인증이 실패하는 상황
확인 해야 하는 부분
- 저장된 Hash (
storedHash
)가 실제로 Base64 인코딩 된 값인지- 저장된 hash에서 salt를 정확히 추출하고 있는지
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);
-
저장: 반환된 문자열에는 해시와 솔트가 모두 포함되어 있습니다. 이는 단일 문자열을 데이터베이스에 저장할 수 있으며 나중에 비밀번호를 검증할 때 필요한 모든 것을 갖추게 됩니다.
-
검증: 비밀번호를 검증할 필요가 있을 때, 이 문자열을 디코드하여 솔트와 해시를 분리하여 추출하고, 해당 솔트를 사용하여 비밀번호 시도를 해시할 수 있습니다. 새로운 해시가 저장된 해시와 일치하면 비밀번호가 맞는 것입니다.
해시와 함께 솔트를 저장하는 것은 비밀번호 검증 과정이 의도한 대로 작동할 수 있도록 하는 일반적인 관행입니다. 원본 솔트에 접근할 수 없다면, 동일한 비밀번호라도 다른 솔트로 해시하면 다른 해시가 생성되기 때문에 비밀번호가 맞는지 검증할 수 없습니다.
결론적으로, 비밀번호 검증 과정이 의도한 대로 작동할 수 있도록 하려면 두 번째 방법이 올바른 방법입니다. 첫 번째 메소드는 나중에 검증을 위해 필요한 솔트를 해시와 함께 포함하지 않아서 적절하지 않습니다.
암호화로 저장된 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 }
}
}
]