using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; using QSafe.Core; using QSafe.Desktop; internal static class Program { private static int passed; private static void Assert(bool condition, string message = "Assertion failed") { if (!condition) throw new Exception(message); } private static void Reject(Action action) where T : Exception { try { action(); } catch (T) { return; } throw new Exception("Expected " + typeof(T).Name); } private static async Task RejectAsync(Func action) where T : Exception { try { await action(); } catch (T) { return; } throw new Exception("Expected " + typeof(T).Name); } private static void Test(string title, Action action) { action(); passed++; Console.WriteLine("PASS " + title); } private static async Task TestAsync(string title, Func action) { await action(); passed++; Console.WriteLine("PASS " + title); } [STAThread] private static int Main(string[] args) { try { if (args.FirstOrDefault() == "--lan-probe") { var config = Connection.Parse(File.ReadAllBytes(args[1])); var binding = HomeWifiNetwork.Instance.Current(config); LanPolicy.Check(config, binding); Console.WriteLine($"PASS home Wi-Fi confirmed: {binding.Ssid}, {binding.Address}/{binding.PrefixLength}, interface {binding.InterfaceIndex}"); using var client = new SyncClient(config, HomeWifiNetwork.Instance); var encrypted = client.Download().GetAwaiter().GetResult(); VaultCrypto.Validate(encrypted); Console.WriteLine($"PASS pinned LAN TLS and authenticated encrypted vault download ({encrypted.Length} bytes); no plaintext read, no writes"); return 0; } var artifact = Path.GetFullPath(args.FirstOrDefault() ?? "artifacts/windows-qa"); Directory.CreateDirectory(artifact); CoreTests(artifact).GetAwaiter().GetResult(); DesktopTests(artifact); Console.WriteLine($"RESULT {passed} checks passed"); return 0; } catch (Exception ex) { Console.Error.WriteLine(ex); return 1; } } private static async Task CoreTests(string artifact) { using var key = VaultCrypto.Create("Long synthetic test password 🔐"); var original = new VaultData(); var card = Templates.Create("login"); card.Title = "Test"; card.Fields[1].Value = "Secret"; original.Entries.Add(card); var blob = Vault.Encrypt(original, key); Test("AES-GCM roundtrip and random nonce", () => { Assert(Vault.Read(blob, key).Entries[0].Fields[1].Value == "Secret"); Assert(!blob.SequenceEqual(Vault.Encrypt(original, key))); }); Test("Wrong password and tampering rejected", () => { Reject(() => VaultCrypto.Unlock(blob, "wrong")); var corrupt = blob.ToArray(); corrupt[^1] ^= 1; Reject(() => Vault.Read(corrupt, key)); }); Test("Envelope bounds and iteration ceiling checked before KDF", () => { Reject(() => VaultCrypto.Validate(new byte[8])); var corrupt = blob.ToArray(); Array.Fill(corrupt, (byte)127, 8, 4); Reject(() => VaultCrypto.Unlock(corrupt, "wrong")); }); Test("Key disposal clears bytes", () => { var temporary = VaultCrypto.Create("test password"); temporary.Dispose(); Assert(temporary.Bytes.All(b => b == 0)); }); Test("Password generator character groups, options and bounds", () => { for (int i = 0; i < 150; i++) { var p = VaultCrypto.Generate(32); Assert(p.Length == 32 && p.Any(char.IsUpper) && p.Any(char.IsLower) && p.Any(char.IsDigit) && p.Any(c => !char.IsLetterOrDigit(c))); Assert(VaultCrypto.Generate(16, false).All(char.IsLetterOrDigit)); } Reject(() => VaultCrypto.Generate(8)); }); Test("Password modes enforce lengths, digits, words and symbol choice", () => { foreach (int size in new[] { 4, 12, 64, 128 }) for (int i = 0; i < 50; i++) { var p = PasswordGenerator.Generate(PasswordMode.Digits, size); Assert(p.Length == size && p.All(c => c is >= '0' and <= '9')); } foreach (int size in new[] { 2, 3, 8, 12 }) foreach (bool symbols in new[] { true, false }) foreach (bool digits in new[] { true, false }) for (int i = 0; i < 50; i++) { var p = PasswordGenerator.Generate(PasswordMode.Memorable, size, symbols, digits); Assert(Regex.Matches(p, "[A-Z]?[a-z]+").Count == size && p.Any(char.IsDigit) == digits && p.Any(c => !char.IsLetterOrDigit(c)) == symbols); Assert(WholeWords(p, size), "Words must stay whole"); if (digits) Assert(p.Count(char.IsDigit) == 2); } Assert(PasswordGenerator.Generate(PasswordMode.Complex, 16, false).All(char.IsLetterOrDigit)); foreach (var (mode, size) in new[] { (PasswordMode.Complex, 15), (PasswordMode.Complex, 129), (PasswordMode.Digits, 3), (PasswordMode.Digits, 129), (PasswordMode.Memorable, 1), (PasswordMode.Memorable, 13) }) Reject(() => PasswordGenerator.Generate(mode, size)); }); Test("Memorable digits and signs reach every word boundary in either order", () => { var digitPositions = new bool[3]; var signPositions = new bool[3]; bool signBeforeDigit = false, signAfterDigit = false; for (int i = 0; i < 1000; i++) { var p = PasswordGenerator.Generate(PasswordMode.Memorable, 2); var words = Regex.Matches(p, "[A-Z][a-z]+"); Assert(words.Count == 2 && WholeWords(p, 2)); string[] boundaries = [p[..words[0].Index], p[(words[0].Index + words[0].Length)..words[1].Index], p[(words[1].Index + words[1].Length)..]]; for (int slot = 0; slot < 3; slot++) { digitPositions[slot] |= boundaries[slot].Any(char.IsDigit); signPositions[slot] |= boundaries[slot].Any(c => !char.IsLetterOrDigit(c)); } int sign = p.IndexOfAny("!@#$%&*+-=?".ToCharArray()); for (int j = 0; j < p.Length; j++) if (char.IsDigit(p[j])) { signBeforeDigit |= sign < j; signAfterDigit |= sign > j; } } Assert(digitPositions.All(p => p) && signPositions.All(p => p) && signBeforeDigit && signAfterDigit); }); Test("Malformed vaults and duplicate entry IDs rejected", () => { Reject(() => Vault.Parse("{}"u8.ToArray())); var d = Vault.Copy(original); d.Entries.Add(Vault.Copy(card)); Reject(() => Vault.Parse(Vault.Bytes(d))); Reject(() => Vault.Parse("{\"format\":1,\"id\":\"x\",\"entries\":[{\"fields\":[]}]}"u8.ToArray())); Reject(() => Vault.Parse("{\"format\":1,\"id\":\"x\",\"entries\":[null]}"u8.ToArray())); }); Test("All eight templates use compatible secret types", () => { foreach (string kind in Templates.Kinds) { var e = Templates.Create(kind); Assert(e.Category == kind); Assert(e.Fields.All(f => f.Type != "password" || f.Secret)); } Assert(new Field { Type = "unknown" }.Secret); }); var local = Vault.Copy(original); var remote = Vault.Copy(original); Test("Independent edits merge", () => { var extra = Templates.Create("note"); Vault.Update(local, extra); var changed = Vault.Copy(card); changed.Notes = "remote"; Vault.Update(remote, changed); var m = Vault.Merge(original, local, remote); Assert(m.Conflicts == 0 && m.Data.Entries.Count == 2 && m.Data.Entries.Single(e => e.Id == card.Id).Notes == "remote"); }); Test("Concurrent edits preserve both revisions", () => { var a = Vault.Copy(original); var b = Vault.Copy(original); var ea = Vault.Copy(card); ea.Notes = "A"; Vault.Update(a, ea); var eb = Vault.Copy(card); eb.Notes = "B"; Vault.Update(b, eb); var m = Vault.Merge(original, a, b); Assert(m.Conflicts == 1 && m.Data.Entries.Count == 2 && m.Data.Entries.Select(e => e.Notes).Order().SequenceEqual(new[] { "A", "B" })); }); Test("Delete/edit conflict retains edited card", () => { var a = Vault.Copy(original); var b = Vault.Copy(original); var ea = Vault.Copy(card); ea.Deleted = true; Vault.Update(a, ea); var eb = Vault.Copy(card); eb.Notes = "keep"; Vault.Update(b, eb); var m = Vault.Merge(original, a, b); Assert(m.Conflicts == 1 && m.Data.Entries.Single().Notes == "keep" && !m.Data.Entries.Single().Deleted); }); Test("Different vaults cannot merge", () => Reject(() => Vault.Merge(null, original, new VaultData()))); Test("XML import retains all metadata and deduplicates", () => { var xml = Encoding.UTF8.GetBytes(""); var p = XmlImport.Preview(xml, new VaultData()); Assert(p.Added == 1 && p.Skipped == 2 && p.Attachments == 1 && p.Data.Entries[0].Fields[1].Value == "a&b" && p.Data.Entries[0].Tags.Single() == "Работа"); Assert(XmlImport.Preview(xml, p.Data).Duplicates == 1); p.Data.Entries[0].Extra!.Remove("safeInCloudFingerprint"); Assert(XmlImport.Preview(xml, p.Data).Duplicates == 1); var round = Vault.Read(Vault.Encrypt(p.Data, key), key); Assert(round.Entries[0].Extra!["safeInCloudXml"].GetString()!.Contains("")); }); Test("XML rejects XXE, DTD and foreign roots", () => { Reject(() => XmlImport.Preview(Encoding.UTF8.GetBytes("]>&x;"), original)); Reject(() => XmlImport.Preview(""u8.ToArray(), original)); }); Test("Connection rejects HTTP, userinfo, fragments and malformed tokens", () => { foreach (var url in new[] { "http://example.test/", "https://user@example.test/", "https://example.test/?x=1", "https://example.test/#fragment" }) Reject(() => new Connection { Url = url, Token = new string('a', 43) }.Validate()); Reject(() => Connection.Parse("{\"url\":null,\"token\":null}"u8.ToArray())); }); var config = TestConnection(); var home = new FakeHomeNetwork(); var handler = new FakeServer(); using var client = new SyncClient(config, home, handler); await TestAsync("Vault transport is never reached outside home Wi-Fi or local subnet", async () => { foreach (var binding in new[] { new LanBinding("Other Wi-Fi", true, IPAddress.Parse("192.168.0.61"), 24, 3), new LanBinding("gen", false, IPAddress.Parse("192.168.0.61"), 24, 3), new LanBinding("gen", true, IPAddress.Parse("192.168.1.61"), 24, 3) }) { home.Binding = binding; int before = handler.Requests; await RejectAsync(() => client.Download()); Assert(handler.Requests == before); } home.Binding = FakeHomeNetwork.Home; Assert(!LanPolicy.PrivateIpv4(IPAddress.Parse("8.8.8.8")) && !LanPolicy.PrivateIpv4(IPAddress.Parse("127.0.0.1")) && !LanPolicy.PrivateIpv4(IPAddress.Parse("169.254.1.1")) && !LanPolicy.PrivateIpv4(IPAddress.IPv6Loopback)); }); Test("Public vault URLs and hostnames rejected without DNS or transport", () => { foreach (var url in new[] { "https://argus.kusoft.xyz/qsafe/", "https://8.8.8.8/", "https://localhost/", "https://192.168.0.185.evil.test/", "http://192.168.0.185/", "https://192.168.0.185/?x=1" }) { var c = TestConnection(); c.Url = url; Reject(c.Validate); } var missing = TestConnection(); missing.HomeWifiSsid = ""; Reject(missing.Validate); missing = TestConnection(); missing.CertificateSha256 = ""; Reject(missing.Validate); }); await TestAsync("Sync creates remote using If-None-Match and ciphertext only", async () => { var result = await client.Sync(blob, null, key); Assert(handler.Blob!.SequenceEqual(blob) && handler.LastCondition == "*" && result.Data.Id == original.Id); }); config.Etag = VaultCrypto.Hash(blob); var changedLocal = Vault.Copy(original); var edit = Vault.Copy(card); edit.Notes = "local change"; Vault.Update(changedLocal, edit); var changedBlob = Vault.Encrypt(changedLocal, key); await TestAsync("Sync upload uses If-Match and confirmed SHA-256", async () => { await client.Sync(changedBlob, blob, key); Assert(handler.Blob!.SequenceEqual(changedBlob) && handler.LastCondition == '"' + VaultCrypto.Hash(blob) + '"'); }); config.Etag = VaultCrypto.Hash(changedBlob); await TestAsync("Sync remote-only change downloaded without upload", async () => { handler.Blob = blob; var puts = handler.Puts; var r = await client.Sync(changedBlob, changedBlob, key); Assert(r.Envelope.SequenceEqual(blob) && handler.Puts == puts); }); await TestAsync("Sync concurrent changes preserve both entries", async () => { var r = Vault.Copy(original); var e = Vault.Copy(card); e.Notes = "remote change"; Vault.Update(r, e); handler.Blob = Vault.Encrypt(r, key); config.Etag = VaultCrypto.Hash(blob); var merged = await client.Sync(changedBlob, blob, key); Assert(merged.Conflicts == 1 && merged.Data.Entries.Count == 2); }); await TestAsync("Sync detects disappeared remote and never recreates it", async () => { handler.Blob = null; var puts = handler.Puts; await RejectAsync(() => client.Sync(blob, blob, key)); Assert(handler.Puts == puts); }); await TestAsync("Sync rejects corrupted remote without uploading", async () => { handler.Blob = blob.ToArray(); handler.Blob[^1] ^= 1; var puts = handler.Puts; await RejectAsync(() => client.Sync(changedBlob, blob, key)); Assert(handler.Puts == puts); }); await TestAsync("Sync validates response checksum", async () => { handler.Blob = blob; handler.BadHash = true; await RejectAsync(() => client.Sync(blob, null, key)); handler.BadHash = false; }); await TestAsync("Sync retries conditional-write conflicts with fresh GET", async () => { handler.Blob = blob; config.Etag = VaultCrypto.Hash(blob); handler.ConflictOnce = true; var r = await client.Sync(changedBlob, blob, key); Assert(r.Envelope.SequenceEqual(changedBlob) && !handler.ConflictOnce); }); await TestAsync("Leaving home between GET and PUT blocks upload", async () => { handler.Blob = blob; config.Etag = VaultCrypto.Hash(blob); int before = handler.Requests, puts = handler.Puts; handler.AfterGet = () => home.Binding = FakeHomeNetwork.Home with { Ssid = "Away" }; try { await RejectAsync(() => client.Sync(changedBlob, blob, key)); Assert(handler.Requests == before + 1 && handler.Puts == puts); } finally { handler.AfterGet = null; home.Binding = FakeHomeNetwork.Home; } }); Test("LAN HTTP handler disables proxy, redirects and certificate downloads", () => { var method = typeof(SyncClient).GetMethod("CreateHandler", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; using var transport = (SocketsHttpHandler)method.Invoke(client, null)!; Assert(!transport.UseProxy && !transport.AllowAutoRedirect && transport.SslOptions.CertificateChainPolicy!.DisableCertificateDownloads && transport.SslOptions.CertificateRevocationCheckMode == System.Security.Cryptography.X509Certificates.X509RevocationMode.NoCheck && transport.ConnectCallback != null); }); await TestAsync("Bounded reads reject oversized streams", async () => { using var stream = new MemoryStream(new byte[20]); await RejectAsync(() => SyncClient.Bounded(stream, 10)); }); if (File.Exists(Path.Combine(artifact, "java.qsafe"))) { Test("Actual Android Java envelope opens in Windows", () => { var javaBlob = File.ReadAllBytes(Path.Combine(artifact, "java.qsafe")); using var javaKey = VaultCrypto.Unlock(javaBlob, "Публичный тест 🔐 QSafe 2026"); var d = Vault.Read(javaBlob, javaKey); Assert(d.Entries.Single().Title == "Привет из Android 🔐" && d.Extra!["futureRoot"].GetProperty("a").GetInt32() == 1 && d.Entries.Single().Fields.Single().Extra!["futureField"].GetBoolean()); d.Entries[0].Rev = "dotnet-revision"; File.WriteAllBytes(Path.Combine(artifact, "dotnet.qsafe"), Vault.Encrypt(d, javaKey)); }); } } private static void DesktopTests(string artifact) { var path = Path.Combine(Path.GetTempPath(), "QSafe-Tests", Guid.NewGuid().ToString("N")); var store = new LocalStore(path); Test("Windows DPAPI seals credentials for current user", () => { var secret = Encoding.UTF8.GetBytes("synthetic test token"); var sealedBytes = DeviceProtection.Protect(secret); Assert(!sealedBytes.SequenceEqual(secret)); Assert(DeviceProtection.Unprotect(sealedBytes).SequenceEqual(secret)); }); Test("Atomic persistence keeps encrypted history and reloads config", () => { using var key = VaultCrypto.Create("Test password for desktop"); var d = new VaultData(); for (int i = 0; i < 14; i++) { d.Entries.Add(new Entry { Title = "card " + i }); store.Save(d, key); } Assert(Directory.GetFiles(Path.Combine(path, "backups"), "*.qsafe").Length == 10); Assert(Vault.Read(store.Read(), key).Entries.Count == 14); var config = TestConnection(); config.Token = new string('b', 43); store.Configure(config); store.MarkSynced(store.Read()); var loaded = new LocalStore(path); Assert(loaded.Config.Etag == VaultCrypto.Hash(store.Read()) && loaded.Connected); Assert(!Encoding.UTF8.GetString(File.ReadAllBytes(Path.Combine(path, "connection.enc"))).Contains(new string('b', 43))); }); Test("Changing server resets ancestor and ETag", () => { var config = TestConnection(); config.Url = "https://192.168.0.186:18443/qsafe/"; store.Configure(config); Assert(store.ReadBase() == null && store.Config.Etag == ""); }); Test("WPF resources and synthetic demo render at desktop and minimum size", () => { var app = new App(); app.InitializeComponent(); var demoStore = new LocalStore(Path.Combine(Path.GetTempPath(), "QSafe-Demo", Guid.NewGuid().ToString("N"))); var main = new MainWindow(demoStore, true); app.MainWindow = main; var search = (TextBox)main.FindName("Search"); var cards = (ListBox)main.FindName("Cards"); Test("Search finds non-secret fields and excludes passwords", () => { search.Text = "GITHUB.COM"; Assert(cards.Items.Count == 1); search.Text = "Demo-Only-Password"; Assert(cards.Items.Count == 0); search.Clear(); search.Text = "GitHub"; search.Clear(); }); Test("Background operations do not reset the inactivity timer", () => { var field = typeof(MainWindow).GetField("lastInput", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; var old = DateTime.UtcNow.AddSeconds(-80); field.SetValue(main, old); var work = typeof(MainWindow).GetMethod("Work", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; ((Task)work.Invoke(main, ["Test", (Func)(() => Task.CompletedTask)])!).GetAwaiter().GetResult(); Assert((DateTime)field.GetValue(main)! == old); }); Render((FrameworkElement)main.Content, Path.Combine(artifact, "QSafe-desktop.png"), 1360, 810); Render((FrameworkElement)main.Content, Path.Combine(artifact, "QSafe-desktop-compact.png"), 1080, 660); var label = ((StackPanel)main.FindName("Details")).Children.OfType().First().Text; Assert(label.Contains("САЙТЫ")); // Calls the application's lock logic in-process; no OS input or user vault is involved. search.Text = "GitHub"; typeof(MainWindow).GetMethod("Lock", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!.Invoke(main, null); Assert(cards.Items.Count == 0 && ((StackPanel)main.FindName("Details")).Children.Count == 0); Render((FrameworkElement)main.Content, Path.Combine(artifact, "QSafe-unlock.png"), 1360, 810); Test("Generator preview, mode lengths, cancel and explicit use", () => { main.Show(); main.Hide(); string? used = null, expected = null; Exception? failure = null; var generator = new PasswordGeneratorDialog(main, value => used = value); var preview = Descendants(generator.Body).Single(); var slider = Descendants(generator.Body).Single(); var modes = Descendants(generator.Body).ToArray(); generator.Loaded += (_, _) => { try { Assert(preview.Text.Length == 24 && used == null); slider.Value = 32; Assert(preview.Text.Length == 32); Render((FrameworkElement)generator.Content, Path.Combine(artifact, "QSafe-generator-complex.png"), 620, 680); modes[1].IsChecked = true; slider.Value = 4; Assert(preview.Text.Length == 4 && preview.Text.All(char.IsDigit)); Render((FrameworkElement)generator.Content, Path.Combine(artifact, "QSafe-generator-digits.png"), 620, 680); modes[2].IsChecked = true; slider.Value = 12; Assert(Regex.Matches(preview.Text, "[A-Z]?[a-z]+").Count == 12); Render((FrameworkElement)generator.Content, Path.Combine(artifact, "QSafe-generator-memorable.png"), 620, 680); Assert(slider.Minimum == 2); slider.Value = 2; var symbols = Descendants(generator.Body).Single(c => ((string)c.Content).StartsWith("Специальные")); var digits = Descendants(generator.Body).Single(c => ((string)c.Content).StartsWith("Добавлять цифры")); Assert(symbols.Visibility == Visibility.Visible && digits.Visibility == Visibility.Visible); Assert(WholeWords(preview.Text, 2) && preview.Text.Count(char.IsDigit) == 2 && preview.Text.Count(c => !char.IsLetterOrDigit(c)) == 1); Render((FrameworkElement)generator.Content, Path.Combine(artifact, "QSafe-generator-two-words.png"), 620, 680); digits.IsChecked = false; Assert(!preview.Text.Any(char.IsDigit)); symbols.IsChecked = false; Assert(Regex.IsMatch(preview.Text, @"^[A-Z][a-z]+[A-Z][a-z]+$")); digits.IsChecked = true; symbols.IsChecked = true; modes[0].IsChecked = true; Assert(slider.Minimum == 16 && slider.Value == 32 && preview.Text.Length == 32); modes[1].IsChecked = true; Assert(slider.Minimum == 4 && slider.Value == 4); modes[2].IsChecked = true; Assert(slider.Value == 2); expected = preview.Text; Assert(used == null); generator.Actions.Children.OfType