내가 맡고 있는 서비스의 개인정보가 DB에 그대로 노출되는 이슈가 있어(오래된 서비스이다), 회원 패스워드와 휴대폰 번호, 이메일에 대해 암호화를 진행했다. 비밀번호는 SHA-1 방식, 휴대폰 번호와 이메일은 AES를 채택했다(이유는 본문에).
🔊해시(HASH) / 암호화(Encryption)
해쉬
해시는 동일한 입력값을 받아 동일한 출력값을 보장한다. 또한 특징으로 조금만 달라져도 해쉬된 값은 크게 달라지며(눈사태효과) 복호화가 되지 않는다. 예를들어 입력값 A에 의해 B가 출력되었다면, 출력된 B값만 주어졌을 때는 입력값 A를 계산적으로 찾는 것이 불가능하다. 그래서 이와 관련된 보안이슈도 무차별대입공격에 의해 이루어지며 최근 SHA-1의 충돌 이슈가 존재하기도 한다. 해쉬의 대표적인 예로 MD5,SHA 종류의 해쉬함수가 존재한다.
이를 단방향 암호화라고 하는데 비밀번호를 찾을 수는 없고 새로운 비밀번호로 입력해야 되는 것도 이 때문이다. (따라서 원래 비밀번호가 뭐였는지 모른다.. 혹시 비밀번호를 알려주는 곳은... 신고하세요)
암호화
해쉬와 암호화의 가장 큰 차이점은 암호화의 양방향성에 있다. 복호화가 안되는 해쉬와 달리 암호화는 복호화가 가능하다는 점이다. 대표적으로 RSA, AES등의 암호화가 존재한다. 큰 틀에서 대칭형 암호화 방식과 비대칭형 암호화 방식으로 나뉜다.
1)대칭형 암호화
암호화, 복호화에 쓰이는 키가 동일한 암호화 방식이다. DES, AES 등의 알고리즘이 있다. 공개키 암호화 방식에 비해 속도가 빠르지만 키를 교환해야 하는 문제가 발생한다. 키를 임의로 개발자가 정하는 데, 키가 탈취될 수 있는 문제도 당연히 발생한다. 이러한 교환에서 사용하는 사람이 증가하면 키 또한 많아지게 된다.
2)비대칭형 암호화(공개키 알고리즘)
공개키와 개인키가 존재하며, 공개키는 말그대로 모든 사람이 접근 가능하며 개인키는 private한 키이다. 공개키로 암호화를 진행하고 개인키로 복호화를 진행해서 키 분배 단점을 해결했지만, 로직이 추가되는 만큼 속도 또한 대칭키 암호화 방식에 비해 느린 단점이 있다.
🎭SHA-1
Secure Hash Algorithm -1
해쉬 함수
SHA-1은 이전에 널리 통용되던 MD5를 대신해서 등장했다. MD5의 경우 노트북 한대의 연산 능력으로 1분의 무차별 대입 공격을 통해 충돌을 찾을 정도로 보안에 취약했는데 SHA-1의 경우 이러한 보안 이슈를 어느정도 해소시켜 주었다.(최근 Google, CWI 암스테르담 연구소는 암호학적 해쉬함수 SHA-1에 대한 충돌 공격 성공으로 SHA-2이상의 해쉬함수 이용이 권장됨).
SHA-1은 2^64비트보다 작은 입력 데이터를 160비트의 고정된 크기로 출력한다. 입력 블록 단위 처리는 32비트 *80개 값(W0~W79)을 계산한다. 512비트로 나누어떨어지지 않는 경우 패딩(padding)처리가 필요한데 메시지 다음에 여분의 데이터를 부가하여 메시지 길이가 512비트의 정수배가 되도록 한다.
보안 이슈때문에 현재는 OS, 브라우저에서 SHA-1 알고리즘을 지원 중단하고 SHA-2 계열을 지원한다. SHA-1의 취약점은 실제 사례가 아닌 이론적인 이야기일 뿐이었는데 2017년 구글에서 충돌성공으로 2018년 까지 구글과 MS에서는 SHA-2로 전환을 의무화 했다고 한다. 현재 전 세계 대부분 기업이 SHA-2 전환을 완료했다고 한다.
현재 내가 맡은 서비스에도 아직까지도 많이 사용되는 SHA-1방식으로 적용되어 있다.(사실 그냥 전사적으로 SHA-1방식인데) 이제 이를 SHA-2로 바꾸어야할 때가 왔다. 문제는 내가 한번 시범적으로 적용하고 테스트를 해보고 싶은데(일 만든다고 혼날까봐), 이미 DB 속 SHA-1 메시지 다이제스트를 SHA-2로 바꾸는 걸 고민하고 있다.
C#에서는 이미 알고리즘을 구현해놔서 구현자체는 편하게 할 수있다. 필자는 선배들 따라서 일단 MBC 회원관리 DB중 패스워드가 이미 SHA-1으로 입력되있기도 해서 SHA-1으로 구현해놨는데 청개구리처럼 SHA-2로 하고싶다.
public class SHA1CryptoService
{
public static string EncryptText(string targetText)
{
SHA1CryptoServiceProvider shaProvider = new SHA1CryptoServiceProvider();
byte[] hashedDataBytes;
hashedDataBytes = shaProvider.ComputeHash(ASCIIEncoding.UTF8.GetBytes(targetText));
return BitConverter.ToString(hashedDataBytes).Replace("-", "");
}
}
🎭AES
Advanced Encryption Standard
암호화
표준 암호화 알고리즘으로 DES대신 나온 방식이다. 알고리즘적인 부분은 다른 곳의 설명을 참고하면 되겠다. 가장 큰 장점은 구현이 쉽고 메모리를 적게 소모한다는 점, 아직 보안 이슈가 없라는 점등이 있다. 많은 암호화 방식 중 AES를 선택한 것은 속도에 있다. 대칭형 알고리즘의 가장 큰 장점은 속도인데 최소 10~1000배 이상 빠르기 때문이다. 하지만 동일한 키를 사용하기 때문에 원리상 데이터를 송신하는 사이에서 KEY를 가져야 하는데 이 KEY를 송신하는 과정에서 중간에 가로챌 수 있는 보안 문제가 있기 때문에, Password 와 같은 핵심 정보에는 사용할 수 없다. 또는 인터넷 뱅킹과 같은 서비스를 한다면 AES가 아닌 공개키 알고리즘을 필수적으로 고려했을 것이다. 다만 MBC 이메일과 번호등은 일반적으로 많이 쓰이고 속도도 빠른 AES를 나도 채택해서 아카이브 서비스에 적용했다. SHA-1과 다르게 회원이 이메일과 전화번호 등을 수정할 때 원문 Text가 보여야 하므로 복호화 과정도 필요하다. 다음을 코드로 구현해보았다. (키값은 가변 바이트길이 가능)
/// <summary>
/// AESCryptoService의 요약 설명입니다.
/// </summary>
public class AESCryptoService
{
//암호화를 위한 키값
private const string cipherPassword = "임의의 키값ㅇㅅㅇ(비밀)";
//AES 암호화
public static string EncryptString(string InputText)
{
// Rihndael class를 선언하고, 초기화 합니다
RijndaelManaged RijndaelCipher = new RijndaelManaged();
// 입력받은 문자열을 바이트 배열로 변환
byte[] PlainText = System.Text.Encoding.Unicode.GetBytes(InputText);
// 딕셔너리 공격을 대비해서 키를 더 풀기 어렵게 만들기 위해서
// Salt를 사용합니다.
byte[] Salt = Encoding.ASCII.GetBytes(cipherPassword.Length.ToString());
// PasswordDeriveBytes 클래스를 사용해서 SecretKey를 얻습니다.
PasswordDeriveBytes SecretKey = new PasswordDeriveBytes(cipherPassword, Salt);
// Create a encryptor from the existing SecretKey bytes.
// encryptor 객체를 SecretKey로부터 만듭니다.
// Secret Key에는 32바이트
// (Rijndael의 디폴트인 256bit가 바로 32바이트입니다)를 사용하고,
// Initialization Vector로 16바이트
// (역시 디폴트인 128비트가 바로 16바이트입니다)를 사용합니다
ICryptoTransform Encryptor = RijndaelCipher.CreateEncryptor(SecretKey.GetBytes(32), SecretKey.GetBytes(16));
// 메모리스트림 객체를 선언,초기화
MemoryStream memoryStream = new MemoryStream();
// CryptoStream객체를 암호화된 데이터를 쓰기 위한 용도로 선언
CryptoStream cryptoStream = new CryptoStream(memoryStream, Encryptor, CryptoStreamMode.Write);
// 암호화 프로세스가 진행됩니다.
cryptoStream.Write(PlainText, 0, PlainText.Length);
// 암호화 종료
cryptoStream.FlushFinalBlock();
// 암호화된 데이터를 바이트 배열로 담습니다.
byte[] CipherBytes = memoryStream.ToArray();
// 스트림 해제
memoryStream.Close();
cryptoStream.Close();
// 암호화된 데이터를 Base64 인코딩된 문자열로 변환합니다.
string EncryptedData = Convert.ToBase64String(CipherBytes);
// 최종 결과를 리턴
return EncryptedData;
}
public static string DecryptString(string InputText)
{
RijndaelManaged RijndaelCipher = new RijndaelManaged();
byte[] EncryptedData = Convert.FromBase64String(InputText);
byte[] Salt = Encoding.ASCII.GetBytes(cipherPassword.Length.ToString());
PasswordDeriveBytes SecretKey = new PasswordDeriveBytes(cipherPassword, Salt);
// Decryptor 객체를 만듭니다.
ICryptoTransform Decryptor = RijndaelCipher.CreateDecryptor(SecretKey.GetBytes(32), SecretKey.GetBytes(16));
MemoryStream memoryStream = new MemoryStream(EncryptedData);
// 데이터 읽기(복호화이므로) 용도로 cryptoStream객체를 선언, 초기화
CryptoStream cryptoStream = new CryptoStream(memoryStream, Decryptor, CryptoStreamMode.Read);
// 복호화된 데이터를 담을 바이트 배열을 선언합니다.
// 길이는 알 수 없지만, 일단 복호화되기 전의 데이터의 길이보다는
// 길지 않을 것이기 때문에 그 길이로 선언합니다
byte[] PlainText = new byte[EncryptedData.Length];
// 복호화 시작
int DecryptedCount = cryptoStream.Read(PlainText, 0, PlainText.Length);
memoryStream.Close();
cryptoStream.Close();
// 복호화된 데이터를 문자열로 바꿉니다.
string DecryptedData = Encoding.Unicode.GetString(PlainText, 0, DecryptedCount);
// 최종 결과 리턴
return DecryptedData;
}
}
💻결론
SHA-1 에서 SHA-2 전환 작업 하신 분 댓글 주세요
'Web > Web지식' 카테고리의 다른 글
[모듈번들링] Module Bundler: Webpack 도입 적용 경험 #1,2 (0) | 2021.08.05 |
---|---|
우리가 Base64를 사용하는 이유 (0) | 2021.02.18 |
Web Protocol 정리 (0) | 2021.02.17 |