프로젝트 개요
문서 중앙화 DLP/ECM 시스템의 Windows 클라이언트입니다. .enc 파일을 더블클릭하면 KMS 서버에서 복호화 키를 받아 임시 복호화 후 원본 앱으로 열고, 사용자가 저장하면 자동으로 재암호화합니다.
GitHub: https://github.com/LEntropy/Document_Management
전체 파일 열기 흐름
사용자가 file.docx.enc 더블클릭
↓
EncOpenHelper.DecryptToTempAndOpenAsync(encPath)
↓
EncCryptoBridge.DecryptEncToBytesAsync(encPath)
├─ .enc 파일에서 헤더(keyId, namespace, IV) 파싱
├─ KmsApiClient.RequestDecrypt(encKey, requesterOrg, fileOrg, keyId)
│ └─ TLS 소켓 → KMS 서버(3.37.149.78:8443)
│ └─ 서버가 RSA 개인키로 AES 키 복호화 후 반환
└─ AesCrypto.Decrypt(ciphertext, aesKey, iv) → 원본 bytes
↓
%LOCALAPPDATA%/KmsClient/TempOpen/file.docx 임시 저장
├─ ACL: 현재 사용자만 접근 허용
↓
explorer.exe로 열기 (연결 프로그램 자동 실행)
↓
FileSystemWatcher 저장 감지 (Changed/Renamed 이벤트)
├─ 디바운스 700ms
├─ 파일 안정화 확인 (StableCheck × 4회)
└─ EncCryptoBridge.EncryptAndWriteBack() → .enc 재암호화
↓
30초 후 임시 파일 자동 삭제
1. RSA+AES 하이브리드 암호화 구조
| 단계 | 알고리즘 | 역할 |
|---|---|---|
| 파일 암호화 | AES-256-CBC | 실제 파일 내용 암호화 (고속) |
| 키 암호화 | RSA 4096bit PKCS#1 v1.5 | AES 키를 조직 공개키로 보호 |
| 키 복호화 | RSA 개인키 (KMS 서버 보관) | 서버만 AES 키 복호화 가능 |
클라이언트는 RSA 개인키를 절대 보유하지 않습니다. 복호화는 항상 KMS 서버를 거쳐야 합니다.
2. RSA 암호화 - RsaCrypto.cs
public static class RsaCrypto
{
// AES 키를 RSA 공개키(X.509 인증서)로 암호화
public static byte[] EncryptKey(byte[] plainKey, string pemText)
{
using (RSA rsa = LoadRsaFromCertificatePem(pemText))
{
byte[] encrypted = rsa.Encrypt(
plainKey,
RSAEncryptionPadding.Pkcs1 // OpenSSL RSA_private_decrypt와 호환
);
// RSA 4096bit = 512 bytes 검증
if (encrypted.Length != rsa.KeySize / 8)
throw new Exception("RSA encrypted length mismatch");
return encrypted;
}
}
// X.509 PEM → RSA 공개키 추출
private static RSA LoadRsaFromCertificatePem(string pem)
{
byte[] der = DecodePem(pem, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
var cert = new X509Certificate2(der);
return cert.PublicKey.Key as RSA;
}
}
3. AES-256-CBC 암호화/복호화 - AesCrypto.cs
public static class AesCrypto
{
public static byte[] Encrypt(byte[] plaintext, byte[] key, byte[] iv)
{
using (var aes = Aes.Create())
{
aes.KeySize = 256;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
aes.IV = iv;
using (var encryptor = aes.CreateEncryptor())
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
cs.Write(plaintext, 0, plaintext.Length);
return ms.ToArray();
}
}
}
public static byte[] Decrypt(byte[] ciphertext, byte[] key, byte[] iv)
{
using (var aes = Aes.Create())
{
aes.Key = key; aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var decryptor = aes.CreateDecryptor())
using (var ms = new MemoryStream(ciphertext))
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
using (var result = new MemoryStream())
{
cs.CopyTo(result);
return result.ToArray();
}
}
}
}
4. KMS 서버 통신 - KmsApiClient.cs
TLS 소켓으로 KMS 서버(포트 8443)에 연결해 암호화된 AES 키의 복호화를 요청합니다.
public static byte[] RequestDecrypt(byte[] encKey, string requesterOrg, string fileOrg, string keyId)
{
using (TcpClient client = new TcpClient())
{
client.ReceiveTimeout = 5000;
client.Connect("3.37.149.78", 8443);
using (SslStream ssl = new SslStream(client.GetStream(), false, (a,b,c,d) => true))
{
ssl.AuthenticateAsClient("kms-server");
// 요청자 조직, 파일 조직, 키 ID 전송
WriteString(ssl, requesterOrg);
WriteString(ssl, fileOrg);
WriteString(ssl, keyId);
// 암호화된 AES 키 전송 (512 bytes)
WriteInt32BE(ssl, encKey.Length);
WriteExact(ssl, encKey);
ssl.Flush();
// 서버 응답 코드 수신
int result = ReadInt32BE(ssl);
if (result == -403)
{
MainForm.ShowAccessDeniedNotification("허용되지 않은 접근입니다.");
throw new Exception("KMS 403 Forbidden");
}
// 복호화된 AES 키 수신
int outLen = ReadInt32BE(ssl);
return ReadExact(ssl, outLen);
}
}
}
5. .enc 파일 열기 - EncOpenHelper.cs
public static async Task DecryptToTempAndOpenAsync(string encPath, Action<string> log, Func<string, Task> onSaveBack)
{
// 1. .enc → 원본 bytes 복호화
byte[] plain = await EncCryptoBridge.DecryptEncToBytesAsync(encPath, log);
// 2. ACL로 보호된 임시 디렉토리 생성 (현재 사용자만 접근)
string tempDir = EnsureSecureTempDirectory(log);
string tempPath = Path.Combine(tempDir, RemoveTrailingEnc(Path.GetFileName(encPath)));
// 3. 임시 파일 저장 (Temporary 속성)
File.WriteAllBytes(tempPath, plain);
File.SetAttributes(tempPath, FileAttributes.Temporary);
// 4. 저장 감지 Watcher 등록
if (onSaveBack != null)
InstallSaveWatcher(tempPath, log, onSaveBack);
// 5. explorer.exe로 열기 (연결 프로그램 자동 실행)
Process.Start(new ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = '"' + tempPath + '"',
UseShellExecute = true
});
// 6. 30초 후 임시 파일 자동 삭제 (재시도 포함)
Task.Run(() => DelayedDeleteWithRetry(tempPath, log));
}
6. 저장 감지 + 자동 재암호화 (FileSystemWatcher)
사용자가 임시 파일을 저장하면 원본 .enc 파일에 재암호화하여 덮어씁니다. 디바운스와 파일 안정화 확인으로 이벤트 폭주를 방지합니다.
private static void InstallSaveWatcher(string tempPath, Action<string> log, Func<string,Task> onSaveBack)
{
var w = new FileSystemWatcher(Path.GetDirectoryName(tempPath))
{
Filter = Path.GetFileName(tempPath),
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName
};
// Changed: 일반 저장 (Word, Notepad 등)
w.Changed += (s, e) => ScheduleSaveBack(tempPath, log, onSaveBack, "Changed");
// Renamed: Office가 임시파일로 저장 후 rename하는 방식 대응
w.Renamed += (s, e) => ScheduleSaveBack(tempPath, log, onSaveBack, "Renamed");
w.EnableRaisingEvents = true;
_watchers[tempPath] = w;
}
// 디바운스 700ms + 파일 안정화 확인 후 재암호화 콜백 호출
private static void ScheduleSaveBack(string tempPath, Action<string> log,
Func<string,Task> onSaveBack, string reason)
{
// 이전 예약 취소
if (_debounce.TryGetValue(tempPath, out var old)) { old.Cancel(); }
var cts = new CancellationTokenSource();
_debounce[tempPath] = cts;
Task.Run(async () =>
{
await Task.Delay(700, cts.Token); // 디바운스 700ms
bool stable = await WaitForStableAsync(tempPath, cts.Token); // 안정화 확인
if (stable) await onSaveBack(tempPath); // 재암호화 실행
});
}
7. .enc 파일 포맷 구조
[ .enc 파일 헤더 ]
+-------------------+------------------+
| 필드 | 크기 |
+-------------------+------------------+
| Magic (ECM1) | 4 bytes |
| keyId (UTF-8) | 2 + N bytes |
| namespace (UTF-8) | 2 + N bytes |
| IV (AES) | 16 bytes |
| encKey (RSA) | 512 bytes |
| ciphertext | 나머지 전체 |
+-------------------+------------------+
복호화 흐름:
1. 헤더에서 keyId, namespace, encKey, IV 파싱
2. KMS 서버에 encKey 복호화 요청 → rawAesKey 반환
3. AesCrypto.Decrypt(ciphertext, rawAesKey, IV) → 원본 파일
8. 아이콘 변경 - Shell/IconComposer.cs
.enc 파일에 자물쇠 아이콘을 오버레이하여 암호화 상태를 시각적으로 표시합니다.
public static class IconComposer
{
// 원본 파일 아이콘 + 자물쇠 오버레이 합성
public static Icon ComposeWithLock(string originalExtension)
{
Icon baseIcon = IconChache.GetIconForExtension(originalExtension);
Bitmap bmp = baseIcon.ToBitmap();
using (Graphics g = Graphics.FromImage(bmp))
{
// 우하단에 자물쇠 아이콘 오버레이
g.DrawImage(Resources.LockOverlay, bmp.Width - 12, bmp.Height - 12, 12, 12);
}
return Icon.FromHandle(bmp.GetHicon());
}
}
9. 가상 드라이브 마운트
암호화 보관 폴더를 Windows 가상 드라이브(Z:)로 매핑하여 탐색기에서 편리하게 접근할 수 있게 합니다.
// 가상 드라이브 마운트 (subst 명령 활용)
subst Z: "%TEMP%\ECM_Virtual_Root"
// 프로그램 코드에서:
Process.Start("subst", @"Z: " + virtualRootPath);'勉強 > C#' 카테고리의 다른 글
| 문서 중앙화 프로젝트 - C# Windows 클라이언트 (SMB, USB 차단, 화면잠금, 소켓 통신) 및 프로젝트 실패 원인 정리 (0) | 2026.06.02 |
|---|