서버와 클라이언트 간 통신에서 비밀번호와 같은 민감한 정보를 공개키 방식으로 통신하는 이유는 보안과 개인 정보 보호를 강화하기 위해서입니다. 공개키 암호화는 대칭키 암호화에 비해 추가적인 보안 기능을 제공합니다.
💬 시작하기 앞서...
본 글은 소켓 서버 자체 구축 중 로그인 기능을 구현하기위해 서버로 비밀번호를 요청할 때 비밀번호를 평문으로 보내면 유출가능성이 있어 이를 해결하기위한 공개키 암호화 방식을 사용하는 방법을 구현하고 정리한 글 입니다.
💬 서론
공개키 암호화 방식은 공개키와 개인키라는 두 개의 키를 사용합니다. 공개키는 모든 사람에게 공개되어 있으며, 개인키는 키 소유자에게만 비밀로 유지됩니다. 이 방식은 다음과 같은 장점을 가지고 있습니다.
- 기밀성: 공개키로 암호화된 정보는 개인키로만 해독이 가능합니다. 따라서, 공개키로 암호화된 정보를 가로채더라도 개인키를 알지 못한다면 해독이 불가능합니다. 이는 민감한 정보의 기밀성을 보호하는 데 도움이 됩니다.
- 인증: 공개키 암호화 방식은 개인키를 가지고 있는 유일한 소유자가 해당 정보를 생성했다는 것을 보장합니다. 클라이언트는 서버의 공개키로 암호화하여 보낸 정보를 서버의 개인키로만 해독할 수 있으므로, 서버가 신원을 입증하는 데 사용될 수 있습니다.
- 변조 방지: 공개키 암호화 방식을 사용하면 정보가 전송되는 동안 변조되지 않았음을 확인할 수 있습니다. 공개키로 암호화된 정보가 서버에 도착할 때까지 중간에서 정보를 변경하는 것은 매우 어렵기 때문입니다.
- 중간자 공격 방지: 공개키 암호화 방식은 중간자 공격을 예방합니다. 중간자 공격은 공격자가 클라이언트와 서버 간의 통신을 가로채어 정보를 조작하거나 도청하는 것을 말합니다. 공개키 암호화 방식을 사용하면 클라이언트는 서버의 공개키를 통해 정보를 암호화하고, 서버는 개인키를 사용하여 정보를 해독합니다. 따라서, 공격자는 클라이언트와 서버 간의 통신을 해독할 수 없습니다.
이러한 이유로, 비밀번호와 같은 민감한 정보를 공개키 방식으로 암호화하여 전송하면 보안 강화와 개인 정보 보호를 더욱 효과적으로 할 수 있습니다.
📖 구현 내용
- 구현 내용
⚒️ 구현
- 서버와 클라이언트에서 각각 필요한 기능을 구현하였습니다.
· 클라이언트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Security.Cryptography;
using System.Text;
using Google.Protobuf;
public class SecurityManager : MonoBehaviour
{
private static string _PublicKeyXml; // 서버가 생성한 공개 키
/// <summary>
/// 서버에 연결되면 공개키를 받아 저장
/// </summary>
/// <param name="publicKeyXml">서버가 생성한 공개 키</param>
public static void Init(string publicKeyXml)
{
// 공개 키 저장
_PublicKeyXml = publicKeyXml;
}
/// <summary>
/// 공개키를 이용하여 평문을 암호화
/// </summary>
/// <param name="plaintext">암호화 할 평문</param>
/// <returns></returns>
public static ByteString EncryptRSA(string plaintext)
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// Xml 형식의 공개키를 불러옴
rsa.FromXmlString(_PublicKeyXml);
// 평문을 byte[]로 변환
byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
// 암호화
byte[] encryptedBytes = rsa.Encrypt(plaintextBytes, false);
return ByteString.CopyFrom(encryptedBytes);
}
}
}
private static string _PublicKeyXml; // 서버가 생성한 공개 키
/// <summary>
/// 서버에 연결되면 공개키를 받아 저장
/// </summary>
/// <param name="publicKeyXml">서버가 생성한 공개 키</param>
public static void Init(string publicKeyXml)
{
// 공개 키 저장
_PublicKeyXml = publicKeyXml;
}
- 공개키는 고유하며, 서버가 생성하였기에 서버로부터 공개키를 받아와야합니다.
- 서버로부터 연결되면, 서버는 공개키를 클라이언트에게 전송합니다.
/// <summary>
/// 공개키를 이용하여 평문을 암호화
/// </summary>
/// <param name="plaintext">암호화 할 평문</param>
/// <returns></returns>
public static ByteString EncryptRSA(string plaintext)
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// Xml 형식의 공개키를 불러옴
rsa.FromXmlString(_PublicKeyXml);
// 평문을 byte[]로 변환
byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
// 암호화
byte[] encryptedBytes = rsa.Encrypt(plaintextBytes, false);
return ByteString.CopyFrom(encryptedBytes);
}
}
- 암호화가 필요한 평문을 공개키를 이용하여 암호화합니다.
- 리턴타입인 ByteString은 Protobuf(구글 프로토콜 버퍼)에서 패킷통신을 위해 사용합니다.
· 서버
- 서버는 RSA에 필요한 키쌍을 생성하고, 이를 보관하고있다가 다른 플레이어가 접속할경우 공개키를 전달해줘야합니다.
- 클라이언트가 공개키로 암호화한 암호문을 전송하면, 서버는 개인키를 이용하여 이를 복호화하여 적절하게 사용합니다.
using Google.Protobuf;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
public class SecurityManager
{
public static string _PublicKeyXml { get; private set; } // Public Key
public static RSAParameters _PrivateKey { get; private set; } // Private Key
static SecurityManager()
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// RSA 파라미터 생성
RSAParameters publicKey = rsa.ExportParameters(false);
_PrivateKey = rsa.ExportParameters(true);
Console.WriteLine("\n==== RSA 키 생성 ====");
// 공개 키 출력
Console.WriteLine("Public Key Modulus (n): " + Convert.ToBase64String(publicKey.Modulus));
Console.WriteLine("Public Key Exponent (e): " + Convert.ToBase64String(publicKey.Exponent));
// 공개 키 변환
_PublicKeyXml = ConvertToXml(publicKey);
Console.WriteLine("Public Key XML: " + _PublicKeyXml);
// 비밀 키 출력
Console.WriteLine("Private Key Modulus (n): " + Convert.ToBase64String(_PrivateKey.Modulus));
Console.WriteLine("Private Key Exponent (d): " + Convert.ToBase64String(_PrivateKey.D));
Console.WriteLine("\n");
}
}
/// <summary>
/// 클라이언트에 전송하기위해 Xml 형식으로 생성
/// </summary>
/// <param name="rsaParams"></param>
/// <returns></returns>
static string ConvertToXml(RSAParameters rsaParams)
{
StringBuilder sb = new StringBuilder();
using (StringWriter sw = new StringWriter(sb))
{
using (XmlWriter xmlWriter = XmlWriter.Create(sw))
{
xmlWriter.WriteStartDocument();
xmlWriter.WriteStartElement("RSAKeyValue");
WriteElement(xmlWriter, "Modulus", rsaParams.Modulus);
WriteElement(xmlWriter, "Exponent", rsaParams.Exponent);
xmlWriter.WriteEndElement();
xmlWriter.WriteEndDocument();
}
}
return sb.ToString();
}
/// <summary>
/// Xml 요소를 작성
/// </summary>
/// <param name="writer"></param>
/// <param name="elementName"></param>
/// <param name="data"></param>
private static void WriteElement(XmlWriter writer, string elementName, byte[] data)
{
writer.WriteStartElement(elementName);
writer.WriteBase64(data, 0, data.Length);
writer.WriteEndElement();
}
/// <summary>
/// 공개키로 암호화 한 암호문을 비밀키를 복호화
/// </summary>
/// <param name="encryptedData">복호화 할 암호문</param>
/// <returns></returns>
public static string DecryptRSA(ByteString encryptedData)
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// RSAParameters로부터 RSACSP를 가져옴
rsa.ImportParameters(_PrivateKey);
// Byte[]로 변환
byte[] encryptedBytes = encryptedData.ToByteArray();
// 복호화
byte[] decryptedBytes = rsa.Decrypt(encryptedBytes, false);
// 평문 획득
string decryptedText = Encoding.UTF8.GetString(decryptedBytes);
return decryptedText;
}
}
}
public static string _PublicKeyXml { get; private set; } // Public Key
public static RSAParameters _PrivateKey { get; private set; } // Private Key
static SecurityManager()
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// RSA 파라미터 생성
RSAParameters publicKey = rsa.ExportParameters(false);
_PrivateKey = rsa.ExportParameters(true);
Console.WriteLine("\n==== RSA 키 생성 ====");
// 공개 키 출력
Console.WriteLine("Public Key Modulus (n): " + Convert.ToBase64String(publicKey.Modulus));
Console.WriteLine("Public Key Exponent (e): " + Convert.ToBase64String(publicKey.Exponent));
// 공개 키 변환
_PublicKeyXml = ConvertToXml(publicKey);
Console.WriteLine("Public Key XML: " + _PublicKeyXml);
// 비밀 키 출력
Console.WriteLine("Private Key Modulus (n): " + Convert.ToBase64String(_PrivateKey.Modulus));
Console.WriteLine("Private Key Exponent (d): " + Convert.ToBase64String(_PrivateKey.D));
Console.WriteLine("\n");
}
}
- 서버는 프로그램이 실행되면 최초 1회 RSA에 사용될 암호키 쌍을 생성하고 보관합니다.
- 이 값은 서버 프로그램이 실행될 때마다 변경되며, Seed를 이용하여 고정키 쌍을 생성할 수 없습니다. (난수 생성 규칙)
// 공개 키 변환
_PublicKeyXml = ConvertToXml(publicKey);
/// <summary>
/// 클라이언트에 전송하기위해 Xml 형식으로 생성
/// </summary>
/// <param name="rsaParams"></param>
/// <returns></returns>
static string ConvertToXml(RSAParameters rsaParams)
{
StringBuilder sb = new StringBuilder();
using (StringWriter sw = new StringWriter(sb))
{
using (XmlWriter xmlWriter = XmlWriter.Create(sw))
{
xmlWriter.WriteStartDocument();
xmlWriter.WriteStartElement("RSAKeyValue");
WriteElement(xmlWriter, "Modulus", rsaParams.Modulus);
WriteElement(xmlWriter, "Exponent", rsaParams.Exponent);
xmlWriter.WriteEndElement();
xmlWriter.WriteEndDocument();
}
}
return sb.ToString();
}
- 공개키는 클라이언트에 전송하기위해 보내기 쉬운 Xml로 변환하여 저장합니다.
/// <summary>
/// 공개키로 암호화 한 암호문을 비밀키를 복호화
/// </summary>
/// <param name="encryptedData">복호화 할 암호문</param>
/// <returns></returns>
public static string DecryptRSA(ByteString encryptedData)
{
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// RSAParameters로부터 RSACSP를 가져옴
rsa.ImportParameters(_PrivateKey);
// Byte[]로 변환
byte[] encryptedBytes = encryptedData.ToByteArray();
// 복호화
byte[] decryptedBytes = rsa.Decrypt(encryptedBytes, false);
// 평문 획득
string decryptedText = Encoding.UTF8.GetString(decryptedBytes);
return decryptedText;
}
}
- 공개키로 암호화 된 암호문을 자신의 개인키를 이용하여 평문으로 복호화합니다.
🕹️ Unity Affiliate
- Unity Affiliate Program 파트너로서 아래의 배너를 통해 접속하신 경우 수수료를 받을 수 있습니다.
- 아래 배너의 에셋들은 '실시간 무료 에셋 랭킹'을 나타냅니다.
'server > socket server' 카테고리의 다른 글
[C# 서버] 소켓 서버 문서 (0) | 2024.01.28 |
---|---|
[Socket + MySql] 비동기 쿼리 사용 This MySqlConnection is already in use. (0) | 2023.06.21 |
[구글 프로토콜 버퍼] 패킷 한글 주석 인코딩 해결 (0) | 2023.05.18 |
[C# 서버] Async Session, Event (0) | 2023.02.06 |
[C# 서버] Async Listener (0) | 2023.01.17 |