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);
}
}