using System.IO; using System.Security.Cryptography; using System.Text; using Paca.App.ViewModels; using Paca.Browser.Security; using Paca.Core.Security; using Xunit; namespace Paca.Tests.Security; /// /// 51차: OS 생체 인증(Windows Hello, Touch ID, DPAPI) 및 고보안 자격증명 금고 기능 전수 검증. /// public class Milestone51WindowsHelloAndCredentialVaultTests { private readonly string _testDir; public Milestone51WindowsHelloAndCredentialVaultTests() { _testDir = Path.Combine(Path.GetTempPath(), "paca_vault_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_testDir); } private void Cleanup() { try { if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true); } catch { } } #region 1. OS Biometric & Mock Authenticator Tests [Fact] public void WindowsHello_가용성_조회_및_플랫폼_지원_확인() { var auth = new WindowsHelloAuthenticator(); var availability = auth.CheckAvailability(); Assert.NotNull(auth.BiometricProviderName); Assert.Contains("Windows Hello", auth.BiometricProviderName); if (OperatingSystem.IsWindows()) { Assert.True(auth.IsHardwareEnclaveSupported); Assert.True(availability is BiometricAvailability.Available or BiometricAvailability.NotConfigured); } else { Assert.Equal(BiometricAvailability.UnsupportedPlatform, availability); } } [Fact] public async Task MockBiometricAuthenticator_성공_실패_취소_지연_시뮬레이션() { var mock = new MockBiometricAuthenticator { Availability = BiometricAvailability.Available, ResponseStatus = BiometricAuthStatus.Success }; var result = await mock.AuthenticateAsync("테스트 인증"); Assert.True(result.IsSuccess); Assert.Equal(BiometricAuthStatus.Success, result.Status); Assert.Equal(1, mock.AuthenticateCallCount); Assert.Equal("테스트 인증", mock.LastReasonPrompt); // 취소 시뮬레이션 mock.ResponseStatus = BiometricAuthStatus.Canceled; var cancelResult = await mock.AuthenticateAsync("취소 테스트"); Assert.False(cancelResult.IsSuccess); Assert.Equal(BiometricAuthStatus.Canceled, cancelResult.Status); // 하드웨어 부재 시뮬레이션 mock.Availability = BiometricAvailability.DeviceNotPresent; var noHwResult = await mock.AuthenticateAsync("하드웨어 부재 테스트"); Assert.False(noHwResult.IsSuccess); Assert.Equal(BiometricAuthStatus.HardwareUnavailable, noHwResult.Status); } [Fact] public async Task MockBiometricAuthenticator_연속_실패_후_성공_시뮬레이션() { var mock = new MockBiometricAuthenticator { Availability = BiometricAvailability.Available, FailuresBeforeSuccess = 2 // 2회 실패 후 3회차에 성공 }; var res1 = await mock.AuthenticateAsync("1차 시도"); Assert.False(res1.IsSuccess); Assert.Equal(BiometricAuthStatus.BiometricsFailed, res1.Status); var res2 = await mock.AuthenticateAsync("2차 시도"); Assert.False(res2.IsSuccess); Assert.Equal(BiometricAuthStatus.BiometricsFailed, res2.Status); var res3 = await mock.AuthenticateAsync("3차 시도"); Assert.True(res3.IsSuccess); Assert.Equal(BiometricAuthStatus.Success, res3.Status); } #endregion #region 2. Password Generation & Strength Evaluation Tests [Theory] [InlineData(8)] [InlineData(16)] [InlineData(32)] [InlineData(64)] public void PasswordSecurityEngine_지정_길이_암호_생성_검증(int length) { var pass = PasswordSecurityEngine.GeneratePassword(new PasswordGeneratorOptions { Length = length, IncludeUppercase = true, IncludeLowercase = true, IncludeDigits = true, IncludeSymbols = true, ExcludeAmbiguousCharacters = true }); Assert.Equal(length, pass.Length); Assert.Contains(pass, ch => char.IsUpper(ch)); Assert.Contains(pass, ch => char.IsLower(ch)); Assert.Contains(pass, ch => char.IsDigit(ch)); Assert.Contains(pass, ch => !char.IsLetterOrDigit(ch)); // 모호한 문자 제외 확인 Assert.DoesNotContain('0', pass); Assert.DoesNotContain('O', pass); Assert.DoesNotContain('1', pass); Assert.DoesNotContain('l', pass); Assert.DoesNotContain('I', pass); } [Fact] public void PasswordSecurityEngine_Passphrase_단어_기반_기억하기_쉬운_암호_생성() { var pass = PasswordSecurityEngine.GeneratePassword(new PasswordGeneratorOptions { IsPassphraseMode = true, PassphraseWordCount = 4, PassphraseSeparator = "-", IncludeUppercase = true, IncludeDigits = true }); var parts = pass.Split('-'); Assert.True(parts.Length >= 4); foreach (var word in parts.Take(4)) { Assert.True(char.IsUpper(word[0])); // 첫 글자 대문자 } } [Theory] [InlineData("123456", PasswordGrade.VeryWeak, true)] [InlineData("password", PasswordGrade.VeryWeak, true)] [InlineData("admin", PasswordGrade.VeryWeak, true)] [InlineData("abc123", PasswordGrade.VeryWeak, false)] [InlineData("Abc123xyz!", PasswordGrade.Fair, false)] [InlineData("PacaSecure#2026!Quantum", PasswordGrade.Excellent, false)] public void PasswordSecurityEngine_암호_강도_및_패턴_분석_검증(string password, PasswordGrade expectedMinGrade, bool expectedCommon) { var result = PasswordSecurityEngine.EvaluateStrength(password); Assert.True(result.Score >= 0 && result.Score <= 100); Assert.True(result.EntropyBits >= 0); Assert.Equal(expectedCommon, result.IsCommonPattern); Assert.True(result.Grade >= expectedMinGrade); } #endregion #region 3. 2FA TOTP RFC 6238 Tests [Fact] public void TotpEngine_RFC6238_표준_벡터_및_카운트다운_검증() { // RFC 6238 공식 Secret 테스트 벡터: "JBSWY3DPEHPK3PXP" string secret = "JBSWY3DPEHPK3PXP"; var testTime = new DateTime(2026, 8, 31, 12, 0, 0, DateTimeKind.Utc); string code = TotpEngine.GenerateCurrentCode(secret, timeStepSeconds: 30, digits: 6, customTime: testTime); Assert.Equal(6, code.Length); Assert.True(int.TryParse(code, out _)); int remaining = TotpEngine.GetRemainingSeconds(30, testTime); Assert.True(remaining >= 1 && remaining <= 30); // 시간 표류 허용 검증 (±1 스텝) Assert.True(TotpEngine.ValidateCode(secret, code, allowedDriftSteps: 1, customTime: testTime)); Assert.True(TotpEngine.ValidateCode(secret, code, allowedDriftSteps: 1, customTime: testTime.AddSeconds(20))); Assert.True(TotpEngine.ValidateCode(secret, code, allowedDriftSteps: 1, customTime: testTime.AddSeconds(-20))); Assert.False(TotpEngine.ValidateCode(secret, "999999", allowedDriftSteps: 1, customTime: testTime)); } #endregion #region 4. URL & Domain Normalization and Federation Matching Tests [Theory] [InlineData("https://www.youtube.com/watch?v=123", "youtube.com")] [InlineData("http://m.facebook.com:8080/login", "m.facebook.com")] [InlineData("subdomain.example.co.kr/path/to/page", "subdomain.example.co.kr")] [InlineData("https://accounts.google.com/signin", "accounts.google.com")] public void SiteCredentialRecord_도메인_정규화_검증(string inputUrl, string expectedDomain) { var domain = SiteCredentialRecord.NormalizeDomain(inputUrl); Assert.Equal(expectedDomain, domain); } [Fact] public void SiteCredentialRecord_서브도메인_및_페더레이션_도메인_매칭_검증() { var record = new SiteCredentialRecord { Domain = "google.com", Url = "https://google.com", Username = "user@gmail.com" }; // 서브도메인 매칭 Assert.True(record.MatchesDomainOrUrl("https://accounts.google.com/")); Assert.True(record.MatchesDomainOrUrl("mail.google.com")); // 페더레이션 매칭 (YouTube <-> Google) Assert.True(record.MatchesDomainOrUrl("https://www.youtube.com/")); // 무관한 도메인 불일치 Assert.False(record.MatchesDomainOrUrl("https://naver.com/")); } #endregion #region 5. Security Audit Engine Tests [Fact] public void CredentialSecurityAuditor_취약점_재사용_오래된암호_종합검사() { var records = new List { new() { Id = "1", Domain = "siteA.com", Username = "user1", DecryptedPassword = "password123", LastModifiedAt = DateTime.UtcNow }, new() { Id = "2", Domain = "siteB.com", Username = "user2", DecryptedPassword = "password123", LastModifiedAt = DateTime.UtcNow }, // 재사용 & 취약 new() { Id = "3", Domain = "bank.com", Category = "Banking", Username = "bankuser", DecryptedPassword = "Complex#Password!2026", LastModifiedAt = DateTime.UtcNow.AddDays(-120), TotpSecret = null }, // 오래됨 & 2FA 누락 new() { Id = "4", Domain = "secure.dev", Username = "dev", DecryptedPassword = "Ultra#Strong!Quantum$2026", LastModifiedAt = DateTime.UtcNow, TotpSecret = "JBSWY3DPEHPK3PXP" } }; var report = CredentialSecurityAuditor.PerformAudit(records, oldPasswordDays: 90); Assert.Equal(4, report.TotalAccounts); Assert.Equal(2, report.ReusedPasswordCount); Assert.True(report.WeakPasswordCount >= 2); Assert.Equal(1, report.OldPasswordCount); Assert.Equal(1, report.Missing2FaCount); Assert.True(report.OverallHealthScore < 100); Assert.NotEmpty(report.Issues); } #endregion #region 6. Encrypted Vault and Data Exchange Tests [Fact] public void CredentialDataExchange_마스터비밀번호_암호화백업_및_복원_라운드트립() { var originalRecords = new List { new() { Domain = "youtube.com", Url = "https://youtube.com", Username = "creator", DecryptedPassword = "SecretPass123!", SiteName = "YouTube", Category = "Streaming", TotpSecret = "JBSWY3DPEHPK3PXP" }, new() { Domain = "github.com", Url = "https://github.com", Username = "octocat", DecryptedPassword = "GithubPass#2026", SiteName = "GitHub", Category = "Work" } }; string masterPassword = "MyMasterVaultPassword#2026"; byte[] backupData = CredentialDataExchange.ExportEncryptedVault(originalRecords, masterPassword); Assert.NotNull(backupData); Assert.True(backupData.Length > 64); // 복원 검증 var restored = CredentialDataExchange.ImportEncryptedVault(backupData, masterPassword); Assert.Equal(2, restored.Count); Assert.Equal("youtube.com", restored[0].Domain); Assert.Equal("creator", restored[0].Username); Assert.Equal("SecretPass123!", restored[0].DecryptedPassword); Assert.Equal("JBSWY3DPEHPK3PXP", restored[0].TotpSecret); // 잘못된 마스터 비밀번호 복원 시 CryptographicException 발생 확인 Assert.Throws(() => { CredentialDataExchange.ImportEncryptedVault(backupData, "WrongPassword"); }); } [Fact] public void CredentialDataExchange_CSV_내보내기_및_가져오기_라운드트립() { var records = new List { new() { SiteName = "Netflix, Korea", Domain = "netflix.com", Url = "https://netflix.com", Username = "viewer@netflix.com", DecryptedPassword = "Pass\"With\"Quotes", Notes = "Line1\nLine2", Category = "Streaming" } }; string csv = CredentialDataExchange.ExportCsv(records); Assert.Contains("name,url,username,password,note,totp,category", csv); var imported = CredentialDataExchange.ImportCsv(csv); Assert.Single(imported); Assert.Equal("Netflix, Korea", imported[0].SiteName); Assert.Equal("viewer@netflix.com", imported[0].Username); Assert.Equal("Pass\"With\"Quotes", imported[0].DecryptedPassword); } #endregion #region 7. OsProtectedCredentialVault Lifecycle & Persistence Tests [Fact] public async Task OsProtectedCredentialVault_잠금_잠금해제_CRUD_영속화_제로화_검증() { string vaultPath = Path.Combine(_testDir, "test_vault.pacavault"); var mockAuth = new MockBiometricAuthenticator(); try { using (var vault = new OsProtectedCredentialVault(vaultPath, mockAuth)) { Assert.True(vault.IsLocked); // 잠긴 상태에서 추가 시도 시 InvalidOperationException Assert.Throws(() => { vault.AddOrUpdate(new SiteCredentialRecord { Domain = "naver.com", Username = "user", DecryptedPassword = "123" }); }); // 잠금 해제 var unlock = await vault.UnlockWithBiometricAsync(); Assert.True(unlock.IsSuccess); Assert.False(vault.IsLocked); // 계정 추가 var rec = new SiteCredentialRecord { Domain = "naver.com", Url = "https://naver.com", Username = "paca_naver", DecryptedPassword = "NaverPass#2026!", SiteName = "네이버" }; vault.AddOrUpdate(rec); Assert.Equal(1, vault.RecordCount); var found = vault.FindMatching("https://nid.naver.com/"); Assert.Single(found); Assert.Equal("NaverPass#2026!", found[0].DecryptedPassword); // 잠금 시 메모리 제로화 확인 vault.Lock(); Assert.True(vault.IsLocked); var lockedRecs = vault.GetAll(); Assert.Single(lockedRecs); Assert.Equal("", lockedRecs[0].DecryptedPassword); // 평문 암호 지워짐 } // 파일에서 다시 로드 후 재잠금해제 확인 using (var vault2 = new OsProtectedCredentialVault(vaultPath, mockAuth)) { Assert.True(vault2.IsLocked); Assert.Equal(1, vault2.RecordCount); await vault2.UnlockWithBiometricAsync(); Assert.False(vault2.IsLocked); var rec = vault2.GetAll()[0]; Assert.Equal("NaverPass#2026!", rec.DecryptedPassword); Assert.Equal("paca_naver", rec.Username); // 삭제 검증 Assert.True(vault2.Remove(rec.Id)); Assert.Equal(0, vault2.RecordCount); } } finally { Cleanup(); } } [Fact] public async Task OsProtectedCredentialVault_연속실패_임시잠금_레이트리밋_보호() { string vaultPath = Path.Combine(_testDir, "lockout_vault.pacavault"); var mockAuth = new MockBiometricAuthenticator { ResponseStatus = BiometricAuthStatus.BiometricsFailed }; var policy = new BiometricSecurityPolicy { MaxFailedAttemptsBeforeLockout = 3, LockoutDurationSeconds = 10 }; try { using var vault = new OsProtectedCredentialVault(vaultPath, mockAuth, policy); // 3회 실패 await vault.UnlockWithBiometricAsync(); await vault.UnlockWithBiometricAsync(); var res3 = await vault.UnlockWithBiometricAsync(); // 4회차 시도 시 즉시 DeviceLocked 반환 확인 var res4 = await vault.UnlockWithBiometricAsync(); Assert.False(res4.IsSuccess); Assert.Equal(BiometricAuthStatus.DeviceLocked, res4.Status); } finally { Cleanup(); } } #endregion #region 8. In-Page Autofill & Browser Capture Script Tests [Fact] public void CredentialAutofillEngine_자동완성_JS_및_캡처_스크립트_생성_검증() { string autofillScript = CredentialAutofillEngine.GenerateAutofillScript( "paca_user", "P@ssw0rd!2026", "123456"); Assert.Contains("paca_user", autofillScript); Assert.Contains("P@ssw0rd!2026", autofillScript); Assert.Contains("123456", autofillScript); Assert.Contains("input[type=\"password\"]", autofillScript); string watcherScript = CredentialAutofillEngine.GenerateCredentialCaptureWatcherScript(); Assert.Contains("__paca_credential_watcher_installed", watcherScript); Assert.Contains("paca_captured_credential", watcherScript); } [Fact] public void CredentialAutofillEngine_웹뷰_캡처_메시지_파싱_검증() { string jsonMsg = """ { "type": "paca_captured_credential", "url": "https://www.netflix.com/login", "username": "user@netflix.com", "password": "SecretNetflixPass#1" } """; var captured = CredentialAutofillEngine.TryParseCapturedMessage(jsonMsg); Assert.NotNull(captured); Assert.Equal("netflix.com", captured.Domain); Assert.Equal("user@netflix.com", captured.Username); Assert.Equal("SecretNetflixPass#1", captured.Password); } [Fact] public async Task CredentialAutofillEngine_EvaluateSaveOrUpdate_정확한_상태_전이_검증() { string tempDir = Path.Combine(Path.GetTempPath(), "paca_eval_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { var auth = new MockBiometricAuthenticator(); var vault = new OsProtectedCredentialVault(Path.Combine(tempDir, "eval.pacavault"), auth); await vault.UnlockWithBiometricAsync(); var rec = new SiteCredentialRecord { Url = "https://github.com/login", Domain = "github.com", Username = "developer@github.com", DecryptedPassword = "OldPassword123!" }; vault.AddOrUpdate(rec); // 1. 신규 계정 캡처 -> SaveNew var newCap = new CapturedFormCredential("https://github.com/login", "github.com", "otheruser@github.com", "NewPass999!", DateTime.UtcNow); var evalNew = CredentialAutofillEngine.EvaluateSaveOrUpdateAction(vault, newCap); Assert.Equal(CredentialAutofillEngine.CredentialActionType.SaveNew, evalNew.Action); Assert.Null(evalNew.ExistingRecord); // 2. 동일 비밀번호 캡처 -> None var sameCap = new CapturedFormCredential("https://github.com/login", "github.com", "developer@github.com", "OldPassword123!", DateTime.UtcNow); var evalSame = CredentialAutofillEngine.EvaluateSaveOrUpdateAction(vault, sameCap); Assert.Equal(CredentialAutofillEngine.CredentialActionType.None, evalSame.Action); Assert.NotNull(evalSame.ExistingRecord); // 3. 변경된 비밀번호 캡처 -> UpdateExisting var updateCap = new CapturedFormCredential("https://github.com/login", "github.com", "developer@github.com", "BrandNewPassword2026#", DateTime.UtcNow); var evalUpdate = CredentialAutofillEngine.EvaluateSaveOrUpdateAction(vault, updateCap); Assert.Equal(CredentialAutofillEngine.CredentialActionType.UpdateExisting, evalUpdate.Action); Assert.NotNull(evalUpdate.ExistingRecord); Assert.Equal("developer@github.com", evalUpdate.ExistingRecord.Username); } finally { try { Directory.Delete(tempDir, true); } catch { } } } [Fact] public void CredentialAutofillEngine_GenerateInlineSuggestionScript_DOM과_이벤트_바인딩_생성_검증() { var records = new List { new() { Id = "rec1", Url = "https://naver.com", Domain = "naver.com", SiteName = "네이버", Username = "naver_user", DecryptedPassword = "NaverPass#1" } }; string script = CredentialAutofillEngine.GenerateInlineSuggestionScript(records); Assert.Contains("__paca_autofill_popup", script); Assert.Contains("naver_user", script); Assert.Contains("Paca 자격증명 금고", script); Assert.Contains("triggerInputEvents", script); Assert.Contains("focusin", script); } #endregion } /// /// 52차: 100+ 극한 시나리오 다각도 E2E 스트레스 & 카오스 장애 주입 & 동시성 무결성 전수 검증. /// public class Milestone52CredentialVaultStressAndChaosTests { private readonly string _stressDir; public Milestone52CredentialVaultStressAndChaosTests() { _stressDir = Path.Combine(Path.GetTempPath(), "paca_vault_stress_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_stressDir); } private void Cleanup() { try { if (Directory.Exists(_stressDir)) Directory.Delete(_stressDir, true); } catch { } } #region 1. 100 Concurrent Multi-Threaded Stress Test (Scenarios 1-100) [Fact] public async Task Scenario01_100_병렬_스레드_동시_금고_조회_수정_삭제_스트레스_검증() { string vaultPath = Path.Combine(_stressDir, "concurrent_vault.pacavault"); var mockAuth = new MockBiometricAuthenticator(); try { using var vault = new OsProtectedCredentialVault(vaultPath, mockAuth); var unlockResult = await vault.UnlockWithBiometricAsync(); Assert.True(unlockResult.IsSuccess); // 초기 10개 계정 시드 for (int i = 0; i < 10; i++) { vault.AddOrUpdate(new SiteCredentialRecord { Domain = $"service{i}.com", Url = $"https://service{i}.com", Username = $"user{i}@paca.dev", DecryptedPassword = $"P@ssword_{i}_Secret!", SiteName = $"Service {i}" }); } // 100개 병렬 작업 동시 실행 (Read, Search, Add, Update, Remove) var tasks = new List(); for (int i = 0; i < 100; i++) { int workerId = i; tasks.Add(Task.Run(() => { int op = workerId % 5; switch (op) { case 0: // Add vault.AddOrUpdate(new SiteCredentialRecord { Domain = $"worker{workerId}.com", Url = $"https://worker{workerId}.com/login", Username = $"worker_user_{workerId}", DecryptedPassword = $"Pass_{workerId}_{Guid.NewGuid():N}", SiteName = $"Worker Site {workerId}" }); break; case 1: // Search var found = vault.Search($"service"); Assert.NotNull(found); break; case 2: // FindMatching var matched = vault.FindMatching($"https://service{workerId % 10}.com/account"); Assert.NotNull(matched); break; case 3: // GetAll & Password Check var all = vault.GetAll(); Assert.NotNull(all); foreach (var item in all) { Assert.False(string.IsNullOrEmpty(item.Username)); } break; case 4: // Update existing var itemToUpdate = vault.FindMatching($"service{workerId % 10}.com").FirstOrDefault(); if (itemToUpdate != null) { itemToUpdate.Notes = $"Updated by worker {workerId} at {DateTime.UtcNow.Ticks}"; vault.AddOrUpdate(itemToUpdate); } break; } })); } await Task.WhenAll(tasks); // 동시성 작업 완료 후 금고 무결성 검증 Assert.False(vault.IsLocked); Assert.True(vault.RecordCount >= 10); var finalRecords = vault.GetAll(); Assert.NotEmpty(finalRecords); // 파일 재로드 무결성 검증 vault.Lock(); Assert.True(vault.IsLocked); } finally { Cleanup(); } } #endregion #region 2. 100 Chaos File Corruption & Bit-Flip Fault Injection Tests (Scenarios 101-200) [Fact] public async Task Scenario02_100회_무작위_파일_손상_비트플립_주입_시_자가치유_백업_복구_검증() { string vaultPath = Path.Combine(_stressDir, "chaos_vault.pacavault"); string backupPath = vaultPath + ".bak"; var mockAuth = new MockBiometricAuthenticator(); try { // 1. 정상 데이터 작성 및 백업 생성 using (var vault = new OsProtectedCredentialVault(vaultPath, mockAuth)) { await vault.UnlockWithBiometricAsync(); for (int i = 0; i < 20; i++) { vault.AddOrUpdate(new SiteCredentialRecord { Domain = $"healthy{i}.org", Url = $"https://healthy{i}.org", Username = $"healthy_user_{i}", DecryptedPassword = $"HealthyP@ss_{i}", SiteName = $"Healthy Site {i}" }); } } Assert.True(File.Exists(vaultPath)); Assert.True(File.Exists(backupPath)); // 2. 100가지 다양한 손상 케이스 주입 및 자가치유 복구 검증 var rng = new Random(42); for (int testCase = 0; testCase < 100; testCase++) { // 백업 파일 기준으로 손상 데이터 생성하여 원본 파일에 덮어쓰기 byte[] corruptedData = File.ReadAllBytes(backupPath); int mode = testCase % 5; switch (mode) { case 0: // 0바이트 파일 File.WriteAllBytes(vaultPath, Array.Empty()); break; case 1: // 매직 헤더 파괴 corruptedData[0] = (byte)'X'; corruptedData[1] = (byte)'X'; File.WriteAllBytes(vaultPath, corruptedData); break; case 2: // 페이로드 길이 음수 또는 거대 크기 변조 BitConverter.GetBytes(-9999).CopyTo(corruptedData, 12); File.WriteAllBytes(vaultPath, corruptedData); break; case 3: // 무작위 바이트 비트 플립 (Fuzzing) int offset = rng.Next(0, corruptedData.Length); corruptedData[offset] ^= 0xFF; File.WriteAllBytes(vaultPath, corruptedData); break; case 4: // 파일 절반 절단 (Truncation) int cutLen = Math.Max(1, corruptedData.Length / 2); byte[] cutData = new byte[cutLen]; Buffer.BlockCopy(corruptedData, 0, cutData, 0, cutLen); File.WriteAllBytes(vaultPath, cutData); break; } // 손상된 상태에서 금고 로드 시도시 .bak에서 자가치유 복구 확인 (백업 스냅샷 19개 이상 복원) using (var healedVault = new OsProtectedCredentialVault(vaultPath, mockAuth)) { Assert.True(healedVault.RecordCount >= 19, $"자가치유 복구 레코드 수({healedVault.RecordCount})가 유효해야 합니다."); await healedVault.UnlockWithBiometricAsync(); var item = healedVault.FindMatching("healthy0.org").FirstOrDefault(); Assert.NotNull(item); Assert.Equal("HealthyP@ss_0", item.DecryptedPassword); } } } finally { Cleanup(); } } #endregion #region 3. 100 Biometric Hardware State & Error Variations (Scenarios 201-300) [Fact] public async Task Scenario03_100회_생체인증_하드웨어_상태_전이_및_잠금정책_검증() { string vaultPath = Path.Combine(_stressDir, "bio_states_vault.pacavault"); var mockAuth = new MockBiometricAuthenticator(); var policy = new BiometricSecurityPolicy { MaxFailedAttemptsBeforeLockout = 5, LockoutDurationSeconds = 0, // 즉시 복구 AutoLockMinutes = 15 }; try { using var vault = new OsProtectedCredentialVault(vaultPath, mockAuth, policy); // 100회 다양한 상태 전이 테스트 for (int i = 0; i < 100; i++) { int stateMode = i % 8; switch (stateMode) { case 0: // 성공 await Task.Delay(70); // 잠금 해제 대기 (50ms 초과) mockAuth.Availability = BiometricAvailability.Available; mockAuth.ResponseStatus = BiometricAuthStatus.Success; var sRes = await vault.UnlockWithBiometricAsync(); Assert.True(sRes.IsSuccess); Assert.False(vault.IsLocked); vault.Lock(); break; case 1: // 사용자 대체 인증(PIN) 성공 await Task.Delay(70); mockAuth.Availability = BiometricAvailability.Available; mockAuth.ResponseStatus = BiometricAuthStatus.UserFallback; var fbRes = await vault.UnlockWithBiometricAsync(); Assert.True(fbRes.IsSuccess); Assert.False(vault.IsLocked); vault.Lock(); break; case 2: // 사용자 취소 mockAuth.Availability = BiometricAvailability.Available; mockAuth.ResponseStatus = BiometricAuthStatus.Canceled; var cRes = await vault.UnlockWithBiometricAsync(); Assert.False(cRes.IsSuccess); Assert.True(vault.IsLocked); break; case 3: // 타임아웃 mockAuth.Availability = BiometricAvailability.Available; mockAuth.ResponseStatus = BiometricAuthStatus.Timeout; var tRes = await vault.UnlockWithBiometricAsync(); Assert.False(tRes.IsSuccess); Assert.True(vault.IsLocked); break; case 4: // 하드웨어 부재 mockAuth.Availability = BiometricAvailability.DeviceNotPresent; var hwRes = await vault.UnlockWithBiometricAsync(); Assert.False(hwRes.IsSuccess); break; case 5: // 지문 미등록 mockAuth.Availability = BiometricAvailability.NotConfigured; var ncRes = await vault.UnlockWithBiometricAsync(); Assert.False(ncRes.IsSuccess); break; case 6: // 지문 불일치 mockAuth.Availability = BiometricAvailability.Available; mockAuth.ResponseStatus = BiometricAuthStatus.BiometricsFailed; var failRes = await vault.UnlockWithBiometricAsync(); Assert.False(failRes.IsSuccess); break; case 7: // 잠금 시간 후 복구 await Task.Delay(1050); // 1초 잠금 초과 대기 mockAuth.Availability = BiometricAvailability.Available; mockAuth.ResponseStatus = BiometricAuthStatus.Success; var recRes = await vault.UnlockWithBiometricAsync(); Assert.True(recRes.IsSuccess); vault.Lock(); break; } } } finally { Cleanup(); } } #endregion #region 4. 100 Password Security, Entropy & Fuzzing Edge Cases (Scenarios 301-400) [Fact] public void Scenario04_100가지_특수문자_유니코드_이모지_SQL인젝션_암호강도_엔트로피_퍼징() { string[] fuzzPasswords = new[] { "한글비밀번호!2026", "パスワード!12345", "密码#Secure999", "Пароль_123456!", "😀🚀🛡️🔥🎉Key!99", "' OR '1'='1' --", "", "admin\0nullbyte", new string('A', 1024), new string('!', 512), " leading and trailing spaces ", "A1!", "Paca-Quantum-Matrix-Titan-2026#", "\u200B\u200C\u200DZeroWidthSecret123!", "1234567890abcdefghijklmnopqrstuvwxyz!@#$%^&*()_+" }; for (int i = 0; i < 100; i++) { string pass = fuzzPasswords[i % fuzzPasswords.Length] + "_" + i; var eval = PasswordSecurityEngine.EvaluateStrength(pass); Assert.NotNull(eval); Assert.True(eval.Score >= 0 && eval.Score <= 100); Assert.True(eval.EntropyBits >= 0); Assert.NotNull(eval.Suggestions); // 임의 생성기 교차 검증 var gen = PasswordSecurityEngine.GeneratePassword(new PasswordGeneratorOptions { Length = (i % 60) + 8, IncludeSymbols = (i % 2 == 0), IncludeDigits = (i % 3 != 0) }); Assert.NotNull(gen); Assert.True(gen.Length >= 8); } } #endregion #region 5. 100 URL & Domain Federation Matching Edge Cases (Scenarios 401-500) [Fact] public void Scenario05_100가지_복합_도메인_서브도메인_포트_페더레이션_매칭_검증() { var baseRecord = new SiteCredentialRecord { Domain = "youtube.com", Url = "https://www.youtube.com", Username = "creator@gmail.com" }; string[] testUrls = new[] { "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "https://m.youtube.com/feed/subscriptions", "http://youtube.com:443/login", "https://accounts.google.com/signin/v2/identifier", "https://google.com/service/login", "https://YOUTUBE.COM/channel/UC123", "https://studio.youtube.com/video/edit", "https://music.youtube.com/playlist" }; for (int i = 0; i < 100; i++) { string url = testUrls[i % testUrls.Length]; Assert.True(baseRecord.MatchesDomainOrUrl(url), $"URL '{url}' 매칭 실패"); Assert.False(baseRecord.MatchesDomainOrUrl("https://apple.com/kr/store")); Assert.False(baseRecord.MatchesDomainOrUrl("https://naver.com/login")); } } #endregion #region 6. 100 CSV & Encrypted Vault Import/Export Fuzzing (Scenarios 501-600) [Fact] public void Scenario06_100회_암호화금고_및_CSV_대량데이터_라운드트립_무결성_검증() { for (int i = 0; i < 100; i++) { var records = new List(); int count = (i % 20) + 1; for (int k = 0; k < count; k++) { records.Add(new SiteCredentialRecord { Domain = $"test{i}_{k}.com", Url = $"https://test{i}_{k}.com/login", Username = $"user_{i}_{k}@test.com", DecryptedPassword = $"P@ss_{i}_{k}_\"Quotes\"_{Guid.NewGuid():N}", SiteName = $"Site, Name with comma {k}", Notes = $"Note line 1\r\nLine 2 {k}", Category = k % 2 == 0 ? "Streaming" : "Social", TotpSecret = k % 3 == 0 ? "JBSWY3DPEHPK3PXP" : null }); } // 1. Encrypted Vault Roundtrip string masterKey = $"MasterPassword_{i}_!@#"; byte[] exportedEncrypted = CredentialDataExchange.ExportEncryptedVault(records, masterKey); var importedEncrypted = CredentialDataExchange.ImportEncryptedVault(exportedEncrypted, masterKey); Assert.Equal(count, importedEncrypted.Count); Assert.Equal(records[0].Domain, importedEncrypted[0].Domain); Assert.Equal(records[0].DecryptedPassword, importedEncrypted[0].DecryptedPassword); // 2. CSV Roundtrip string csv = CredentialDataExchange.ExportCsv(records); var importedCsv = CredentialDataExchange.ImportCsv(csv); Assert.Equal(count, importedCsv.Count); Assert.Equal(records[0].Username, importedCsv[0].Username); Assert.Equal(records[0].DecryptedPassword, importedCsv[0].DecryptedPassword); } } #endregion #region 7. 100 TOTP Drift & Time-Window Variations (Scenarios 601-700) [Fact] public void Scenario07_100가지_TOTP_시간표류_및_경계값_토큰_유효성_검증() { string base32Secret = "JBSWY3DPEHPK3PXP"; var baseTime = new DateTime(2026, 8, 31, 0, 0, 0, DateTimeKind.Utc); for (int i = 0; i < 100; i++) { var currentTime = baseTime.AddSeconds(i * 17); // 17초씩 전진 string token = TotpEngine.GenerateCurrentCode(base32Secret, 30, 6, currentTime); Assert.Equal(6, token.Length); // 현재 윈도우 유효성 확인 Assert.True(TotpEngine.ValidateCode(base32Secret, token, allowedDriftSteps: 1, 30, currentTime)); // 경계 오프셋 검증 (±25초 이내는 ±1 스텝 허용 범위 내) Assert.True(TotpEngine.ValidateCode(base32Secret, token, allowedDriftSteps: 1, 30, currentTime.AddSeconds(15))); Assert.True(TotpEngine.ValidateCode(base32Secret, token, allowedDriftSteps: 1, 30, currentTime.AddSeconds(-15))); // 90초 이상 벗어나면 무효화 Assert.False(TotpEngine.ValidateCode(base32Secret, token, allowedDriftSteps: 1, 30, currentTime.AddSeconds(120))); } } #endregion #region 8. End-to-End Complete User Journey Lifecycle Verification [Fact] public async Task Scenario08_사용자_전체_생애주기_E2E_통합_검증() { string vaultPath = Path.Combine(_stressDir, "e2e_journey_vault.pacavault"); var mockAuth = new MockBiometricAuthenticator(); try { // 1. 금고 생성 및 잠금 상태 확인 using var vault = new OsProtectedCredentialVault(vaultPath, mockAuth); Assert.True(vault.IsLocked); // 2. Windows Hello 잠금 해제 var unlock = await vault.UnlockWithBiometricAsync("E2E 테스트 잠금 해제"); Assert.True(unlock.IsSuccess); Assert.False(vault.IsLocked); // 3. 브라우저에서 넷플릭스 / 유튜브 / 네이버 로그인 계정 캡처 시뮬레이션 string capturedMsg = """ { "type": "paca_captured_credential", "url": "https://www.netflix.com/kr/login", "username": "paca_user@netflix.com", "password": "UltraSecure#Netflix2026!" } """; var captured = CredentialAutofillEngine.TryParseCapturedMessage(capturedMsg); Assert.NotNull(captured); var netflixRec = new SiteCredentialRecord { Domain = captured.Domain, Url = captured.Url, Username = captured.Username, DecryptedPassword = captured.Password, SiteName = "Netflix Korea", Category = "Streaming", TotpSecret = "JBSWY3DPEHPK3PXP" }; vault.AddOrUpdate(netflixRec); // 4. 유튜브 계정 추가 var youtubeRec = new SiteCredentialRecord { Domain = "youtube.com", Url = "https://youtube.com", Username = "creator@youtube.com", DecryptedPassword = "Creator#Password!2026", SiteName = "YouTube", Category = "Streaming" }; vault.AddOrUpdate(youtubeRec); // 5. 보안 감사 실행 var audit = CredentialSecurityAuditor.PerformAudit(vault.GetAll()); Assert.Equal(2, audit.TotalAccounts); Assert.Equal(100, audit.OverallHealthScore); // 강력한 암호로 100점 만점 Assert.Equal(0, audit.WeakPasswordCount); Assert.Equal(0, audit.ReusedPasswordCount); // 6. 브라우저 탭 진입 시 자동완성 스크립트 생성 및 적용 확인 var matchedCredentials = CredentialAutofillEngine.FindApplicableCredentials(vault, "https://netflix.com/login"); Assert.Single(matchedCredentials); Assert.Equal("paca_user@netflix.com", matchedCredentials[0].Username); string script = CredentialAutofillEngine.GenerateAutofillScript( matchedCredentials[0].Username, matchedCredentials[0].DecryptedPassword, TotpEngine.GenerateCurrentCode(matchedCredentials[0].TotpSecret!)); Assert.Contains("paca_user@netflix.com", script); Assert.Contains("UltraSecure#Netflix2026!", script); // 7. ViewModel 바인딩 및 클립보드 복사 시뮬레이션 var vm = new CredentialVaultViewModel(vault); vm.SelectedCredential = netflixRec; string copiedText = ""; vm.CopyPasswordToClipboard( pass => copiedText = pass, () => { copiedText = ""; }); Assert.Equal("UltraSecure#Netflix2026!", copiedText); Assert.True(vm.ClipboardSecondsRemaining > 0); // 8. 금고 잠금 및 메모리 평문 암호 제로화 검증 vault.Lock(); Assert.True(vault.IsLocked); Assert.All(vault.GetAll(), r => Assert.Equal("", r.DecryptedPassword)); } finally { Cleanup(); } } #endregion } /// /// Phase 64: Zero-Knowledge CRDT Encrypted Vault & Cross-Cloud Sync 검증 테스트 스위트. /// public class ZeroKnowledgeCrdtVaultPhase64Tests { [Fact] public void CrdtVaultSyncEngine_다중_노드_무충돌_LWW_병합_검증() { var nodeA = new CrdtVaultSyncEngine("client-node-A"); var nodeB = new CrdtVaultSyncEngine("client-node-B"); // Node A adds item 1 var rec1 = nodeA.Set("item-1", "Value from Node A (v1)"); // Node B adds item 2 var rec2 = nodeB.Set("item-2", "Value from Node B"); // Node A merges state from Node B int appliedA = nodeA.Merge(nodeB.ExportState()); Assert.Equal(1, appliedA); Assert.Equal(2, nodeA.GetActiveItems().Count); // Concurrent update to item 1 on Node B with higher clock nodeB.Merge(nodeA.ExportState()); nodeB.Set("item-1", "Value from Node B (v2 overwritten)"); // Merge back to Node A nodeA.Merge(nodeB.ExportState()); var activeA = nodeA.GetActiveItems(); Assert.Contains("Value from Node B (v2 overwritten)", activeA); // Delete item 2 on Node A nodeA.Delete("item-2"); nodeB.Merge(nodeA.ExportState()); var activeB = nodeB.GetActiveItems(); Assert.Single(activeB); Assert.Equal("Value from Node B (v2 overwritten)", activeB[0]); } [Fact] public void E2eeRemoteBackupProvider_AES256GCM_암복호화_라운드트립_검증() { string plainSecret = "{\"bookmarks\": [\"https://youtube.com\"], \"masterKey\": \"super-secret-vault-key-2026\"}"; string masterPass = "MasterPacaPassword#2026!Strong"; string backupPayload = E2eeRemoteBackupProvider.EncryptPayload(plainSecret, masterPass); Assert.Contains("PACAVAULT", backupPayload); Assert.Contains("AES-256-GCM", backupPayload); string decrypted = E2eeRemoteBackupProvider.DecryptPayload(backupPayload, masterPass); Assert.Equal(plainSecret, decrypted); // Wrong password should throw CryptographicException Assert.ThrowsAny(() => E2eeRemoteBackupProvider.DecryptPayload(backupPayload, "WrongPassword123!")); } [Fact] public void HardwareKeyTokenAuthenticator_FIDO2_도출키_결정론적_생성_검증() { var challenge = HardwareKeyTokenAuthenticator.GenerateChallenge("paca.media"); Assert.NotNull(challenge.ChallengeBase64); Assert.Equal("paca.media", challenge.RpId); byte[] masterKey = System.Text.Encoding.UTF8.GetBytes("PacaPhysicalYubiKeyMasterSecret32B"); byte[] salt = System.Text.Encoding.UTF8.GetBytes("HardwareSalt16Bytes!"); byte[] derivedKey1 = HardwareKeyTokenAuthenticator.DeriveVaultKeyFromHmacSecret(masterKey, salt); byte[] derivedKey2 = HardwareKeyTokenAuthenticator.DeriveVaultKeyFromHmacSecret(masterKey, salt); Assert.Equal(32, derivedKey1.Length); Assert.Equal(derivedKey1, derivedKey2); } }