diff --git a/.gitignore b/.gitignore index 990fde9..ad42bfb 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,9 @@ web-ext-artifacts/ # ─── Publish output ────────────────────────────────────────────────────────── publish/ + +# ─── claude-code workspace artefacts (per-developer, never commit) ─────────── +.claude/ + +# ─── Password-manager export files (real plaintext credentials — test only) ── +pm-export/ diff --git a/Installer/PassKey.iss b/Installer/PassKey.iss index a707b6a..7f40a74 100644 --- a/Installer/PassKey.iss +++ b/Installer/PassKey.iss @@ -4,8 +4,8 @@ [Setup] AppId={{A7F3C2D1-8E4B-4F9A-B6D5-3C1E7A2F0D84} AppName=PassKey -AppVersion=1.0.17 -AppVerName=PassKey 1.0.17 +AppVersion=2.0.0 +AppVerName=PassKey 2.0.0 AppPublisher=Giuseppe Imperato AppPublisherURL=https://github.com/pexatar/PassKey AppSupportURL=https://github.com/pexatar/PassKey/issues diff --git a/README.md b/README.md index 216bce8..d7ae25d 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ PassKey stores your passwords, credit cards, identities, and secure notes in a l | Version | Platform | Type | |---------|----------|------| -| [v1.0.17](https://github.com/pexatar/PassKey/releases/tag/v1.0.17) | Windows x64 | Installer EXE | -| [v1.0.17](https://github.com/pexatar/PassKey/releases/tag/v1.0.17) | Windows x64 | Portable ZIP | +| [v2.0.0](https://github.com/pexatar/PassKey/releases/tag/v2.0.0) | Windows x64 | Installer EXE | +| [v2.0.0](https://github.com/pexatar/PassKey/releases/tag/v2.0.0) | Windows x64 | Portable ZIP | > **Requirements:** Windows 10 version 1809 (build 17763) or later, x64 processor. > No .NET runtime required — PassKey is fully self-contained. diff --git a/docs/assets/img/passkey/00 - welcome page.png b/docs/assets/img/passkey/00 - welcome page.png index 839063b..1c9fd70 100644 Binary files a/docs/assets/img/passkey/00 - welcome page.png and b/docs/assets/img/passkey/00 - welcome page.png differ diff --git a/docs/assets/img/passkey/01 - dashboard.png b/docs/assets/img/passkey/01 - dashboard.png index 248b290..8bd0142 100644 Binary files a/docs/assets/img/passkey/01 - dashboard.png and b/docs/assets/img/passkey/01 - dashboard.png differ diff --git a/docs/assets/img/passkey/02 - password.png b/docs/assets/img/passkey/02 - password.png index ae177e6..2eacc51 100644 Binary files a/docs/assets/img/passkey/02 - password.png and b/docs/assets/img/passkey/02 - password.png differ diff --git a/docs/assets/img/passkey/03 - add password.png b/docs/assets/img/passkey/03 - add password.png index aa34619..8a89363 100644 Binary files a/docs/assets/img/passkey/03 - add password.png and b/docs/assets/img/passkey/03 - add password.png differ diff --git a/docs/assets/img/passkey/04 - card.png b/docs/assets/img/passkey/04 - card.png index cc04434..39562e8 100644 Binary files a/docs/assets/img/passkey/04 - card.png and b/docs/assets/img/passkey/04 - card.png differ diff --git a/docs/assets/img/passkey/05 - card list view.png b/docs/assets/img/passkey/05 - card list view.png index e97dc4d..2595ffa 100644 Binary files a/docs/assets/img/passkey/05 - card list view.png and b/docs/assets/img/passkey/05 - card list view.png differ diff --git a/docs/assets/img/passkey/06 - edit card.png b/docs/assets/img/passkey/06 - edit card.png index 0482869..63cfaf5 100644 Binary files a/docs/assets/img/passkey/06 - edit card.png and b/docs/assets/img/passkey/06 - edit card.png differ diff --git "a/docs/assets/img/passkey/07 - identit\303\240.png" "b/docs/assets/img/passkey/07 - identit\303\240.png" index 1792571..7cea083 100644 Binary files "a/docs/assets/img/passkey/07 - identit\303\240.png" and "b/docs/assets/img/passkey/07 - identit\303\240.png" differ diff --git "a/docs/assets/img/passkey/08 - edit identit\303\240.png" "b/docs/assets/img/passkey/08 - edit identit\303\240.png" index 6f88731..e99225d 100644 Binary files "a/docs/assets/img/passkey/08 - edit identit\303\240.png" and "b/docs/assets/img/passkey/08 - edit identit\303\240.png" differ diff --git a/docs/assets/img/passkey/09 - note sicure.png b/docs/assets/img/passkey/09 - note sicure.png index fdd98fc..de39a56 100644 Binary files a/docs/assets/img/passkey/09 - note sicure.png and b/docs/assets/img/passkey/09 - note sicure.png differ diff --git a/docs/assets/img/passkey/10 - edit note sicure.png b/docs/assets/img/passkey/10 - edit note sicure.png index 226e822..e02458b 100644 Binary files a/docs/assets/img/passkey/10 - edit note sicure.png and b/docs/assets/img/passkey/10 - edit note sicure.png differ diff --git a/docs/assets/img/passkey/11 - generatore password.png b/docs/assets/img/passkey/11 - generatore password.png index ebde251..ba95f7e 100644 Binary files a/docs/assets/img/passkey/11 - generatore password.png and b/docs/assets/img/passkey/11 - generatore password.png differ diff --git a/docs/assets/img/passkey/12 - verifica password.png b/docs/assets/img/passkey/12 - verifica password.png index 106df60..34d0bc6 100644 Binary files a/docs/assets/img/passkey/12 - verifica password.png and b/docs/assets/img/passkey/12 - verifica password.png differ diff --git a/docs/assets/img/passkey/13 - test verifica password.png b/docs/assets/img/passkey/13 - test verifica password.png index 2960e0d..f6cac11 100644 Binary files a/docs/assets/img/passkey/13 - test verifica password.png and b/docs/assets/img/passkey/13 - test verifica password.png differ diff --git a/docs/assets/img/passkey/14 - audit vault.png b/docs/assets/img/passkey/14 - audit vault.png index c5bbb25..0a13b5c 100644 Binary files a/docs/assets/img/passkey/14 - audit vault.png and b/docs/assets/img/passkey/14 - audit vault.png differ diff --git a/docs/assets/img/passkey/15 - guida 01.png b/docs/assets/img/passkey/15 - guida 01.png index ad05adf..d7f8136 100644 Binary files a/docs/assets/img/passkey/15 - guida 01.png and b/docs/assets/img/passkey/15 - guida 01.png differ diff --git a/docs/assets/img/passkey/16 - guida 02.png b/docs/assets/img/passkey/16 - guida 02.png deleted file mode 100644 index bb8002a..0000000 Binary files a/docs/assets/img/passkey/16 - guida 02.png and /dev/null differ diff --git a/docs/assets/img/passkey/17 - impostazioni.png b/docs/assets/img/passkey/17 - impostazioni.png index e8ad2ae..07e7b38 100644 Binary files a/docs/assets/img/passkey/17 - impostazioni.png and b/docs/assets/img/passkey/17 - impostazioni.png differ diff --git a/docs/assets/img/passkey/18 - impostazioni lingua.png b/docs/assets/img/passkey/18 - impostazioni lingua.png deleted file mode 100644 index 0556dd0..0000000 Binary files a/docs/assets/img/passkey/18 - impostazioni lingua.png and /dev/null differ diff --git a/docs/assets/img/passkey/19 - impostazioni tema.png b/docs/assets/img/passkey/19 - impostazioni tema.png deleted file mode 100644 index 0ea655a..0000000 Binary files a/docs/assets/img/passkey/19 - impostazioni tema.png and /dev/null differ diff --git a/docs/assets/img/passkey/20 - impostazioni blocco automatico.png b/docs/assets/img/passkey/20 - impostazioni blocco automatico.png deleted file mode 100644 index b13cbba..0000000 Binary files a/docs/assets/img/passkey/20 - impostazioni blocco automatico.png and /dev/null differ diff --git a/docs/assets/img/passkey/21 - impostazioni cambio master password.png b/docs/assets/img/passkey/21 - impostazioni cambio master password.png deleted file mode 100644 index f849ba8..0000000 Binary files a/docs/assets/img/passkey/21 - impostazioni cambio master password.png and /dev/null differ diff --git a/docs/assets/img/passkey/22 - impostazioni crea backup.png b/docs/assets/img/passkey/22 - impostazioni crea backup.png deleted file mode 100644 index 7d88be3..0000000 Binary files a/docs/assets/img/passkey/22 - impostazioni crea backup.png and /dev/null differ diff --git a/docs/assets/img/passkey/23 - impostazioni ripristina backup.png b/docs/assets/img/passkey/23 - impostazioni ripristina backup.png deleted file mode 100644 index 5efd1b0..0000000 Binary files a/docs/assets/img/passkey/23 - impostazioni ripristina backup.png and /dev/null differ diff --git a/docs/assets/img/passkey/24 - impostazioni importa dati.png b/docs/assets/img/passkey/24 - impostazioni importa dati.png deleted file mode 100644 index dbbca68..0000000 Binary files a/docs/assets/img/passkey/24 - impostazioni importa dati.png and /dev/null differ diff --git a/docs/assets/img/passkey/25 - impostazioni informazioni.png b/docs/assets/img/passkey/25 - impostazioni informazioni.png deleted file mode 100644 index bc379e8..0000000 Binary files a/docs/assets/img/passkey/25 - impostazioni informazioni.png and /dev/null differ diff --git a/extensions/chrome/manifest.json b/extensions/chrome/manifest.json index 7b1e196..548ce20 100644 --- a/extensions/chrome/manifest.json +++ b/extensions/chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "PassKey", - "version": "1.0.0", + "version": "1.0.1", "author": "Giuseppe Imperato", "homepage_url": "https://github.com/pexatar/PassKey", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjdNrt0WikUv9qMTjwXhpg5jIYvCLS0WYu8lRlUCzcvdB/aEuHOEqaQ4ByEES+eISVoOo4Vmdr4u3Zo1+IyPwE1zGqwX1agNMsZbctr3Ur8esnHp/gMaLkrONFHDrOXhaVdUJy5BDqa5yXuTN5jsZWhk3cmqxpxlfWu70piFWN4bsPPwdKNXuDYPSBQCcdUFlUxUz3GXX8zkm1GsXQONg1vMSYE9E5GHD4ORk5oSWlRDccsTHtnWVlfaNzlBLmuiXTbmrCBRO2zfKU+8T3byMZTWGf/0ygD7+XuNbHvMAxQgFFwdAeP9RX/kXnBej2KwXxlVE65rd/kSwfa5hFLcubQIDAQAB", diff --git a/extensions/chrome/popup/popup.css b/extensions/chrome/popup/popup.css index ecb2b57..a74b94c 100644 --- a/extensions/chrome/popup/popup.css +++ b/extensions/chrome/popup/popup.css @@ -358,31 +358,15 @@ body { text-overflow: ellipsis; } -/* Password presence indicator (visible at rest, hidden on hover) */ -.pk-pw-indicator { - color: var(--pk-text-3); - display: flex; - align-items: center; - flex-shrink: 0; - transition: opacity 0.12s; - margin-right: 2px; -} -.pk-cred-item:hover .pk-pw-indicator, -.pk-cred-item:focus-within .pk-pw-indicator { opacity: 0; pointer-events: none; } +/* Password presence indicator — hidden: the always-visible copy-password action + now conveys password presence, so the separate dot would be redundant. */ +.pk-pw-indicator { display: none; } -/* Action buttons — hidden at rest, revealed on hover/focus */ +/* Action buttons — always visible for discoverability (Bitwarden/1Password pattern) */ .pk-cred-actions { display: flex; gap: 3px; flex-shrink: 0; - opacity: 0; - pointer-events: none; - transition: opacity 0.15s; -} -.pk-cred-item:hover .pk-cred-actions, -.pk-cred-item:focus-within .pk-cred-actions { - opacity: 1; - pointer-events: auto; } /* Labeled action buttons (icon + text) */ @@ -422,7 +406,10 @@ body { height: 28px; padding: 0; justify-content: center; + color: var(--pk-text-1); + background: var(--pk-fill-hover); } +.pk-action-btn:not([data-action="fill"]):hover { background: var(--pk-fill-active); } .pk-action-btn:not([data-action="fill"]) .pk-action-label { display: none; } /* Legacy icon-only buttons (kept for other potential uses) */ diff --git a/extensions/chrome/popup/popup.js b/extensions/chrome/popup/popup.js index 136e756..37f755d 100644 --- a/extensions/chrome/popup/popup.js +++ b/extensions/chrome/popup/popup.js @@ -50,8 +50,13 @@ const SPIN_SVG = ` pattern of exposing a display-ready computed value + /// directly on the model. + /// + public string MaskedCardNumber => string.IsNullOrWhiteSpace(CardNumber) + ? "•••• •••• •••• ••••" + : CardTypeDetector.MaskCardNumber(CardNumber, CardType); + /// Gets or sets the expiry month (1–12). public int ExpiryMonth { get; set; } diff --git a/src/PassKey.Core/Models/IdentityEntry.cs b/src/PassKey.Core/Models/IdentityEntry.cs index b0b5a98..602dfd3 100644 --- a/src/PassKey.Core/Models/IdentityEntry.cs +++ b/src/PassKey.Core/Models/IdentityEntry.cs @@ -16,6 +16,9 @@ public sealed class IdentityEntry : IVaultEntry /// Gets or sets the person's first name. public string FirstName { get; set; } = string.Empty; + /// Gets or sets the person's middle name (FU5). + public string MiddleName { get; set; } = string.Empty; + /// Gets or sets the person's last name (surname). public string LastName { get; set; } = string.Empty; @@ -28,6 +31,12 @@ public sealed class IdentityEntry : IVaultEntry /// Gets or sets the primary phone number (including country code if applicable). public string Phone { get; set; } = string.Empty; + /// Gets or sets the company / organisation name (FU5). + public string Company { get; set; } = string.Empty; + + /// Gets or sets a generic username associated with this identity (FU5). + public string Username { get; set; } = string.Empty; + /// Gets or sets the street address (number and street name). public string Street { get; set; } = string.Empty; diff --git a/src/PassKey.Core/Services/BitwardenImporter.cs b/src/PassKey.Core/Services/BitwardenImporter.cs index 7a44b7b..66b6b40 100644 --- a/src/PassKey.Core/Services/BitwardenImporter.cs +++ b/src/PassKey.Core/Services/BitwardenImporter.cs @@ -26,6 +26,12 @@ public Vault ParseBitwarden(string jsonContent) var vault = new Vault(); var export = JsonSerializer.Deserialize(jsonContent, BitwardenJsonContext.Default.BitwardenExport); + // FU3: an encrypted export has no plaintext items — surface a clear message + // instead of silently importing an empty vault. Throws an error CODE; the Desktop + // layer maps it to a localized message (Core stays UI/i18n-free). + if (export?.Encrypted == true) + throw new ImportFileException("IMPORT_BW_ENCRYPTED"); + if (export?.Items is null) return vault; foreach (var item in export.Items) @@ -163,24 +169,33 @@ private static IdentityEntry MapIdentity(BitwardenItem item) Id = Guid.NewGuid(), Label = item.Name ?? string.Empty, FirstName = id?.FirstName ?? string.Empty, + MiddleName = id?.MiddleName ?? string.Empty, LastName = id?.LastName ?? string.Empty, Email = id?.Email ?? string.Empty, Phone = id?.Phone ?? string.Empty, - Street = CombineAddress(id?.Address1, id?.Address2), + Company = id?.Company ?? string.Empty, + Username = id?.Username ?? string.Empty, + Street = CombineAddress(id?.Address1, id?.Address2, id?.Address3), City = id?.City ?? string.Empty, Province = id?.State ?? string.Empty, PostalCode = id?.PostalCode ?? string.Empty, Country = id?.Country ?? string.Empty, + // For Italian users Codice Fiscale and Tessera Sanitaria coincide in common use, + // so Bitwarden's "ssn" maps onto the existing HealthCardNumber field (FU4/FU5). + HealthCardNumber = id?.Ssn ?? string.Empty, + PassportNumber = id?.PassportNumber ?? string.Empty, + DrivingLicenseNumber = id?.LicenseNumber ?? string.Empty, Notes = item.Notes ?? string.Empty, CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow }; } - private static string CombineAddress(string? address1, string? address2) + private static string CombineAddress(string? address1, string? address2, string? address3) { - if (string.IsNullOrEmpty(address2)) return address1 ?? string.Empty; - if (string.IsNullOrEmpty(address1)) return address2; - return $"{address1}, {address2}"; + var parts = new[] { address1, address2, address3 } + .Where(a => !string.IsNullOrWhiteSpace(a)) + .Select(a => a!.Trim()); + return string.Join(", ", parts); } } diff --git a/src/PassKey.Core/Services/BitwardenJsonContext.cs b/src/PassKey.Core/Services/BitwardenJsonContext.cs index d4f6a69..8e9656e 100644 --- a/src/PassKey.Core/Services/BitwardenJsonContext.cs +++ b/src/PassKey.Core/Services/BitwardenJsonContext.cs @@ -6,6 +6,15 @@ namespace PassKey.Core.Services; public sealed class BitwardenExport { + /// True for an encrypted Bitwarden export (account-restricted or + /// password-protected). Such files have no plaintext and cannot + /// be imported — detected so the user gets a clear message (FU3). + public bool Encrypted { get; set; } + + /// True when the encrypted export is protected by a file password (as + /// opposed to the account key). Informational companion to . + public bool PasswordProtected { get; set; } + public BitwardenItem[]? Items { get; set; } } @@ -45,11 +54,18 @@ public sealed class BitwardenCard public sealed class BitwardenIdentity { public string? FirstName { get; set; } + public string? MiddleName { get; set; } public string? LastName { get; set; } + public string? Username { get; set; } + public string? Company { get; set; } + public string? Ssn { get; set; } + public string? PassportNumber { get; set; } + public string? LicenseNumber { get; set; } public string? Email { get; set; } public string? Phone { get; set; } public string? Address1 { get; set; } public string? Address2 { get; set; } + public string? Address3 { get; set; } public string? City { get; set; } public string? State { get; set; } public string? PostalCode { get; set; } diff --git a/src/PassKey.Core/Services/CsvImporter.cs b/src/PassKey.Core/Services/CsvImporter.cs index 80be7ae..3c7ec5b 100644 --- a/src/PassKey.Core/Services/CsvImporter.cs +++ b/src/PassKey.Core/Services/CsvImporter.cs @@ -50,12 +50,17 @@ public Vault ParseCsv(string csvContent) ModifiedAt = DateTime.UtcNow }; - // Skip rows with no meaningful data + // Skip rows with no meaningful data (judged on the raw imported fields). if (string.IsNullOrWhiteSpace(entry.Title) && string.IsNullOrWhiteSpace(entry.Username) && string.IsNullOrWhiteSpace(entry.Password)) continue; + // FU6a: exporters like Firefox omit a title column and identify logins only + // by URL. Fall back to the URL host so the surviving entry isn't anonymous. + if (string.IsNullOrWhiteSpace(entry.Title)) + entry.Title = DeriveTitleFromUrl(entry.Url); + vault.Passwords.Add(entry); } @@ -71,19 +76,24 @@ private static Dictionary MapHeaders(List headers) for (int i = 0; i < headers.Count; i++) { - var header = headers[i].Trim().ToLowerInvariant(); + // Normalize first so that aliases differing only by separators/case match: + // "Login Name", "login_name" and "login name" all collapse to "login name". + var header = NormalizeHeader(headers[i]); switch (header) { - case "title" or "name" or "service" or "login_name": + // "account" is KeePass's title column; "login name" is now treated as a + // username (it is KeePass's username field), not a title. + case "title" or "name" or "service" or "account": map.TryAdd(ColumnType.Title, i); break; - case "username" or "email" or "login" or "user" or "login_username": + case "username" or "email" or "login" or "user" or "user name" + or "login username" or "login name": map.TryAdd(ColumnType.Username, i); break; - case "password" or "pass" or "login_password": + case "password" or "pass" or "login password": map.TryAdd(ColumnType.Password, i); break; - case "url" or "uri" or "website" or "login_uri": + case "url" or "uri" or "website" or "web site" or "login uri": map.TryAdd(ColumnType.Url, i); break; case "notes" or "note" or "comment" or "comments" or "extra": @@ -96,6 +106,18 @@ private static Dictionary MapHeaders(List headers) return map; } + /// + /// Normalizes a CSV header for alias matching: lowercased, trimmed, underscores + /// treated as spaces, and runs of whitespace collapsed to a single space. This lets + /// a single alias list cover exporters that write "login_name", "Login Name" or + /// "login name" interchangeably (FU6b). + /// + private static string NormalizeHeader(string header) + { + var lowered = header.Trim().ToLowerInvariant().Replace('_', ' '); + return string.Join(' ', lowered.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + private static string GetField(List fields, Dictionary map, ColumnType type) { if (!map.TryGetValue(type, out var idx) || idx >= fields.Count) @@ -118,6 +140,23 @@ private static int TryFindTypeColumn(List headers) return -1; } + /// + /// Derives a display title from a URL when the CSV has no usable title column + /// (FU6a). Returns the host part of an absolute URL (e.g. "accounts.google.com" + /// from "https://accounts.google.com/"), or the raw URL when it cannot be parsed + /// as an absolute URI, or an empty string when no URL is available. + /// + internal static string DeriveTitleFromUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) return string.Empty; + + var trimmed = url.Trim(); + if (Uri.TryCreate(trimmed, UriKind.Absolute, out var uri) && !string.IsNullOrEmpty(uri.Host)) + return uri.Host; + + return trimmed; + } + /// /// Parses a CSV line handling RFC 4180 quoted fields. /// diff --git a/src/PassKey.Core/Services/ImportFileException.cs b/src/PassKey.Core/Services/ImportFileException.cs new file mode 100644 index 0000000..985b380 --- /dev/null +++ b/src/PassKey.Core/Services/ImportFileException.cs @@ -0,0 +1,13 @@ +namespace PassKey.Core.Services; + +/// +/// Thrown when an import file cannot be processed for a reason worth surfacing to the +/// user verbatim — an encrypted export, an unsupported legacy format, or a malformed +/// archive (FU3). The is a ready-to-display, +/// user-facing string: the import flow shows it directly instead of importing an empty +/// vault silently or surfacing a raw technical error. +/// +public sealed class ImportFileException : Exception +{ + public ImportFileException(string message) : base(message) { } +} diff --git a/src/PassKey.Core/Services/OnePuxImporter.cs b/src/PassKey.Core/Services/OnePuxImporter.cs index b7bb240..bd00e26 100644 --- a/src/PassKey.Core/Services/OnePuxImporter.cs +++ b/src/PassKey.Core/Services/OnePuxImporter.cs @@ -45,6 +45,20 @@ public Vault ParseOnePux(string exportDataJson) } private void MapItem(Vault vault, OnePuxItem item) + { + try + { + MapItemCore(vault, item); + } + catch + { + // Defence-in-depth (FU7): a single malformed item must not abort the whole + // import. The email-object crash is prevented upstream by OnePuxEmailConverter; + // this guard covers any other unexpected per-item mapping failure. + } + } + + private void MapItemCore(Vault vault, OnePuxItem item) { var title = item.Overview?.Title ?? string.Empty; var notes = item.Details?.NotesPlain ?? string.Empty; @@ -193,6 +207,7 @@ private static bool HasCreditCardSection(OnePuxSection[] sections) { return sections.Any(s => s.Fields?.Any(f => f.Value?.CreditCardNumber is not null || + string.Equals(f.Id, "ccnum", StringComparison.OrdinalIgnoreCase) || f.Title?.Contains("card", StringComparison.OrdinalIgnoreCase) == true) == true); } @@ -214,29 +229,35 @@ private static CreditCardEntry MapToCreditCard(string title, string notes, OnePu if (section.Fields is null) continue; foreach (var field in section.Fields) { + // Prefer the stable, language-independent field id; fall back to the + // localized title for older exports / tests that lack ids (FU7b). + var id = field.Id?.Trim().ToLowerInvariant() ?? string.Empty; var fieldTitle = field.Title?.ToLowerInvariant() ?? string.Empty; var val = field.Value; - if (val?.CreditCardNumber is not null) + if (val?.CreditCardNumber is not null || id == "ccnum") { - entry.CardNumber = val.CreditCardNumber; + entry.CardNumber = val?.CreditCardNumber ?? val?.String ?? string.Empty; entry.CardType = CardTypeDetector.Detect(entry.CardNumber); } - else if (fieldTitle.Contains("cardholder") || fieldTitle.Contains("holder") || fieldTitle.Contains("name")) + else if (id == "cardholder" || fieldTitle.Contains("cardholder") || fieldTitle.Contains("holder") || fieldTitle.Contains("name")) { entry.CardholderName = val?.String ?? string.Empty; } - else if (fieldTitle.Contains("cvv") || fieldTitle.Contains("verification")) + else if (id == "cvv" || fieldTitle.Contains("cvv") || fieldTitle.Contains("verification")) { entry.Cvv = val?.Concealed ?? val?.String ?? string.Empty; } - else if (val?.MonthYear is int monthYear and > 0) + else if (val?.MonthYear is int monthYear and > 0 || id == "expiry") { // MonthYear format: YYYYMM - entry.ExpiryYear = monthYear / 100; - entry.ExpiryMonth = monthYear % 100; + if (val?.MonthYear is int my and > 0) + { + entry.ExpiryYear = my / 100; + entry.ExpiryMonth = my % 100; + } } - else if (fieldTitle.Contains("pin")) + else if (id == "pin" || fieldTitle.Contains("pin")) { entry.Pin = val?.Concealed ?? val?.String ?? string.Empty; } @@ -250,9 +271,11 @@ private static bool HasIdentitySection(OnePuxSection[] sections) { return sections.Any(s => s.Fields?.Any(f => { + var id = f.Id?.Trim().ToLowerInvariant() ?? string.Empty; var t = f.Title?.ToLowerInvariant() ?? string.Empty; - return t.Contains("first name") || t.Contains("last name") || - t.Contains("address") || f.Value?.Address is not null; + return id is "firstname" or "lastname" + || t.Contains("first name") || t.Contains("last name") + || t.Contains("address") || f.Value?.Address is not null; }) == true); } @@ -272,6 +295,9 @@ private static IdentityEntry MapToIdentity(string title, string notes, OnePuxSec if (section.Fields is null) continue; foreach (var field in section.Fields) { + // Prefer the stable, language-independent field id; fall back to the + // localized title for older exports / tests that lack ids (FU7b). + var id = field.Id?.Trim().ToLowerInvariant() ?? string.Empty; var fieldTitle = field.Title?.ToLowerInvariant() ?? string.Empty; var val = field.Value; @@ -283,16 +309,25 @@ private static IdentityEntry MapToIdentity(string title, string notes, OnePuxSec entry.PostalCode = addr.Zip ?? string.Empty; entry.Country = addr.Country ?? string.Empty; } - else if (fieldTitle.Contains("first name")) + else if (id == "firstname" || fieldTitle.Contains("first name")) entry.FirstName = val?.String ?? string.Empty; - else if (fieldTitle.Contains("last name")) + else if (id == "lastname" || fieldTitle.Contains("last name")) entry.LastName = val?.String ?? string.Empty; - else if (fieldTitle.Contains("email") || val?.Email is not null) + else if (id == "email" || fieldTitle.Contains("email") || val?.Email is not null) entry.Email = val?.Email ?? val?.String ?? string.Empty; - else if (fieldTitle.Contains("phone") || val?.Phone is not null) - entry.Phone = val?.Phone ?? val?.String ?? string.Empty; - else if (val?.Date is { } date) - entry.BirthDate = $"{date.Year:D4}-{date.Month:D2}-{date.Day:D2}"; + else if (id is "defphone" or "cellphone" or "homephone" or "busphone" + || fieldTitle.Contains("phone") || val?.Phone is not null) + { + // Several phone fields exist (default/cell/home/business); keep the + // first non-empty one instead of letting a later empty field clear it. + var phone = val?.Phone ?? val?.String ?? string.Empty; + if (!string.IsNullOrEmpty(phone)) entry.Phone = phone; + } + else if (id == "birthdate" || val?.Date is not null) + { + if (val?.Date is { } date) + entry.BirthDate = $"{date.Year:D4}-{date.Month:D2}-{date.Day:D2}"; + } } } diff --git a/src/PassKey.Core/Services/OnePuxJsonContext.cs b/src/PassKey.Core/Services/OnePuxJsonContext.cs index c62abaf..f1cdc9b 100644 --- a/src/PassKey.Core/Services/OnePuxJsonContext.cs +++ b/src/PassKey.Core/Services/OnePuxJsonContext.cs @@ -1,7 +1,53 @@ +using System.Text.Json; using System.Text.Json.Serialization; namespace PassKey.Core.Services; +/// +/// Tolerates both shapes of the 1Password 1pux email field value (FU7a): +/// the legacy plain string ("email": "user@host") and the current object form +/// ("email": { "email_address": "user@host", "provider": null }). The default +/// deserialiser throws on the object form, which aborts the entire +/// import — every recent 1Password export contains the object form in its default +/// "Starter Kit" identity, so without this converter 1PUX import is broken out of the box. +/// +public sealed class OnePuxEmailConverter : JsonConverter +{ + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.Null: + return null; + + case JsonTokenType.String: + return reader.GetString(); + + case JsonTokenType.StartObject: + string? email = null; + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType != JsonTokenType.PropertyName) continue; + var prop = reader.GetString(); + reader.Read(); + if (string.Equals(prop, "email_address", StringComparison.OrdinalIgnoreCase) + && reader.TokenType == JsonTokenType.String) + email = reader.GetString(); + else + reader.Skip(); + } + return email; + + default: + reader.Skip(); + return null; + } + } + + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + => writer.WriteStringValue(value); +} + // --- 1Password 1PUX Export DTOs --- public sealed class OnePuxExport @@ -62,6 +108,13 @@ public sealed class OnePuxSection public sealed class OnePuxSectionField { + /// + /// Language-independent, stable field identifier (e.g. "firstname", "lastname", + /// "email", "ccnum", "cvv"). Preferred over for mapping because + /// the title is localized in the user's 1Password language (FU7b). + /// + public string? Id { get; set; } + public string? Title { get; set; } public OnePuxFieldValue? Value { get; set; } } @@ -74,7 +127,10 @@ public sealed class OnePuxFieldValue public string? CreditCardType { get; set; } public int? MonthYear { get; set; } public string? Phone { get; set; } + + [JsonConverter(typeof(OnePuxEmailConverter))] public string? Email { get; set; } + public string? Concealed { get; set; } public OnePuxAddress? Address { get; set; } public OnePuxDate? Date { get; set; } diff --git a/src/PassKey.Core/Services/PasswordStrengthAnalyzer.cs b/src/PassKey.Core/Services/PasswordStrengthAnalyzer.cs index 31a0435..14f14ae 100644 --- a/src/PassKey.Core/Services/PasswordStrengthAnalyzer.cs +++ b/src/PassKey.Core/Services/PasswordStrengthAnalyzer.cs @@ -131,16 +131,19 @@ private static string EstimateCrackTime(int length, bool hasUpper, bool hasLower var combinations = Math.Pow(charsetSize, length); var seconds = combinations / guessesPerSecond / 2; // average case - return seconds switch - { - < 1 => "instant", - < TimeConstants.SecondsPerMinute => "seconds", - < TimeConstants.SecondsPerHour => $"{(int)(seconds / TimeConstants.SecondsPerMinute)} minutes", - < TimeConstants.SecondsPerDay => $"{(int)(seconds / TimeConstants.SecondsPerHour)} hours", - < TimeConstants.SecondsPerYear => $"{(int)(seconds / TimeConstants.SecondsPerDay)} days", - < TimeConstants.SecondsPerCentury => $"{(int)(seconds / TimeConstants.SecondsPerYear)} years", - < TimeConstants.SecondsPerMillennium => "centuries", - _ => "millennia" - }; + if (seconds < 1) return "instant"; + if (seconds < TimeConstants.SecondsPerMinute) return "seconds"; + if (seconds < TimeConstants.SecondsPerHour) return $"{(int)(seconds / TimeConstants.SecondsPerMinute)} minutes"; + if (seconds < TimeConstants.SecondsPerDay) return $"{(int)(seconds / TimeConstants.SecondsPerHour)} hours"; + if (seconds < TimeConstants.SecondsPerYear) return $"{(int)(seconds / TimeConstants.SecondsPerDay)} days"; + + // Above a year, use numeric large-scale buckets so the estimate doesn't collapse + // straight from "years" to "millennia" (each extra char multiplies guesses ~100x). + var years = seconds / TimeConstants.SecondsPerYear; + if (years < 1_000) return $"{(int)years} years"; + if (years < 1_000_000) return $"{(int)(years / 1_000)} thousandyears"; + if (years < 1_000_000_000) return $"{(int)(years / 1_000_000)} millionyears"; + if (years < 1_000_000_000_000) return $"{(int)(years / 1_000_000_000)} billionyears"; + return "trillionyears"; } } diff --git a/src/PassKey.Desktop/App.xaml b/src/PassKey.Desktop/App.xaml index 1a5858e..ce58a91 100644 --- a/src/PassKey.Desktop/App.xaml +++ b/src/PassKey.Desktop/App.xaml @@ -15,6 +15,7 @@ + diff --git a/src/PassKey.Desktop/App.xaml.cs b/src/PassKey.Desktop/App.xaml.cs index 5f95218..da82df9 100644 --- a/src/PassKey.Desktop/App.xaml.cs +++ b/src/PassKey.Desktop/App.xaml.cs @@ -39,6 +39,8 @@ public App() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -75,6 +77,7 @@ public App() services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); }) .Build(); } @@ -105,9 +108,10 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) var ipcService = Services.GetRequiredService(); await ipcService.StartAsync(); } - catch + catch (Exception ex) { - // IPC service failure should not prevent app from starting + // IPC service failure should not prevent app from starting; log for diagnostics. + System.Diagnostics.Debug.WriteLine($"[App] Browser IPC service failed to start: {ex}"); } // Fire-and-forget: silent update check (max once per 24h, 10s timeout) diff --git a/src/PassKey.Desktop/Controls/CreditCardControl.xaml b/src/PassKey.Desktop/Controls/CreditCardControl.xaml index bd51b79..31fcd40 100644 --- a/src/PassKey.Desktop/Controls/CreditCardControl.xaml +++ b/src/PassKey.Desktop/Controls/CreditCardControl.xaml @@ -7,7 +7,11 @@ + Padding="20,16,20,16" + BorderThickness="2" + BorderBrush="Transparent" + PointerEntered="CardRoot_PointerEntered" + PointerExited="CardRoot_PointerExited"> diff --git a/src/PassKey.Desktop/Controls/CreditCardControl.xaml.cs b/src/PassKey.Desktop/Controls/CreditCardControl.xaml.cs index af75149..d3668bc 100644 --- a/src/PassKey.Desktop/Controls/CreditCardControl.xaml.cs +++ b/src/PassKey.Desktop/Controls/CreditCardControl.xaml.cs @@ -275,8 +275,67 @@ private static string GetCategoryName(CardCategory category) CardCategory.Personal => s_res.GetString("CatPersonalLabel"), CardCategory.Work => s_res.GetString("CatWorkLabel"), CardCategory.Travel => s_res.GetString("CatTravelLabel"), - CardCategory.Online => "Online", + CardCategory.Online => s_res.GetString("CatOnlineLabel"), _ => s_res.GetString("CatPersonalLabel") }; } + + #region Hover effect (T5.7) + + // Hover highlights the card border. Prior approaches and why they failed: + // 1. System accent brush (blue) clashed with strong card colours (red, green, gold…). + // 2. Translucent white worked on dark theme but vanished against the white page + // background in light theme. + // 3. Card-accent **lightened** worked on dark theme but stayed washed-out on + // light theme (a brighter colour adjacent to a bright page → poor contrast). + // Current approach (theme-aware): derive the border colour from the card's own + // accent — **lightened** on dark theme, **darkened** on light theme. Each shift is + // ±90 per RGB channel (clamped). This guarantees the border always contrasts both + // with the card (it differs from the card's own gradient start by ~90) and with + // the surrounding page (it shifts toward the opposite end of the value scale from + // the page background). + + private static readonly SolidColorBrush TransparentBorderBrush = + new(Microsoft.UI.Colors.Transparent); + + private const int HoverShift = 90; + + /// + /// Highlights the card border with a shade of the card's own accent — lightened + /// on dark theme, darkened on light theme — so the highlight stays readable on + /// both the card and the surrounding page in any theme. + /// + private void CardRoot_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e) + { + var (start, _) = GetGradientColors(AccentColor); + var isLight = ActualTheme == ElementTheme.Light; + var color = isLight ? Darken(start, HoverShift) : Lighten(start, HoverShift); + CardRoot.BorderBrush = new SolidColorBrush(color); + } + + /// Restores the transparent border on pointer exit. + private void CardRoot_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e) + => CardRoot.BorderBrush = TransparentBorderBrush; + + /// Lightens an RGB colour by clamping each channel up by . + private static Windows.UI.Color Lighten(Windows.UI.Color c, int amount) + { + return Windows.UI.Color.FromArgb( + c.A, + (byte)Math.Min(255, c.R + amount), + (byte)Math.Min(255, c.G + amount), + (byte)Math.Min(255, c.B + amount)); + } + + /// Darkens an RGB colour by clamping each channel down by . + private static Windows.UI.Color Darken(Windows.UI.Color c, int amount) + { + return Windows.UI.Color.FromArgb( + c.A, + (byte)Math.Max(0, c.R - amount), + (byte)Math.Max(0, c.G - amount), + (byte)Math.Max(0, c.B - amount)); + } + + #endregion } diff --git a/src/PassKey.Desktop/Controls/EmptyStateControl.xaml b/src/PassKey.Desktop/Controls/EmptyStateControl.xaml index 68b968b..2d778d1 100644 --- a/src/PassKey.Desktop/Controls/EmptyStateControl.xaml +++ b/src/PassKey.Desktop/Controls/EmptyStateControl.xaml @@ -8,11 +8,19 @@ VerticalAlignment="Center" Spacing="16"> - + + + + Identifies the dependency property. + public static readonly DependencyProperty AccentBrushProperty = + DependencyProperty.Register(nameof(AccentBrush), typeof(Brush), typeof(EmptyStateControl), + new PropertyMetadata(null, OnAccentBrushChanged)); + /// Identifies the dependency property. public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(EmptyStateControl), @@ -57,6 +63,13 @@ public sealed partial class EmptyStateControl : UserControl /// public string Icon { get => (string)GetValue(IconProperty); set => SetValue(IconProperty, value); } + /// + /// Gets or sets the accent brush used to tint the circular badge behind the icon and + /// the glyph itself. When unset, a neutral secondary-text brush is used. Each list view + /// passes its own section brush (Passwords, Cards, Identities, Notes). + /// + public Brush? AccentBrush { get => (Brush?)GetValue(AccentBrushProperty); set => SetValue(AccentBrushProperty, value); } + /// Gets or sets the primary heading text displayed below the icon. public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); } @@ -94,6 +107,16 @@ public EmptyStateControl() // Buttons start hidden; become visible only when their text is set (see callbacks below). PrimaryButton.Visibility = Visibility.Collapsed; SecondaryButton.Visibility = Visibility.Collapsed; + + // Neutral fallback tint until a section AccentBrush is assigned (theme-aware). + ApplyBadgeBrush((Brush)Application.Current.Resources["MutedTextBrush"]); + } + + /// Applies the supplied brush to both the badge circle and the glyph. + private void ApplyBadgeBrush(Brush brush) + { + BadgeCircle.Fill = brush; + IconElement.Foreground = brush; } // ─── Property Changed Callbacks ─────────────────────────────────────────── @@ -104,6 +127,12 @@ private static void OnIconChanged(DependencyObject d, DependencyPropertyChangedE self.IconElement.Glyph = (string)e.NewValue; } + private static void OnAccentBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is EmptyStateControl self && e.NewValue is Brush brush) + self.ApplyBadgeBrush(brush); + } + private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is EmptyStateControl self) diff --git a/src/PassKey.Desktop/Controls/SecureInputBox.xaml b/src/PassKey.Desktop/Controls/SecureInputBox.xaml index e841b7c..056cc2d 100644 --- a/src/PassKey.Desktop/Controls/SecureInputBox.xaml +++ b/src/PassKey.Desktop/Controls/SecureInputBox.xaml @@ -20,6 +20,7 @@ + PointerExited="RevealButton_PointerExited"> diff --git a/src/PassKey.Desktop/Converters/ActionIndicatorConverter.cs b/src/PassKey.Desktop/Converters/ActionIndicatorConverter.cs new file mode 100644 index 0000000..bc15edb --- /dev/null +++ b/src/PassKey.Desktop/Converters/ActionIndicatorConverter.cs @@ -0,0 +1,55 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Media; + +namespace PassKey.Desktop.Converters; + +/// +/// Maps a raw activity-action string ("Created", "Modified", "Deleted", …) to either a +/// Segoe MDL2 glyph or a semantic , selected by the converter parameter +/// ("Glyph" or "Brush"). +/// +/// +/// Used by the recent-activity list (Dashboard) and the activity-log viewer to give each +/// action an accessible indicator — shape and colour, not colour alone — so a +/// destructive "Deleted" is instantly distinguishable from a constructive "Created". +/// +public sealed class ActionIndicatorConverter : IValueConverter +{ + /// + /// Converts the action string to a glyph or brush. + /// + /// The raw action string. + /// Unused. + /// Pass "Brush" for the semantic colour; anything else yields the glyph. + /// Unused. + public object Convert(object value, Type targetType, object parameter, string language) + { + var action = value as string ?? string.Empty; + var wantBrush = parameter is string s && s.Equals("Brush", StringComparison.OrdinalIgnoreCase); + + if (wantBrush) + { + var key = action switch + { + "Created" => "StatAddedBrush", + "Modified" or "Updated" => "StatModifiedBrush", + "Deleted" => "StatRemovedBrush", + _ => "TextFillColorSecondaryBrush" + }; + return (Brush)Application.Current.Resources[key]; + } + + return action switch + { + "Created" => "", // Add + "Modified" or "Updated" => "", // Edit + "Deleted" => "", // Delete + _ => "" // History (neutral — Copied/Unlocked/Locked/…) + }; + } + + /// Not supported. Throws . + public object ConvertBack(object value, Type targetType, object parameter, string language) + => throw new NotSupportedException(); +} diff --git a/src/PassKey.Desktop/Helpers/CrackTimeFormatter.cs b/src/PassKey.Desktop/Helpers/CrackTimeFormatter.cs new file mode 100644 index 0000000..e622c67 --- /dev/null +++ b/src/PassKey.Desktop/Helpers/CrackTimeFormatter.cs @@ -0,0 +1,41 @@ +using Microsoft.Windows.ApplicationModel.Resources; + +namespace PassKey.Desktop.Helpers; + +/// +/// Localizes the brute-force crack-time tokens produced by +/// PasswordStrengthAnalyzer.EstimateCrackTime (e.g. "instant", "5 minutes", +/// "12 thousandyears", "trillionyears"). Shared by the Generator and Verifier pages so +/// the two always show consistent wording. +/// +internal static class CrackTimeFormatter +{ + private static readonly ResourceLoader Res = new(); + + public static string Localize(string token) => token switch + { + "instant" => Res.GetString("CrackTimeInstant"), + "seconds" => Res.GetString("CrackTimeSeconds"), + "trillionyears" => Res.GetString("CrackTimeTrillionYears"), + _ => LocalizeWithNumber(token) + }; + + private static string LocalizeWithNumber(string token) + { + var parts = token.Split(' ', 2); + if (parts.Length != 2) return token; + + var number = parts[0]; + return parts[1].ToLowerInvariant() switch + { + "minutes" or "minute" => string.Format(Res.GetString("CrackTimeMinutes"), number), + "hours" or "hour" => string.Format(Res.GetString("CrackTimeHours"), number), + "days" or "day" => string.Format(Res.GetString("CrackTimeDays"), number), + "years" or "year" => string.Format(Res.GetString("CrackTimeYears"), number), + "thousandyears" => string.Format(Res.GetString("CrackTimeThousandYears"), number), + "millionyears" => string.Format(Res.GetString("CrackTimeMillionYears"), number), + "billionyears" => string.Format(Res.GetString("CrackTimeBillionYears"), number), + _ => token + }; + } +} diff --git a/src/PassKey.Desktop/Helpers/ListViewHelpers.cs b/src/PassKey.Desktop/Helpers/ListViewHelpers.cs deleted file mode 100644 index 728d287..0000000 --- a/src/PassKey.Desktop/Helpers/ListViewHelpers.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; - -namespace PassKey.Desktop.Helpers; - -/// -/// Shared UI helpers for list-view code-behind classes. -/// Centralises boilerplate that would otherwise be duplicated across -/// PasswordsListView, CreditCardsListView, IdentitiesListView -/// and SecureNotesListView. -/// -internal static class ListViewHelpers -{ - /// - /// Opens for 2 seconds, then closes it automatically - /// via a . - /// - /// The to show (e.g. SavedTip). - /// - /// Optional action invoked immediately after opening the tip. - /// Used by SecureNotesListView to trigger the accessibility announcer. - /// - internal static void ShowSavedToast(TeachingTip tip, Action? extraAction = null) - { - tip.IsOpen = true; - extraAction?.Invoke(); - - var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; - timer.Tick += (s, _) => - { - tip.IsOpen = false; - ((DispatcherTimer)s!).Stop(); - }; - timer.Start(); - } -} diff --git a/src/PassKey.Desktop/MainWindow.xaml b/src/PassKey.Desktop/MainWindow.xaml index 9b739c1..cb28e9a 100644 --- a/src/PassKey.Desktop/MainWindow.xaml +++ b/src/PassKey.Desktop/MainWindow.xaml @@ -5,21 +5,42 @@ xmlns:tb="using:H.NotifyIcon" Title="PassKey"> - + + + + + + + + + + + diff --git a/src/PassKey.Desktop/MainWindow.xaml.cs b/src/PassKey.Desktop/MainWindow.xaml.cs index 9833409..b8ae5c6 100644 --- a/src/PassKey.Desktop/MainWindow.xaml.cs +++ b/src/PassKey.Desktop/MainWindow.xaml.cs @@ -2,7 +2,9 @@ using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI; +using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; +using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Desktop.Services; using PassKey.Desktop.ViewModels; using PassKey.Desktop.Views; @@ -13,7 +15,9 @@ namespace PassKey.Desktop; public sealed partial class MainWindow : Window { private readonly MainViewModel _mainViewModel; + private readonly ResourceLoader _resourceLoader = new(); private bool _initialized; + private bool _closePromptOpen; // Comandi tray: x:Bind li risolve a compile-time sulla MainWindow, evitando // il routing XAML degli eventi Click (che non funziona dal popup di H.NotifyIcon). @@ -34,8 +38,16 @@ public MainWindow() InitializeComponent(); Title = "PassKey"; + + // Localized tray context-menu items (MainWindow.xaml has no x:Uid resource map). + TrayShowItem.Text = _resourceLoader.GetString("TrayMenuShow"); + TrayLockItem.Text = _resourceLoader.GetString("TrayMenuLock"); + TrayExitItem.Text = _resourceLoader.GetString("TrayMenuExit"); AppWindow.SetIcon(System.IO.Path.Combine(AppContext.BaseDirectory, "Assets", "PassKey.ico")); + // Open centered on the monitor the window is created on (FU: startup centering). + CenterOnScreen(); + // Imposta l'icona tray con percorso assoluto (AppContext.BaseDirectory). // NON usare path relativo in XAML: H.NotifyIcon risolve tramite File.OpenRead() // relativo alla working directory del processo (C:\Windows\System32 in produzione). @@ -58,27 +70,53 @@ public MainWindow() TrayIcon.DoubleClickCommand = new RelayCommand(RestoreWindow); } + /// Centers the window on the work area of the display it is created on. + private void CenterOnScreen() + { + var area = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest); + if (area is null) return; + + var work = area.WorkArea; + var size = AppWindow.Size; + var x = work.X + System.Math.Max(0, (work.Width - size.Width) / 2); + var y = work.Y + System.Math.Max(0, (work.Height - size.Height) / 2); + AppWindow.Move(new Windows.Graphics.PointInt32(x, y)); + } + private async void OnWindowClosing(Microsoft.UI.Windowing.AppWindow sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs args) { args.Cancel = true; - var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog + // Re-entrancy guard: repeated clicks on the X must not stack multiple close prompts. + if (_closePromptOpen) return; + _closePromptOpen = true; + try { - Title = "PassKey", - Content = "Vuoi mantenere PassKey attivo in background?", - PrimaryButtonText = "Minimizza", - SecondaryButtonText = "Chiudi PassKey", - CloseButtonText = "Annulla", - DefaultButton = Microsoft.UI.Xaml.Controls.ContentDialogButton.Primary, - XamlRoot = Content.XamlRoot - }; + var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog + { + Title = "PassKey", + Content = _resourceLoader.GetString("TrayCloseContent"), + PrimaryButtonText = _resourceLoader.GetString("TrayCloseMinimize"), + SecondaryButtonText = _resourceLoader.GetString("TrayCloseExit"), + CloseButtonText = _resourceLoader.GetString("CancelButton"), + DefaultButton = Microsoft.UI.Xaml.Controls.ContentDialogButton.Primary, + XamlRoot = Content.XamlRoot + }; - var result = await dialog.ShowAsync(); + // Route through the shared dialog queue so it never collides with another open + // ContentDialog ("Only a single ContentDialog can be open at any time"). + var dialogQueue = App.Services.GetRequiredService(); + var result = await dialogQueue.EnqueueAndWait(() => dialog.ShowAsync().AsTask()); - if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Primary) - AppWindow.Hide(); - else if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Secondary) - Application.Current.Exit(); + if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Primary) + AppWindow.Hide(); + else if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Secondary) + Application.Current.Exit(); + } + finally + { + _closePromptOpen = false; + } } private async void OnActivated(object sender, WindowActivatedEventArgs args) @@ -96,6 +134,13 @@ private async void OnActivated(object sender, WindowActivatedEventArgs args) // about to be shown, is always safe. App.Services.GetRequiredService().XamlRootAccessor = () => Content?.XamlRoot; + // Bind the toast service to the bottom-right InfoBar declared in MainWindow.xaml. + // Done here (not in the constructor) so the control is fully realised in the visual tree. + App.Services.GetRequiredService().Attach(ToastHost); + + // Start inactivity monitoring (must run on the UI thread so the timer binds correctly). + App.Services.GetRequiredService().Initialize(); + try { await _mainViewModel.InitializeAsync(); @@ -188,6 +233,25 @@ private void OnCurrentPageChanged(ObservableObject? viewModel) }; RootPresenter.Content = view; + + // Fade-in animation for the new view (300ms) + if (view != null) + { + view.Opacity = 0; + var fadeInStoryboard = new Microsoft.UI.Xaml.Media.Animation.Storyboard(); + var fadeInAnimation = new Microsoft.UI.Xaml.Media.Animation.DoubleAnimation + { + From = 0, + To = 1, + Duration = new TimeSpan(0, 0, 0, 0, 300), + EasingFunction = new Microsoft.UI.Xaml.Media.Animation.QuadraticEase { EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut } + }; + + Microsoft.UI.Xaml.Media.Animation.Storyboard.SetTarget(fadeInAnimation, view); + Microsoft.UI.Xaml.Media.Animation.Storyboard.SetTargetProperty(fadeInAnimation, "Opacity"); + fadeInStoryboard.Children.Add(fadeInAnimation); + fadeInStoryboard.Begin(); + } } private static LoginView CreateLoginView(LoginViewModel vm) diff --git a/src/PassKey.Desktop/PassKey.Desktop.csproj b/src/PassKey.Desktop/PassKey.Desktop.csproj index b97d6f9..623f969 100644 --- a/src/PassKey.Desktop/PassKey.Desktop.csproj +++ b/src/PassKey.Desktop/PassKey.Desktop.csproj @@ -14,9 +14,9 @@ None false PassKey.Desktop - 1.0.17 - 1.0.17.0 - 1.0.17.0 + 2.0.0 + 2.0.0.0 + 2.0.0.0 app.manifest Assets\PassKey.ico DISABLE_XAML_GENERATED_MAIN @@ -25,6 +25,7 @@ + @@ -65,11 +66,7 @@ <_BrowserHostExe>$(MSBuildProjectDirectory)\..\PassKey.BrowserHost\bin\$(Configuration)\net10.0\win-x64\PassKey.BrowserHost.exe - + diff --git a/src/PassKey.Desktop/Program.cs b/src/PassKey.Desktop/Program.cs index 82b575b..dd04a19 100644 --- a/src/PassKey.Desktop/Program.cs +++ b/src/PassKey.Desktop/Program.cs @@ -27,6 +27,11 @@ public static void Main(string[] args) EventWaitHandle? showEvent = null; bool isFirstInstance = true; + // Set by SettingsView when restarting for a language change. The previous + // instance is still shutting down, so we must not treat it as a rival instance. + bool isLanguageRestart = args.Any( + a => string.Equals(a, "--restart", StringComparison.OrdinalIgnoreCase)); + try { showEvent = new EventWaitHandle( @@ -41,6 +46,15 @@ public static void Main(string[] args) // treat as first instance so the app always starts. } + if (!isFirstInstance && isLanguageRestart) + { + // Language-change restart: the previous instance owns the single-instance + // event but is exiting. Release our handle, then poll until the event is + // gone (previous instance fully terminated) and claim ownership ourselves. + showEvent?.Dispose(); + showEvent = WaitForSingleInstanceOwnership(out isFirstInstance); + } + if (!isFirstInstance) { try { showEvent?.Set(); } catch { } @@ -128,6 +142,45 @@ private static void MonitorShowEvent(EventWaitHandle showEvent) } } + /// + /// Polls for ownership of the single-instance named event, giving the previous + /// instance time to terminate during a language-change restart. Returns the owned + /// handle, or null if ownership could not be claimed within the timeout + /// (in which case the caller proceeds as a fresh first instance regardless). + /// + private static EventWaitHandle? WaitForSingleInstanceOwnership(out bool acquired) + { + acquired = false; + + // Poll for up to ~5 seconds — the previous instance only needs to finish + // process teardown after Application.Exit(), which is near-instant in practice. + for (int attempt = 0; attempt < 50; attempt++) + { + Thread.Sleep(100); + try + { + var handle = new EventWaitHandle( + initialState: false, + mode: EventResetMode.AutoReset, + name: ShowEventName, + createdNew: out acquired); + + if (acquired) + return handle; + + handle.Dispose(); + } + catch + { + // Transient failure — keep polling. + } + } + + // Timed out: start anyway so the app is never left unable to launch. + acquired = true; + return null; + } + private static void WriteCrashLog(Exception ex) { try diff --git a/src/PassKey.Desktop/Services/AutoLockService.cs b/src/PassKey.Desktop/Services/AutoLockService.cs new file mode 100644 index 0000000..f3042bf --- /dev/null +++ b/src/PassKey.Desktop/Services/AutoLockService.cs @@ -0,0 +1,165 @@ +using System.Runtime.InteropServices; +using Microsoft.UI.Dispatching; +using Microsoft.Windows.ApplicationModel.Resources; + +namespace PassKey.Desktop.Services; + +/// +/// Default implementation. Runs a single always-on +/// per-second timer that locks the vault once the machine has been idle longer than +/// , warning the user with a "stay active" +/// toast 60 s and 30 s beforehand. +/// +/// +/// +/// Why an always-on timer instead of arming on VaultUnlocked? That event is +/// raised on whatever thread completed the unlock — for the login path a thread-pool thread, +/// since UnlockAsync runs inside Task.Run. Creating/starting a +/// off the UI thread does not work. Creating the timer +/// once in (guaranteed to run on the UI thread) and simply checking +/// on every tick removes that entire class of bug. +/// +/// +/// Why poll GetLastInputInfo? WinUI 3 exposes no global "last input" signal, +/// and tracking input only on PassKey's own windows would miss a user active elsewhere. The +/// Win32 API gives a true machine-wide idle measurement — the correct basis for a security +/// auto-lock. +/// +/// +public sealed class AutoLockService : IAutoLockService, IDisposable +{ + [StructLayout(LayoutKind.Sequential)] + private struct LastInputInfo + { + public uint CbSize; + public uint DwTime; + } + + // Classic DllImport (not LibraryImport): the source-generated variant requires + // and AOT, neither of which this project uses. + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetLastInputInfo(ref LastInputInfo plii); + + /// Below this idle time (seconds) the user is considered freshly active. + private const int ActivityThresholdSeconds = 5; + + private readonly IVaultStateService _vaultState; + private readonly ISettingsService _settings; + private readonly IToastService _toast; + private readonly ResourceLoader _resourceLoader = new(); + + private DispatcherQueueTimer? _timer; + private bool _warned60; + private bool _warned30; + private bool _warned10; + + public AutoLockService(IVaultStateService vaultState, ISettingsService settings, IToastService toast) + { + _vaultState = vaultState; + _settings = settings; + _toast = toast; + } + + /// + public void Initialize() + { + // Called from MainWindow activation → guaranteed UI thread, so the timer is bound + // to the correct DispatcherQueue. It then runs for the whole process lifetime; + // OnTick is a no-op whenever the vault is locked. + var dispatcher = DispatcherQueue.GetForCurrentThread(); + if (dispatcher is null) return; + + _timer = dispatcher.CreateTimer(); + _timer.Interval = TimeSpan.FromSeconds(1); + _timer.IsRepeating = true; + _timer.Tick += OnTick; + _timer.Start(); + } + + private void OnTick(DispatcherQueueTimer sender, object args) + { + // Idle only matters while the vault is open; reset the one-shot warnings so a + // fresh idle period after the next unlock starts clean. + if (!_vaultState.IsUnlocked) + { + _warned60 = false; + _warned30 = false; + _warned10 = false; + return; + } + + var limit = _settings.AutoLockSeconds; + if (limit <= 0) return; // 0 = auto-lock disabled + + var idle = GetIdleSeconds(); + + // Recent activity re-arms the one-shot warnings for the next idle stretch. + if (idle < ActivityThresholdSeconds) + { + _warned60 = false; + _warned30 = false; + _warned10 = false; + } + + var remaining = limit - idle; + + if (remaining <= 0) + { + _warned60 = false; + _warned30 = false; + _warned10 = false; + _vaultState.Lock(); + return; + } + + // Warnings only make sense when the timeout is long enough to have a "before" window. + if (remaining <= 30 && !_warned30 && limit > 30) + { + _warned30 = true; + ShowCountdownToast(30); + } + else if (remaining <= 60 && !_warned60 && limit > 60) + { + _warned60 = true; + ShowCountdownToast(60); + } + // Short timeout (≤30 s) has no room for a 30/60 s warning; give it a final 10 s + // heads-up so every tier keeps a consistent "stay active" prompt before locking. + else if (remaining <= 10 && !_warned10 && limit > 10 && limit <= 30) + { + _warned10 = true; + ShowCountdownToast(10); + } + } + + private void ShowCountdownToast(int seconds) + { + var message = string.Format(_resourceLoader.GetString("AutoLockCountdown"), seconds); + _toast.Show( + ToastSeverity.Warning, + message, + title: null, + actionLabel: _resourceLoader.GetString("AutoLockStayActive"), + // The click itself is user input, which resets the system idle timer — so the + // countdown restarts on its own. The callback only needs to exist to render the button. + actionCallback: static () => { }); + } + + /// Returns whole seconds since the last system-wide user input. + private static int GetIdleSeconds() + { + var info = new LastInputInfo { CbSize = (uint)Marshal.SizeOf() }; + if (!GetLastInputInfo(ref info)) return 0; + + // Unsigned subtraction handles the ~24.9-day GetTickCount wraparound correctly. + var idleMs = unchecked((uint)Environment.TickCount - info.DwTime); + return (int)(idleMs / 1000); + } + + public void Dispose() + { + _timer?.Stop(); + _timer = null; + } +} diff --git a/src/PassKey.Desktop/Services/FilePickerService.cs b/src/PassKey.Desktop/Services/FilePickerService.cs index d42b2c1..d0db7ed 100644 --- a/src/PassKey.Desktop/Services/FilePickerService.cs +++ b/src/PassKey.Desktop/Services/FilePickerService.cs @@ -39,11 +39,16 @@ public sealed class FilePickerService : IFilePickerService /// /// The absolute path of the selected file, or null if the dialog was cancelled. /// - public async Task PickOpenFileAsync(string extension, string extensionDescription) + public Task PickOpenFileAsync(string extension, string extensionDescription) + => PickOpenFileAsync([extension], extensionDescription); + + /// + public async Task PickOpenFileAsync(IReadOnlyList extensions, string extensionDescription) { var picker = new FileOpenPicker(); InitializePicker(picker); - picker.FileTypeFilter.Add(extension); + foreach (var extension in extensions) + picker.FileTypeFilter.Add(extension); var file = await picker.PickSingleFileAsync(); return file?.Path; diff --git a/src/PassKey.Desktop/Services/IAutoLockService.cs b/src/PassKey.Desktop/Services/IAutoLockService.cs new file mode 100644 index 0000000..3c08184 --- /dev/null +++ b/src/PassKey.Desktop/Services/IAutoLockService.cs @@ -0,0 +1,20 @@ +namespace PassKey.Desktop.Services; + +/// +/// Locks the vault automatically after a configurable period of system inactivity, +/// warning the user with a countdown toast shortly before the lock fires. +/// +/// +/// The inactivity period is read live from +/// (a value of 0 disables auto-lock). Inactivity is measured system-wide via the +/// Win32 GetLastInputInfo API, so the vault stays unlocked while the user is active +/// in any application and locks once the machine has been genuinely idle. +/// +public interface IAutoLockService +{ + /// + /// Starts monitoring. Must be called once at application startup on the UI thread. + /// The service then arms itself whenever the vault is unlocked and disarms on lock. + /// + void Initialize(); +} diff --git a/src/PassKey.Desktop/Services/IFilePickerService.cs b/src/PassKey.Desktop/Services/IFilePickerService.cs index 9046c59..6550cee 100644 --- a/src/PassKey.Desktop/Services/IFilePickerService.cs +++ b/src/PassKey.Desktop/Services/IFilePickerService.cs @@ -22,4 +22,13 @@ public interface IFilePickerService /// Human-readable description for the extension filter. /// The chosen file path, or null if cancelled. Task PickOpenFileAsync(string extension, string extensionDescription); + + /// + /// Opens an Open File dialog accepting any of several extensions (e.g. a Bitwarden + /// export that may be .json or a .zip with attachments — FU3). + /// + /// File extension filters, each including the leading dot. + /// Human-readable description for the filter. + /// The chosen file path, or null if cancelled. + Task PickOpenFileAsync(IReadOnlyList extensions, string extensionDescription); } diff --git a/src/PassKey.Desktop/Services/IToastService.cs b/src/PassKey.Desktop/Services/IToastService.cs new file mode 100644 index 0000000..86e0469 --- /dev/null +++ b/src/PassKey.Desktop/Services/IToastService.cs @@ -0,0 +1,76 @@ +using Microsoft.UI.Xaml.Controls; + +namespace PassKey.Desktop.Services; + +/// +/// Severity of a transient toast notification. Maps onto +/// and also drives the auto-dismiss behaviour (see ). +/// +public enum ToastSeverity +{ + /// Neutral information. Auto-dismisses after 3 seconds. + Info, + + /// Successful action confirmation. Auto-dismisses after 5 seconds. + Success, + + /// Non-blocking warning. Auto-dismisses after 5 seconds. + Warning, + + /// Error. Stays open until the user dismisses it manually. + Error, +} + +/// +/// Shows transient, non-blocking toast notifications in a single shared +/// anchored to the bottom-right of the main window. +/// +/// +/// +/// Toasts are serialised through an internal queue and a fire-and-forget pump — +/// the same deadlock-free pattern used by — so +/// rapid-fire calls (e.g. copy username then copy password) never overlap. +/// +/// +/// Auto-dismiss: closes after 3s, +/// and after 5s. +/// never auto-closes — the user must dismiss it, +/// so failures are not missed. +/// +/// +/// Host attachment: the lives in the main window visual +/// tree; is called once at window activation. Calls to +/// before attachment are silently dropped (no host to render into). +/// +/// +public interface IToastService +{ + /// + /// Binds the service to the host . Called once during + /// main-window activation. Until this is called, is a no-op. + /// + /// The bottom-right in the main window. + void Attach(InfoBar host); + + /// + /// Queues a toast for display. Safe to call from any thread — the work is + /// marshalled onto the UI thread. No-op if has not run yet. + /// + /// Severity, which also selects the auto-dismiss timeout. + /// Localised body text. + /// Optional localised title shown in bold above the message. + /// + /// Optional localised label for an action button shown inside the toast + /// (e.g. "Stay active"). When supplied, must also be set. + /// + /// + /// Invoked on the UI thread when the action button is clicked. The toast is dismissed + /// immediately afterwards. + /// + void Show( + ToastSeverity severity, + string message, + string? title = null, + string? actionLabel = null, + Action? actionCallback = null); +} diff --git a/src/PassKey.Desktop/Services/IVaultStateService.cs b/src/PassKey.Desktop/Services/IVaultStateService.cs index 38a74c8..85b7bfa 100644 --- a/src/PassKey.Desktop/Services/IVaultStateService.cs +++ b/src/PassKey.Desktop/Services/IVaultStateService.cs @@ -11,11 +11,31 @@ public interface IVaultStateService event Action? VaultLocked; Task InitializeAsync(ReadOnlyMemory masterPassword); + + /// + /// Creates a brand-new vault keyed to but pre-populated + /// with instead of an empty vault. Overwrites any existing vault + /// metadata and data. Used by the "restore backup" path on the login screen, where the + /// backup's password becomes the new master password. + /// + /// The master password for the new vault (the backup's password). + /// The decrypted vault content to persist. + /// Always true on success. + Task InitializeWithVaultAsync(ReadOnlyMemory masterPassword, Vault vault); + Task UnlockAsync(ReadOnlyMemory masterPassword); void Lock(); Task SaveVaultAsync(); Task ChangeMasterPasswordAsync(ReadOnlyMemory currentPassword, ReadOnlyMemory newPassword); + /// + /// Verifies that the supplied password matches the vault's master password, without + /// altering any state. Used to gate destructive actions such as "clear vault". + /// + /// The password to verify. + /// if the password is correct; otherwise . + Task VerifyMasterPasswordAsync(ReadOnlyMemory password); + /// /// Finds password entries matching the given URL (for browser extension IPC). /// Returns empty list if vault is locked. diff --git a/src/PassKey.Desktop/Services/IWatchtowerScanService.cs b/src/PassKey.Desktop/Services/IWatchtowerScanService.cs index 17b8b91..fa72e39 100644 --- a/src/PassKey.Desktop/Services/IWatchtowerScanService.cs +++ b/src/PassKey.Desktop/Services/IWatchtowerScanService.cs @@ -17,10 +17,11 @@ public interface IWatchtowerScanService WatchtowerResult? LastResult { get; } /// - /// Raised on the UI thread every time a single entry has been checked. The argument - /// is in [0, 1]. Useful for progress bars during long scans. + /// Raised on the UI thread every time a single entry has been checked, reporting + /// (scanned, total) so the UI can show both a determinate ring and a live + /// "X / N" count during long scans. /// - event Action? Progress; + event Action? Progress; /// Raised on the UI thread when the scan finishes (success, cancel, or error). event Action? Completed; diff --git a/src/PassKey.Desktop/Services/ImportOrchestrator.cs b/src/PassKey.Desktop/Services/ImportOrchestrator.cs index a5e158b..cbaa67d 100644 --- a/src/PassKey.Desktop/Services/ImportOrchestrator.cs +++ b/src/PassKey.Desktop/Services/ImportOrchestrator.cs @@ -70,13 +70,49 @@ private async Task ParseCsvAsync(string filePath) /// /// Reads a Bitwarden JSON export file and delegates parsing to . + /// A Bitwarden "export with attachments" is a ZIP wrapping a plaintext data.json + /// (plus an attachments/ folder PassKey doesn't handle); such files are unwrapped + /// transparently so they import like a plain JSON export (FU3). /// private async Task ParseBitwardenAsync(string filePath) { - var content = await File.ReadAllTextAsync(filePath); + var content = IsZipFile(filePath) + ? await ExtractBitwardenDataJsonAsync(filePath) + : await File.ReadAllTextAsync(filePath); + return _bitwardenImporter.ParseBitwarden(content); } + /// Returns true if the file begins with the ZIP local-file-header magic "PK". + private static bool IsZipFile(string filePath) + { + try + { + using var fs = File.OpenRead(filePath); + return fs.ReadByte() == 0x50 && fs.ReadByte() == 0x4B; // 'P','K' + } + catch + { + return false; + } + } + + /// + /// Extracts the data.json entry from a Bitwarden ZIP export. Throws an + /// with a user-facing message if it is absent. + /// + private static async Task ExtractBitwardenDataJsonAsync(string filePath) + { + await using var stream = File.OpenRead(filePath); + using var archive = new ZipArchive(stream, ZipArchiveMode.Read); + + var entry = archive.GetEntry("data.json") + ?? throw new ImportFileException("IMPORT_BW_ZIP"); + + using var reader = new StreamReader(entry.Open()); + return await reader.ReadToEndAsync(); + } + /// /// Reads a 1Password .1pux archive (ZIP), extracts the export.data JSON entry, /// and delegates parsing to . @@ -90,7 +126,7 @@ private async Task ParseOnePuxAsync(string filePath) using (var archive = new ZipArchive(stream, ZipArchiveMode.Read)) { var entry = archive.GetEntry("export.data") - ?? throw new InvalidDataException("The .1pux file does not contain 'export.data'."); + ?? throw new ImportFileException("IMPORT_1PUX"); using var reader = new StreamReader(entry.Open()); exportDataJson = await reader.ReadToEndAsync(); @@ -113,24 +149,59 @@ private Task ParseKdbxAsync(string filePath, string password) { return Task.Run(() => { + // FU3: a KeePass 1.x database (.kdb) shares the first four signature bytes with + // KDBX but differs on the 5th (0x65 vs 0x67). KeePassLib cannot read it, so give + // a clear, actionable message instead of an opaque library exception. + if (IsKeePass1File(filePath)) + throw new ImportFileException("IMPORT_KEEPASS_1X"); + var ioConnInfo = new IOConnectionInfo { Path = filePath }; var compositeKey = new CompositeKey(); compositeKey.AddUserKey(new KcpPassword(password)); var db = new PwDatabase(); - db.Open(ioConnInfo, compositeKey, null); - try { + db.Open(ioConnInfo, compositeKey, null); return MapKdbxToVault(db); } + catch (ImportFileException) + { + throw; + } + catch (Exception) + { + // Wrong password or a file that isn't a valid KDBX 2.x database. + throw new ImportFileException("IMPORT_KEEPASS_OPEN"); + } finally { - db.Close(); + try { db.Close(); } catch { /* never opened, or already closed */ } } }); } + /// + /// Detects the legacy KeePass 1.x (.kdb) file format by its 8-byte signature + /// 03 D9 A2 9A 65 FB 4B B5 — identical to KDBX except the 5th byte is 0x65 + /// (KDBX uses 0x67). + /// + private static bool IsKeePass1File(string filePath) + { + try + { + Span sig = stackalloc byte[8]; + using var fs = File.OpenRead(filePath); + if (fs.Read(sig) < 8) return false; + return sig[0] == 0x03 && sig[1] == 0xD9 && sig[2] == 0xA2 && sig[3] == 0x9A + && sig[4] == 0x65 && sig[5] == 0xFB && sig[6] == 0x4B && sig[7] == 0xB5; + } + catch + { + return false; + } + } + /// /// Maps all non-recycled entries in a to a . /// Entries in the Recycle Bin group are skipped. Completely empty entries (no title, diff --git a/src/PassKey.Desktop/Services/ToastService.cs b/src/PassKey.Desktop/Services/ToastService.cs new file mode 100644 index 0000000..94f6384 --- /dev/null +++ b/src/PassKey.Desktop/Services/ToastService.cs @@ -0,0 +1,156 @@ +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml.Controls; + +namespace PassKey.Desktop.Services; + +/// +/// Default implementation. Drives a single shared +/// through a serial queue so toasts never overlap. +/// +/// +/// The pump is fire-and-forget and guarded by , mirroring +/// . Every toast — auto-dismissed or manually closed — +/// completes via the event, so the pump advances on a +/// single, uniform signal regardless of how the toast was dismissed. +/// +public sealed class ToastService : IToastService +{ + private static readonly TimeSpan InfoDuration = TimeSpan.FromSeconds(3); + private static readonly TimeSpan SuccessDuration = TimeSpan.FromSeconds(5); + private static readonly TimeSpan WarningDuration = TimeSpan.FromSeconds(5); + + /// Gap left between consecutive toasts so the close/open transition is visible. + private static readonly TimeSpan InterToastGap = TimeSpan.FromMilliseconds(150); + + private readonly Queue _queue = new(); + private InfoBar? _host; + private DispatcherQueue? _dispatcher; + private bool _isPumping; + + /// + public void Attach(InfoBar host) + { + _host = host; + _dispatcher = host.DispatcherQueue; + } + + /// + public void Show( + ToastSeverity severity, + string message, + string? title = null, + string? actionLabel = null, + Action? actionCallback = null) + { + var dispatcher = _dispatcher; + if (dispatcher is null) return; // Attach() not called yet — nothing to render into. + + // Marshal onto the UI thread: the queue and pump are single-threaded by design. + dispatcher.TryEnqueue(() => + { + _queue.Enqueue(new ToastItem(severity, message, title, actionLabel, actionCallback)); + _ = PumpAsync(); + }); + } + + /// + /// Drains the queue sequentially. Only one pump runs at a time; concurrent + /// calls return immediately via the guard. + /// + private async Task PumpAsync() + { + if (_isPumping) return; + _isPumping = true; + + try + { + while (_queue.TryDequeue(out var item)) + { + await ShowOneAsync(item); + await Task.Delay(InterToastGap); + } + } + finally + { + _isPumping = false; + } + } + + /// + /// Shows a single toast and completes when it is dismissed — either by the + /// auto-dismiss timer (Info/Success/Warning) or by the user (always allowed, + /// and the only way to dismiss an Error). + /// + private async Task ShowOneAsync(ToastItem item) + { + var host = _host; + if (host is null) return; + + host.Title = item.Title ?? string.Empty; + host.Message = item.Message; + host.Severity = item.Severity switch + { + ToastSeverity.Success => InfoBarSeverity.Success, + ToastSeverity.Warning => InfoBarSeverity.Warning, + ToastSeverity.Error => InfoBarSeverity.Error, + _ => InfoBarSeverity.Informational, + }; + host.IsClosable = true; // user can always dismiss early + + // Optional inline action button (e.g. "Stay active" on the auto-lock warning). + if (item.ActionLabel is not null && item.ActionCallback is not null) + { + var actionButton = new Button { Content = item.ActionLabel }; + actionButton.Click += (_, _) => + { + item.ActionCallback(); + host.IsOpen = false; + }; + host.ActionButton = actionButton; + } + else + { + host.ActionButton = null; + } + + // A single TaskCompletionSource resolved by the Closed event unifies both + // dismiss paths: the auto-dismiss timer sets IsOpen=false (which raises + // Closed), and the user clicking the close button raises Closed directly. + var dismissed = new TaskCompletionSource(); + Windows.Foundation.TypedEventHandler? onClosed = null; + onClosed = (sender, _) => + { + sender.Closed -= onClosed; + dismissed.TrySetResult(); + }; + host.Closed += onClosed; + + host.IsOpen = true; + + if (item.Severity != ToastSeverity.Error) + { + var duration = item.Severity switch + { + ToastSeverity.Success => SuccessDuration, + ToastSeverity.Warning => WarningDuration, + _ => InfoDuration, + }; + + // Fire-and-forget timer: close the bar after the timeout. If the user + // already closed it, setting IsOpen=false again is a harmless no-op. + _ = Task.Delay(duration).ContinueWith( + _ => _dispatcher?.TryEnqueue(() => { if (host.IsOpen) host.IsOpen = false; }), + TaskScheduler.Default); + } + + await dismissed.Task; + } + + /// A queued toast: severity, text, and an optional inline action button. + private readonly record struct ToastItem( + ToastSeverity Severity, + string Message, + string? Title, + string? ActionLabel = null, + Action? ActionCallback = null); +} diff --git a/src/PassKey.Desktop/Services/VaultStateService.cs b/src/PassKey.Desktop/Services/VaultStateService.cs index 2088d60..e39d465 100644 --- a/src/PassKey.Desktop/Services/VaultStateService.cs +++ b/src/PassKey.Desktop/Services/VaultStateService.cs @@ -50,12 +50,27 @@ public VaultStateService(IVaultService vaultService, IVaultRepository repository /// /// The master password used to derive the KEK. /// Always true on success. - public async Task InitializeAsync(ReadOnlyMemory masterPassword) + public Task InitializeAsync(ReadOnlyMemory masterPassword) + => InitializeWithVaultAsync(masterPassword, new Vault()); + + /// + /// Creates a new vault from pre-populated with + /// , persists metadata and the encrypted blob, and transitions + /// the service to the unlocked state. delegates here with + /// an empty vault; the login-screen "restore backup" path passes the decrypted backup. + /// + /// The master password used to derive the KEK. + /// The vault content to persist (empty for first-run setup). + /// Always true on success. + public async Task InitializeWithVaultAsync(ReadOnlyMemory masterPassword, Vault vault) { var (metadata, dek) = _vaultService.InitializeVault(masterPassword.Span); + + // Replace any DEK from a prior unlocked session before adopting the new one. + _dek?.Dispose(); _dek = dek; - CurrentVault = new Vault(); + CurrentVault = vault; var encrypted = _vaultService.EncryptVault(CurrentVault, _dek.ReadOnlySpan); await _repository.SaveMetadataAsync(metadata); @@ -190,6 +205,33 @@ public async Task ChangeMasterPasswordAsync(ReadOnlyMemory currentPa return true; } + /// + /// Verifies the supplied password against the stored vault metadata by attempting a + /// key derivation + unwrap. No state is changed; the candidate key is zeroed immediately. + /// + /// The password to verify. + /// when the password is correct; otherwise . + public async Task VerifyMasterPasswordAsync(ReadOnlyMemory password) + { + var metadata = await _repository.LoadMetadataAsync(); + if (metadata is null) return false; + + PinnedSecureBuffer? verifyDek = null; + try + { + verifyDek = _vaultService.UnlockVault(password.Span, metadata); + return true; + } + catch + { + return false; + } + finally + { + verifyDek?.Dispose(); + } + } + /// /// Searches the current vault for password entries whose URL matches the given URL. /// Returns an empty list when the vault is locked. diff --git a/src/PassKey.Desktop/Services/WatchtowerScanService.cs b/src/PassKey.Desktop/Services/WatchtowerScanService.cs index 91654b9..2a4f955 100644 --- a/src/PassKey.Desktop/Services/WatchtowerScanService.cs +++ b/src/PassKey.Desktop/Services/WatchtowerScanService.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Microsoft.UI.Dispatching; using PassKey.Core.Models; using PassKey.Core.Services; @@ -11,9 +12,11 @@ namespace PassKey.Desktop.Services; /// kept in . /// /// -/// Throttling. HIBP's free tier has no hard rate limit but the courtesy -/// guidance is "no more than ~10 req/s". We stay conservative at 5 req/s (one call -/// every 200 ms). +/// Concurrency. HIBP checks are network-bound, so they run with a bounded +/// degree of parallelism () instead of one-at-a-time with a +/// courtesy delay — the old sequential path made a 1000-password vault take ~6 minutes. +/// The HIBP "range" (k-anonymity) endpoint is CDN-backed and built for volume, so a small +/// fixed concurrency is safe and well-behaved. /// Caching. The result is cached for 24 hours; calls within that window /// return the cached value unless the caller passes forceRefresh: true. The /// field persists the cache anchor @@ -25,7 +28,9 @@ namespace PassKey.Desktop.Services; public sealed class WatchtowerScanService : IWatchtowerScanService { private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(24); - private static readonly TimeSpan HibpThrottle = TimeSpan.FromMilliseconds(200); // 5 req/s + + /// Maximum number of concurrent HIBP requests during a scan. + private const int HibpConcurrency = 8; private readonly IVaultStateService _vaultState; private readonly IPasswordStrengthAnalyzer _strengthAnalyzer; @@ -36,7 +41,7 @@ public sealed class WatchtowerScanService : IWatchtowerScanService public bool IsScanning { get; private set; } public WatchtowerResult? LastResult { get; private set; } - public event Action? Progress; + public event Action? Progress; public event Action? Completed; public WatchtowerScanService( @@ -106,20 +111,31 @@ private async Task RunScanAsync(Vault vault, CancellationToken .SelectMany(kv => kv.Value) .ToHashSet(); - var compromised = new List(); - var weak = new List(); - var duplicates = new List(); - int totalScore = 0, weakCount = 0; + var compromised = new ConcurrentBag(); + var weak = new ConcurrentBag(); + var duplicates = new ConcurrentBag(); + int totalScore = 0, weakCount = 0, scanned = 0; - for (int i = 0; i < total; i++) + // HIBP checks are network-bound: run them with bounded concurrency instead of + // sequentially with a courtesy delay. When HIBP is disabled the work is purely + // local (cheap strength + duplicate pass), so a single worker is enough. + var options = new ParallelOptions { - ct.ThrowIfCancellationRequested(); - var p = passwords[i]; + MaxDegreeOfParallelism = hibpEnabled ? HibpConcurrency : 1, + CancellationToken = ct + }; + await Parallel.ForEachAsync(passwords, options, async (p, token) => + { + // Local strength score — computed synchronously before any await so the Span + // input never has to cross the await boundary. var strength = _strengthAnalyzer.Analyze(p.Password.AsSpan()); - totalScore += strength.Score; - bool isWeak = strength.Score < 40; - if (isWeak) weakCount++; + int score = strength.Score; + string label = strength.Label; + + Interlocked.Add(ref totalScore, score); + bool isWeak = score < 40; + if (isWeak) Interlocked.Increment(ref weakCount); bool isDup = duplicateIds.Contains(p.Id); int breachCount = 0; @@ -127,7 +143,7 @@ private async Task RunScanAsync(Vault vault, CancellationToken { try { - breachCount = await _hibp.CheckPasswordAsync(p.Password, ct).ConfigureAwait(false); + breachCount = await _hibp.CheckPasswordAsync(p.Password, token).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (Exception ex) @@ -137,16 +153,14 @@ private async Task RunScanAsync(Vault vault, CancellationToken System.Diagnostics.Debug.WriteLine( $"[Watchtower] HIBP check failed for entry {p.Id}: {ex.GetType().Name}: {ex.Message}"); } - // Courtesy throttle between successive HIBP calls — only when we actually called. - if (i < total - 1) await Task.Delay(HibpThrottle, ct).ConfigureAwait(false); } var issue = new WatchtowerIssue( EntryId: p.Id, Title: p.Title, Username: p.Username, - StrengthScore: strength.Score, - StrengthLabel: strength.Label, + StrengthScore: score, + StrengthLabel: label, BreachCount: breachCount, IsDuplicate: isDup); @@ -154,8 +168,10 @@ private async Task RunScanAsync(Vault vault, CancellationToken if (isWeak) weak.Add(issue); if (isDup) duplicates.Add(issue); - RaiseProgress((double)(i + 1) / Math.Max(1, total)); - } + // Report live progress (X of total) as each entry completes — order-independent. + var done = Interlocked.Increment(ref scanned); + RaiseProgress(done, total); + }).ConfigureAwait(false); var avg = total > 0 ? totalScore / total : 0; return new WatchtowerResult( @@ -165,15 +181,15 @@ private async Task RunScanAsync(Vault vault, CancellationToken DuplicateCount: duplicates.Count, HealthScore: avg, ScannedUtc: DateTime.UtcNow, - Compromised: compromised, - Weak: weak, - Duplicates: duplicates); + Compromised: compromised.ToList(), + Weak: weak.ToList(), + Duplicates: duplicates.ToList()); } - private void RaiseProgress(double pct) + private void RaiseProgress(int scanned, int total) { - if (_uiDispatcher is null) { Progress?.Invoke(pct); return; } - _uiDispatcher.TryEnqueue(() => Progress?.Invoke(pct)); + if (_uiDispatcher is null) { Progress?.Invoke(scanned, total); return; } + _uiDispatcher.TryEnqueue(() => Progress?.Invoke(scanned, total)); } private void RaiseCompleted(WatchtowerResult? result) diff --git a/src/PassKey.Desktop/Strings/de-DE/Resources.resw b/src/PassKey.Desktop/Strings/de-DE/Resources.resw index 8918eb7..39d13e7 100644 --- a/src/PassKey.Desktop/Strings/de-DE/Resources.resw +++ b/src/PassKey.Desktop/Strings/de-DE/Resources.resw @@ -183,8 +183,8 @@ + Neue Karte - - + Identität hinzufügen + + Identität hinzufügen + Neue Notiz @@ -196,50 +196,53 @@ Änderungen speichern - + Vorname - + Nachname - + + Vorname oder Nachname ist erforderlich + + Geburtsdatum - + E-Mail - + Telefon - + Straße - + Stadt - + Postleitzahl - + Bundesland - + Region - + Land Inhalt - + Persönliche Daten - + Adresse - + Dokumente @@ -249,10 +252,10 @@ Kartendetails - + Speichern - + Löschen @@ -264,9 +267,6 @@ Master-Passwort ändern - - Generieren und kopieren - Importieren @@ -305,10 +305,10 @@ Version - + Identitäten - + Sichere Notizen @@ -321,16 +321,16 @@ Zeichensätze - + Personalausweis - + Gesundheitskarte - + Führerschein - + Reisepass @@ -416,10 +416,10 @@ Weiter - Neue Karte + Karte hinzufügen - Neues Passwort + Passwort hinzufügen Abbrechen @@ -445,6 +445,15 @@ CVV/CVV2 * + + Erforderliches Feld + + + Erforderliches Feld + + + Erforderliches Feld + Gültig bis * @@ -466,6 +475,15 @@ Passwort * + + Erforderliches Feld + + + Erforderliches Feld + + + Erforderliches Feld + PIN @@ -487,6 +505,9 @@ Karten suchen... + + Identitäten suchen... + Passwörter suchen... @@ -841,7 +862,7 @@ Uebersprungen (Duplikate): {0} - Generisches CSV + Generisches CSV (.csv) Abbrechen @@ -898,7 +919,7 @@ KeePass-Masterpasswort - KeePass (.kdbx) + KeePass 2.x (.kdbx) Backup-Passwort @@ -922,7 +943,7 @@ Die Datei ist kein gueltiges PassKey-Backup. - Bitwarden (JSON) + Bitwarden (.json / .zip) Beide behalten @@ -999,127 +1020,127 @@ Gespeichert! - + Hilfe - + Tastenkürzel - + Navigation - + Zwischen Abschnitten wechseln (Dashboard, Passwörter, Karten…, Einstellungen) - + Suchen (im Dashboard) - + Tresor sperren - + Hilfe öffnen - + Aktionen - + Neues Element - + Ausgewähltes Element bearbeiten - + Entf - + Ausgewähltes Element löschen - + Abbrechen / Zurück - + Häufige Fragen - + Wie ändere ich das Hauptpasswort? - + Gehen Sie zu Einstellungen und scrollen Sie zum Bereich Sicherheit. Klicken Sie auf 'Master-Passwort ändern': Sie werden nach Ihrem aktuellen Passwort gefragt (zur Bestätigung Ihrer Identität) und dann zweimal nach dem neuen Passwort. Das neue Master-Passwort wird sofort angewendet. - + Wie erstelle ich eine Sicherungskopie des Tresors? - + Gehen Sie zu Einstellungen und scrollen Sie zum Bereich Sicherung und Import. Klicken Sie auf 'Sicherung erstellen': Eine mit Ihrem Master-Passwort verschlüsselte Datei wird erstellt. Bewahren Sie sie sicher auf. Zur Wiederherstellung nutzen Sie 'Sicherung wiederherstellen' im selben Bereich und geben Sie das beim Erstellen verwendete Master-Passwort ein. - + Wo werden meine Daten gespeichert? - + Alle PassKey-Daten werden ausschließlich auf Ihrem Gerät gespeichert, in einer SQLite-Datenbank, die mit AES-GCM 256-Bit verschlüsselt ist. Es werden keine Informationen über das Internet oder an externe Server übertragen. Der Dateipfad wird unten angezeigt. - + Wie importiere ich Passwörter aus einem anderen Manager? - - Gehen Sie zu Einstellungen und scrollen Sie zum Bereich Sicherung und Import. Klicken Sie auf 'Daten importieren' und wählen Sie das Quellformat: generisches CSV, Bitwarden JSON, 1Password (.1pux) und KeePass (.kdbx). PassKey zeigt eine Zusammenfassung der erkannten Einträge vor der endgültigen Bestätigung. + + Öffnen Sie Einstellungen, Bereich Sicherung und Import, und klicken Sie auf 'Daten importieren', dann wählen Sie das Quellformat. Unterstützte Formate: generisches CSV (.csv), kompatibel mit den meisten Managern sowie mit Exporten aus Chrome, Firefox und NordPass; Bitwarden, entweder als unverschlüsselte .json oder als .zip mit Anhängen (PassKey extrahiert die Daten automatisch); 1Password (.1pux); und KeePass 2.x (.kdbx), wofür das Datenbankkennwort abgefragt wird. Verschlüsselte Bitwarden-Exporte können nicht importiert werden (exportieren Sie sie unverschlüsselt), ebenso wenig KeePass-1.x-Datenbanken (.kdb), ein älteres Format, das Sie zuerst mit KeePass 2.x in .kdbx konvertieren müssen. Vor der endgültigen Bestätigung zeigt PassKey stets eine Zusammenfassung der erkannten Elemente. - + Über - + Sicherer Passwort-Manager für Windows - + Benutzerhandbuch - + Dashboard - + Das Dashboard ist der Hauptbildschirm von PassKey. Es zeigt eine Zusammenfassung der gespeicherten Einträge nach Typ (Passwörter, Karten, Identitäten, Notizen), Tresor-Statistiken und zuletzt geänderte Einträge. Nutzen Sie die Suchleiste (Strg+F), um schnell beliebige Einträge zu finden. - + Passwörter - + Alle im Tresor gespeicherten Passwörter anzeigen und verwalten. Klicken Sie auf einen Eintrag, um das Detailpanel zu öffnen: Dort können Sie Benutzername oder Passwort mit einem Klick kopieren, die zugehörige URL anzeigen, Felder bearbeiten oder den Eintrag mit Bestätigung löschen. - + Kreditkarten - + Kreditkarten im Tresor verwalten. Kartennummer, Inhaber, Ablaufdatum und CVV werden vollständig verschlüsselt gespeichert. Im Detailpanel können Sie die Kartennummer mit einem Klick kopieren und den CVV durch Drücken und Halten anzeigen. Die Kartenfarbe ist anpassbar. - + Identitäten - + Persönliche Identitätsdokumente und Informationen speichern: Vor- und Nachname, Adresse, Dokumenttyp und -nummer, Steuer-ID, Geburtsdatum, Telefonnummer und E-Mail-Adresse. Nützlich für das schnelle Kopieren einzelner Felder beim Ausfüllen von Formularen ohne Suche nach physischen Dokumenten. - + Sichere Notizen - + Sensible Informationen als freie Textnotizen speichern, verschlüsselt wie alle anderen Einträge. Jede Notiz hat einen Titel, eine anpassbare Farbe und ein Textfeld ohne Längenbeschränkung. Ideal für PINs, Zugangscodes, Sicherheitsfragen oder beliebige Daten, die in keine andere Kategorie passen. - + Generator - + Zufällige, kryptografisch sichere Passwörter generieren. Länge und Zeichensätze konfigurieren (Groß-/Kleinbuchstaben, Ziffern und Symbole). Die Passwortstärke wird in Echtzeit angezeigt. Ergebnis mit einem Klick in die Zwischenablage kopieren. - + Passwort-Prüfer - + Passwortsicherheit analysieren, ohne es im Tresor zu speichern. Zeigt eine Bewertung von 0 bis 4 Sternen, eine Schätzung der Crack-Zeit per Brute-Force und ob das Passwort in öffentlichen Datenleck-Datenbanken erscheint. Nützlich zur Überprüfung bereits genutzter Passwörter. - + Einstellungen - + Alle Konfigurationsoptionen zentral verwalten: Sprache, Master-Passwort, automatische Sperrverzögerung, Tresor-Sicherung und -Wiederherstellung sowie Import aus anderen Passwort-Managern (CSV, Bitwarden, 1Password, KeePass). @@ -1497,4 +1518,961 @@ Moechten Sie diese Pruefung aktivieren? Schwach - \ No newline at end of file + + Anderungen gespeichert + + + Element geloscht + + + Benutzername kopiert + + + Passwort kopiert + + + In Zwischenablage kopiert + + + Passwort vergessen + + + PassKey speichert das Master-Passwort niemals. Aus Sicherheitsgrunden (Zero-Knowledge-Modell) kennen nur Sie es. Falls Sie es vergessen haben, konnen Sie eine .pkbak-Sicherung wiederherstellen oder einen neuen Tresor erstellen. + + + Sicherung wiederherstellen + + + Neuen Tresor erstellen + + + Neuen Tresor erstellen + + + Der aktuelle Tresor und alle seine Daten werden dauerhaft unzuganglich. Dieser Vorgang kann nicht ruckgangig gemacht werden. Fortfahren? + + + Loschen und fortfahren + + + Der Tresor wird in {0} Sekunden gesperrt + + + Aktiv bleiben + + + Automatische Sperre aktualisiert + + + Verlauf + + + Letzte Tresor-Aktivität + + + CSV exportieren + + + Keine Aktivität + + + Tresor-Aktionen erscheinen hier. + + + Datum und Uhrzeit + + + Typ + + + Aktion + + + CSV-Datei + + + Verlauf exportiert + + + Export fehlgeschlagen + + + Erstellt + + + Geändert + + + Gelöscht + + + Kopiert + + + Entsperrt + + + Gesperrt + + + Passwort + + + Karte + + + Identität + + + Notiz + + + Tresor + + + Aktivitätsverlauf + + + Browser-Erweiterung + + + Füllt Anmeldedaten auf Websites automatisch aus + + + Browser-Erweiterung installieren + + + Die PassKey-Erweiterung füllt Benutzernamen und Passwörter auf Websites automatisch aus. Sie funktioniert, während PassKey ausgeführt wird. + + + 1. Öffnen Sie den Store Ihres Browsers: + + + Chrome / Edge + + + Firefox + + + 2. Klicken Sie auf der Store-Seite auf "Hinzufügen" (oder "Installieren"). + + + 3. Lassen Sie PassKey laufen: Die Erweiterung verbindet sich automatisch. + + + Schließen + + + Browser-Erweiterung + + + Die Browser-Erweiterung füllt Benutzernamen und Passwörter auf Websites automatisch aus. Zum Installieren öffnen Sie Einstellungen → Allgemein → Browser-Erweiterung: Eine geführte Prozedur führt Sie zu den Chrome/Edge- oder Firefox-Stores. Die Erweiterung verbindet sich mit PassKey, während die App läuft. + + + Tresor leeren + + + Löscht alle Passwörter, Karten, Identitäten und Notizen. Unwiderrufliche Operation. + + + Tresor leeren + + + Tresor leeren + + + WARNUNG: Dieser Vorgang löscht ENDGÜLTIG alle Passwörter, Karten, Identitäten und Notizen aus dem Tresor. Metadaten und das Master-Passwort bleiben erhalten. Der Vorgang ist UNWIDERRUFLICH. Möchten Sie fortfahren? + + + Fortfahren + + + Leeren bestätigen + + + Geben Sie das Master-Passwort ein, um das Leeren des Tresors zu bestätigen. + + + Master-Passwort + + + Tresor leeren + + + Geben Sie das Master-Passwort ein. + + + Falsches Master-Passwort. Der Tresor wurde nicht geleert. + + + Tresor geleert + + + Alle Einträge wurden gelöscht. Der Tresor ist jetzt leer. + + + Fehler + + + Vorhandenes Backup wiederherstellen + + + Passwort hinzufügen + + + Passwort bearbeiten + + + Karte hinzufügen + + + Karte bearbeiten + + + Identität hinzufügen + + + Identität bearbeiten + + + Notiz hinzufügen + + + Notiz bearbeiten + + + Das E-Mail-Format scheint ungültig zu sein + + + Zweiter Vorname + + + Firma + + + Benutzername + + + Notiz hinzufügen + + + Wählen Sie eine Notiz aus oder erstellen Sie eine neue + + + Zusätzliche Notizen... + + + Passwort + + + Master-Passwort eingeben + + + Master-Passwort wiederholen + + + Geben Sie ein Passwort zur Überprüfung ein... + + + Die Überprüfung erfolgt vollständig offline; es werden keine Daten gesendet. + + + Mehrdeutige Zeichen ausschließen + + + Base32-Schlüssel + + + * Pflichtfeld (Vor- oder Nachname) + + + PassKey anzeigen + + + Tresor sperren + + + Beenden + + + PassKey im Hintergrund weiterlaufen lassen? + + + Minimieren + + + PassKey schließen + + + 2FA-Schlüssel eingeben + + + Fügen Sie den vom Dienst bereitgestellten Base32-Schlüssel ein (derselbe Text, den Sie in Google Authenticator eingeben würden). Groß-/Kleinschreibung und Leerzeichen werden ignoriert. + + + z. B. JBSW Y3DP EHPK 3PXP + + + Speichern + + + OK + + + Allgemein + + + Persönlich + + + Arbeit + + + Finanzen + + + Medizinisch + + + Reisen + + + Bildung + + + Rechtliches + + + Technisch + + + Sonstiges + + + Gerade eben + + + vor {0} Min. + + + vor {0} Std. + + + Gestern + + + vor {0} T. + + + de-DE + + + Alle Kategorien + + + Filter: {0} + + + Keine Ergebnisse gefunden. + + + Angeheftet + + + Notizen + + + Angeheftet + + + {0} Z. · {1} Wörter + + + {0} Zeichen, {1} Wörter + + + Nicht gespeicherte Änderungen + + + Notiz oben in der Liste anheften + + + Vom Anfang lösen + + + Wird gespeichert... + + + Gespeichert + + + Nach Kategorie filtern + + + Sofort + + + Wenige Sekunden + + + {0} Minuten + + + {0} Stunden + + + {0} Tage + + + {0} Jahre + + + Neues Passwort generiert + + + Passwort in die Zwischenablage kopiert + + + {0} Tausend Jahre + + + {0} Millionen Jahre + + + {0} Milliarden Jahre + + + über eine Billion Jahre + + + Kompromittierte Passwörter ({0}) + + + Schwache Passwörter ({0}) + + + Wiederverwendete Passwörter ({0}) + + + (ohne Titel) + + + {0:N0} Lecks + + + wiederverwendet + + + Mindestens 8 Zeichen verwenden + + + Mindestens 12 Zeichen für besseren Schutz verwenden + + + Großbuchstaben hinzufügen + + + Kleinbuchstaben hinzufügen + + + Zahlen hinzufügen + + + Sonderzeichen hinzufügen (!@#$%) + + + Gängige Muster vermeiden (password, 123456, qwerty...) + + + Diese Bitwarden-Datei ist verschlüsselt und kann nicht importiert werden. Exportieren Sie Ihre Daten aus Bitwarden im unverschlüsselten Format (.json) und versuchen Sie es erneut. + + + Die Bitwarden-ZIP-Datei enthält keine 'data.json'. Exportieren Sie Ihre Daten erneut aus Bitwarden und versuchen Sie es noch einmal. + + + Die .1pux-Datei enthält keine 'export.data'. + + + Dies ist eine KeePass 1.x-Datenbank (.kdb), die nicht unterstützt wird. Öffnen Sie sie in KeePass 2.x und speichern (oder exportieren) Sie sie als .kdbx, und versuchen Sie es erneut. + + + Die KeePass-Datenbank konnte nicht geöffnet werden. Prüfen Sie, ob das Passwort korrekt ist und die Datei eine gültige .kdbx (KeePass 2.x) ist. + + + Die ausgewählte Datei enthält keine erkennbaren Einträge. Stellen Sie sicher, dass der Quellexport nicht leer ist und das Format unterstützt wird. + + + Bitwarden (JSON oder ZIP) + + + KeePass (.kdbx / .kdb) + + + Kein erkennbarer QR-Code im Bild. + + + QR-Code erkannt, aber keine gültige 'otpauth://'-URI. + + + Kein Text in der Zwischenablage. + + + Der Text in der Zwischenablage ist keine gültige 'otpauth://'-URI. + + + Ungültiger Base32-Schlüssel (nur A-Z und 2-7 verwenden). + + + Bild zu groß (max. 64 KB) + + + Fehler beim Entsperren des Tresors. + + + FEHLER + + + Tresor konnte nicht erstellt werden. Bitte erneut versuchen. + + + PassKey {0} verfügbar + + + PassKey_Verlauf + + + Online + + + Unbenannte Karte + + + Unbenannte Identität + + + Notiz ohne Titel + + + Ein Fehler ist aufgetreten. Bitte erneut versuchen. + + + Zurück zu den Einstellungen + + + Zurück zu den Einstellungen + + + Verlauf als CSV exportieren + + + Tresor erstellen + + + Kompromittierte Passwörter prüfen + + + Zu prüfendes Passwort + + + Entsperren + + + Fügen Sie Ihr erstes Passwort hinzu + + + Aus anderem Manager importieren + + + Vorhandenes Backup wiederherstellen + + + Einstellungen erkunden + + + Weiter + + + Identitäten suchen + + + Identität hinzufügen + + + Identität hinzufügen (Strg+N) + + + Passwort anzeigen + + + Benutzername kopieren + + + Passwort kopieren + + + Bearbeiten + + + Löschen + + + Kartennummer kopieren + + + Kopieren + + + URL öffnen + + + Passwörter suchen + + + Neues Passwort + + + Neues Passwort (Strg+N) + + + Keine Notizen. Erstellen Sie Ihre erste sichere Notiz. + + + Keine Ergebnisse. Ändern Sie die Filter oder die Suche. + + + Notizen suchen + + + Notizen nach Titel, Inhalt oder Kategorie filtern + + + Notiz hinzufügen (Strg+N) + + + Neue Notiz (Strg+N) + + + Karten suchen + + + Karten-/Listenansicht umschalten + + + Ansicht wechseln + + + Neue Karte + + + Neue Karte (Strg+N) + + + Tresor-Zustand — Tresorprüfung öffnen + + + Tresorprüfung öffnen + + + Passwörter gesamt + + + Karten gesamt + + + Identitäten gesamt + + + Notizen gesamt + + + Übersicht + + + Passwörter + + + Kreditkarten + + + Identitäten + + + Sichere Notizen + + + Passwortgenerator + + + Passwortprüfung und Tresor-Audit + + + Tresor sperren + + + Hilfe + + + Generiertes Passwort + + + Passwort in die Zwischenablage kopieren + + + In die Zwischenablage kopieren (Strg+C) + + + Neues Passwort generieren + + + Neues Passwort generieren + + + Passwortlänge + + + Großbuchstaben einbeziehen + + + Kleinbuchstaben einbeziehen + + + Zahlen einbeziehen + + + Sonderzeichen einbeziehen + + + Mehrdeutige Zeichen wie Null/O und Eins/l/I ausschließen + + + Kartenfarbe + + + Nicht gespeicherte Änderungen + + + Nicht gespeicherte Änderungen + + + Notiztitel + + + Notizkategorie + + + Bearbeitungsmodus + + + Markdown-Vorschau + + + Notizinhalt + + + Unterstützt Markdown-Syntax + + + Notizvorschau in Markdown + + + Notiz löschen + + + Notiz anheften + + + Notiz oben in der Liste anheften + + + Notiz speichern + + + Vorname + + + Zweiter Vorname + + + Nachname + + + Geburtsdatum + + + E-Mail + + + Telefon + + + Firma + + + Benutzername + + + Straße + + + Stadt + + + Postleitzahl + + + Bundesland + + + Region + + + Land + + + Personalausweis + + + Gesundheitskarte + + + Führerschein + + + Reisepass + + + Notizen + + + Identitätsbezeichnung + + + Identität löschen + + + Titel + + + URL + + + Bild hochladen + + + Bild hochladen (PNG, JPG, ICO — max. 64 KB) + + + Symbol entfernen + + + Symbol entfernen + + + E-Mail oder Benutzer-ID + + + Passwort generieren + + + Passwort generieren + + + QR-Code aus Bild scannen + + + QR-Code aus PNG/JPG-Datei importieren + + + otpauth-URI aus Zwischenablage einfügen + + + Eine 'otpauth://'-URI aus der Zwischenablage einfügen + + + Base32-Seed manuell eingeben + + + Den Base32-Schlüssel manuell eingeben + + + TOTP-Code kopieren + + + Aktuellen Code in die Zwischenablage kopieren + + + Geheimen Schlüssel anzeigen + + + Den Base32-Schlüssel anzeigen + + + 2FA-Code entfernen + + + Den 2FA-Code aus diesem Eintrag entfernen + + + Base32-Schlüssel (schreibgeschützt) + + + Passwort löschen + + + z. B. Zuhause, Arbeit... + + + Max + + + Mustermann + + + TT/MM/JJJJ + + + max@beispiel.de + + + +49 123 4567890 + + + Musterstraße 1 + + + Berlin + + + Berlin + + + Berlin + + + Deutschland + + + Ausweisnummer + + + Kartennummer + + + Führerscheinnummer + + + Reisepassnummer + + + z. B. Google, Amazon... + + + email@beispiel.de + + + https://... + + + PIN + + diff --git a/src/PassKey.Desktop/Strings/en-GB/Resources.resw b/src/PassKey.Desktop/Strings/en-GB/Resources.resw index ec5885b..058befb 100644 --- a/src/PassKey.Desktop/Strings/en-GB/Resources.resw +++ b/src/PassKey.Desktop/Strings/en-GB/Resources.resw @@ -171,8 +171,8 @@ + New card - - + Add identity + + Add identity + New note @@ -184,50 +184,53 @@ Save Changes - + First Name - + Last Name - + + First or last name is required + + Date of Birth - + Email - + Phone - + Street - + City - + Postcode - + Province - + Region - + Country Content - + Personal Data - + Address - + Documents @@ -237,10 +240,10 @@ Card Details - + Save - + Delete @@ -252,9 +255,6 @@ Change Master Password - - Generate and Copy - Import @@ -293,10 +293,10 @@ Version - + Identities - + Secure Notes @@ -309,16 +309,16 @@ Character Sets - + ID Card - + Health Card - + Driving Licence - + Passport @@ -411,7 +411,7 @@ Search passwords... - New password + Add password Title @@ -437,6 +437,15 @@ Password * + + Required field + + + Required field + + + Required field + Notes @@ -465,8 +474,11 @@ Search cards... + + Search identities... + - New card + Add card Label @@ -492,6 +504,15 @@ CVV/CVV2 * + + Required field + + + Required field + + + Required field + PIN @@ -831,7 +852,7 @@ Skipped (duplicates): {0} - Generic CSV + Generic CSV (.csv) Cancel @@ -888,7 +909,7 @@ KeePass master password - KeePass (.kdbx) + KeePass 2.x (.kdbx) Backup password @@ -912,7 +933,7 @@ The file is not a valid PassKey backup. - Bitwarden (JSON) + Bitwarden (.json / .zip) Keep both @@ -989,127 +1010,127 @@ Saved! - + Help - + Keyboard shortcuts - + Navigation - + Navigate between sections (Dashboard, Passwords, Cards…, Settings) - + Search (in Dashboard) - + Lock vault - + Open Help - + Actions - + New item - + Edit selected item - + Del - + Delete selected item - + Cancel / Go back - + Frequently asked questions - + How do I change the master password? - + Go to Settings and scroll to the Security section. Click 'Change master password': you will be asked for your current password (to confirm your identity) and then the new password twice. The new master password is applied immediately. - + How do I create a vault backup? - + Go to Settings and scroll to the Backup & Import section. Click 'Create backup': an encrypted file is generated using your master password. Store it somewhere safe. To restore, use 'Restore backup' in the same section and enter the master password used when the backup was created. - + Where is my data stored? - + All PassKey data is stored exclusively on your device, in a SQLite database encrypted with AES-GCM 256-bit. No information is transmitted over the Internet or to external servers. The file path is shown below — you can select it and open the folder directly in File Explorer. - + How do I import passwords from another manager? - - Go to Settings and scroll to the Backup & Import section. Click 'Import data' and select the source format: generic CSV (compatible with most managers), Bitwarden JSON, 1Password (.1pux) and KeePass (.kdbx). PassKey shows a summary of detected items before final confirmation. + + Go to Settings, Backup and import section, and click 'Import data', then choose the source format. Supported formats: generic CSV (.csv), compatible with most managers and with Chrome, Firefox and NordPass exports; Bitwarden, either as an unencrypted .json or as a .zip with attachments (PassKey extracts the data automatically); 1Password (.1pux); and KeePass 2.x (.kdbx), for which the database password is requested. Encrypted Bitwarden exports cannot be imported (re-export them unencrypted), nor can KeePass 1.x databases (.kdb), an older format you must first convert to .kdbx with KeePass 2.x. Before the final confirmation PassKey always shows a summary of the detected items. - + About - + Secure password manager for Windows - + Usage Guide - + Dashboard - + The Dashboard is PassKey's main screen. It displays a summary of saved items by type (passwords, cards, identities, notes), vault statistics and recently modified items. Use the search bar (Ctrl+F) to quickly find any item across all categories. - + Passwords - + View and manage all passwords saved in the vault. Click an entry to open the detail panel: from there you can copy the username or password with one click, view the associated URL, edit fields with the Edit button or delete the entry with confirmation. - + Credit Cards - + Manage credit cards saved in the vault. Card number, holder, expiry date and CVV are stored fully encrypted. In the detail panel you can copy the card number with one click and press and hold the CVV button to reveal it. The card colour is customisable. - + Identities - + Store personal identity documents and information: first and last name, address, document type and number, tax code, date of birth, phone number and email address. Useful for quickly copying individual fields when filling in forms without searching for physical documents. - + Secure Notes - + Store sensitive information as free-form text notes, encrypted like all other items in the vault. Each note has a title, a customisable colour and a free-length text field. Ideal for PINs, access codes, security questions or any data that doesn't fit other categories. - + Generator - + Generate random, cryptographically strong passwords. Configure the length and character sets to include (upper case, lower case, digits and symbols). Password strength is shown in real time. Copy the result to the clipboard with one click. - + Password Verifier - + Analyse the security of a password without saving it in the vault. Shows a score from 0 to 4 stars, an estimate of the time needed to crack it with a brute-force attack and an indication of whether it appears in public breach databases. Useful for checking passwords already in use. - + Settings - + Centralises all configuration options: interface language, master password, auto-lock timeout, vault backup and restore, and import from other password managers (CSV, Bitwarden, 1Password, KeePass). @@ -1487,4 +1508,961 @@ Do you want to enable this check? Weak - \ No newline at end of file + + Changes saved + + + Item deleted + + + Username copied + + + Password copied + + + Copied to clipboard + + + Forgotten password + + + PassKey never stores your master password. For security (zero-knowledge model) only you know it. If you have forgotten it, you can restore a .pkbak backup or create a new vault. + + + Restore backup + + + Create new vault + + + Create new vault + + + The current vault and all its data will become permanently inaccessible. This cannot be undone. Continue? + + + Delete and continue + + + The vault will lock in {0} seconds + + + Stay active + + + Auto-lock updated + + + History + + + Recent vault activity + + + Export CSV + + + No activity + + + Vault actions will appear here. + + + Date and time + + + Type + + + Action + + + CSV file + + + History exported + + + Export failed + + + Created + + + Modified + + + Deleted + + + Copied + + + Unlocked + + + Locked + + + Password + + + Card + + + Identity + + + Note + + + Vault + + + Activity history + + + Browser extension + + + Automatically fill credentials on websites + + + Install the browser extension + + + The PassKey extension automatically fills usernames and passwords on websites. It works while PassKey is running. + + + 1. Open your browser's store: + + + Chrome / Edge + + + Firefox + + + 2. On the store page, click "Add" (or "Install"). + + + 3. Keep PassKey running: the extension connects automatically. + + + Close + + + Browser extension + + + The browser extension automatically fills usernames and passwords on websites. To install it, open Settings → General → Browser extension: a guided procedure takes you to the Chrome/Edge or Firefox stores. The extension connects to PassKey while the app is running. + + + Clear vault + + + Deletes all passwords, cards, identities and notes. Irreversible operation. + + + Clear vault + + + Clear vault + + + WARNING: this operation permanently deletes all passwords, cards, identities and notes from the vault. Metadata and the master password are kept. The operation is IRREVERSIBLE. Do you want to continue? + + + Continue + + + Confirm clearing + + + Enter the master password to confirm clearing the vault. + + + Master password + + + Clear vault + + + Enter the master password. + + + Wrong master password. The vault was not cleared. + + + Vault cleared + + + All entries have been deleted. The vault is now empty. + + + Error + + + Restore an existing backup + + + Add password + + + Edit password + + + Add card + + + Edit card + + + Add identity + + + Edit identity + + + Add note + + + Edit note + + + The email format doesn't look valid + + + Middle name + + + Company + + + Username + + + Add note + + + Select a note or create a new one + + + Additional notes... + + + Password + + + Enter the master password + + + Repeat the master password + + + Type a password to check it... + + + Verification happens entirely offline; no data is sent. + + + Exclude ambiguous characters + + + Base32 key + + + * Required field (first or last name) + + + Show PassKey + + + Lock Vault + + + Exit + + + Keep PassKey running in the background? + + + Minimise + + + Close PassKey + + + Enter 2FA key + + + Paste the Base32 key provided by the site (the same text you would enter in Google Authenticator). Case and spaces are ignored. + + + E.g. JBSW Y3DP EHPK 3PXP + + + Save + + + OK + + + General + + + Personal + + + Work + + + Financial + + + Medical + + + Travel + + + Education + + + Legal + + + Technical + + + Other + + + Just now + + + {0} min ago + + + {0} h ago + + + Yesterday + + + {0}d ago + + + en-GB + + + All categories + + + Filter: {0} + + + No results found. + + + Pinned + + + Notes + + + Pinned + + + {0} chars · {1} words + + + {0} characters, {1} words + + + Unsaved changes + + + Pin note to top of list + + + Unpin from top + + + Saving... + + + Saved + + + Filter by category + + + Instant + + + A few seconds + + + {0} minutes + + + {0} hours + + + {0} days + + + {0} years + + + New password generated + + + Password copied to clipboard + + + {0} thousand years + + + {0} million years + + + {0} billion years + + + over a trillion years + + + Compromised passwords ({0}) + + + Weak passwords ({0}) + + + Reused passwords ({0}) + + + (untitled) + + + {0:N0} breaches + + + reused + + + Use at least 8 characters + + + Use at least 12 characters for better protection + + + Add uppercase letters + + + Add lowercase letters + + + Add numbers + + + Add special symbols (!@#$%) + + + Avoid common patterns (password, 123456, qwerty...) + + + This Bitwarden file is encrypted and cannot be imported. Export your data from Bitwarden in unencrypted (.json) format and try again. + + + The Bitwarden ZIP file does not contain 'data.json'. Export your data from Bitwarden again and try again. + + + The .1pux file does not contain 'export.data'. + + + This is a KeePass 1.x database (.kdb), which is not supported. Open it in KeePass 2.x and save (or export) it as .kdbx, then try again. + + + Could not open the KeePass database. Check that the password is correct and that the file is a valid .kdbx (KeePass 2.x). + + + The selected file contains no recognizable entries. Make sure the source export is not empty and the format is supported. + + + Bitwarden (JSON or ZIP) + + + KeePass (.kdbx / .kdb) + + + No recognizable QR code in the image. + + + QR recognized but it isn't a valid 'otpauth://' URI. + + + There's no text in the clipboard. + + + The clipboard text isn't a valid 'otpauth://' URI. + + + Invalid Base32 key (use only A-Z and 2-7). + + + Image too large (max 64 KB) + + + Error unlocking the vault. + + + ERROR + + + Could not create the vault. Please try again. + + + PassKey {0} available + + + PassKey_History + + + Online + + + Unnamed card + + + Unnamed identity + + + Untitled note + + + Something went wrong. Please try again. + + + Back to settings + + + Back to settings + + + Export history to CSV + + + Create Vault + + + Check for compromised passwords + + + Password to verify + + + Unlock + + + Add your first password + + + Import from another manager + + + Restore an existing backup + + + Explore settings + + + Continue + + + Search identities + + + Add identity + + + Add identity (Ctrl+N) + + + Reveal password + + + Copy username + + + Copy password + + + Edit + + + Delete + + + Copy card number + + + Copy + + + Open URL + + + Search passwords + + + New password + + + New password (Ctrl+N) + + + No notes. Create your first secure note. + + + No results. Try changing the filters or search. + + + Search notes + + + Filter notes by title, content or category + + + Add note (Ctrl+N) + + + New note (Ctrl+N) + + + Search cards + + + Toggle card/list view + + + Toggle view + + + New card + + + New card (Ctrl+N) + + + Vault health — open Vault check + + + Open Vault check + + + Total passwords + + + Total cards + + + Total identities + + + Total notes + + + Dashboard + + + Passwords + + + Credit cards + + + Identities + + + Secure notes + + + Password generator + + + Password check and vault audit + + + Lock vault + + + Help + + + Generated password + + + Copy password to clipboard + + + Copy to clipboard (Ctrl+C) + + + Generate new password + + + Generate new password + + + Password length + + + Include uppercase letters + + + Include lowercase letters + + + Include numbers + + + Include special symbols + + + Exclude ambiguous characters like zero/O and one/l/I + + + Card accent colour + + + Unsaved changes + + + Unsaved changes + + + Note title + + + Note category + + + Edit mode + + + Markdown preview + + + Note content + + + Supports Markdown syntax + + + Note preview in Markdown + + + Delete note + + + Pin note + + + Pin note to the top of the list + + + Save note + + + First Name + + + Middle name + + + Last Name + + + Date of Birth + + + Email + + + Phone + + + Company + + + Username + + + Street + + + City + + + Postcode + + + Province + + + Region + + + Country + + + ID Card + + + Health Card + + + Driving Licence + + + Passport + + + Notes + + + Identity label + + + Delete identity + + + Title + + + URL + + + Upload image + + + Upload image (PNG, JPG, ICO — max 64 KB) + + + Remove icon + + + Remove icon + + + Email or User ID + + + Generate password + + + Generate password + + + Scan QR code from image + + + Import a QR code from a PNG/JPG file + + + Paste otpauth URI from clipboard + + + Paste an 'otpauth://' URI from the clipboard + + + Enter Base32 seed manually + + + Enter the Base32 key manually + + + Copy TOTP code + + + Copy the current code to the clipboard + + + Show secret key + + + Show the Base32 key + + + Remove 2FA code + + + Remove the 2FA code from this entry + + + Base32 key (read-only) + + + Delete password + + + e.g. Home, Work... + + + John + + + Smith + + + DD/MM/YYYY + + + john@example.com + + + +44 1234 567890 + + + 1 Main Street + + + London + + + Greater London + + + England + + + United Kingdom + + + Document number + + + Card number + + + Licence number + + + Passport number + + + e.g. Google, Amazon... + + + email@example.com + + + https://... + + + PIN + + diff --git a/src/PassKey.Desktop/Strings/es-ES/Resources.resw b/src/PassKey.Desktop/Strings/es-ES/Resources.resw index cc04c68..07a48ce 100644 --- a/src/PassKey.Desktop/Strings/es-ES/Resources.resw +++ b/src/PassKey.Desktop/Strings/es-ES/Resources.resw @@ -183,8 +183,8 @@ + Nueva tarjeta - - + Añadir identidad + + Añadir identidad + Nueva nota @@ -196,50 +196,53 @@ Guardar cambios - + Nombre - + Apellidos - + + Se requiere el nombre o los apellidos + + Fecha de nacimiento - + Correo electrónico - + Teléfono - + Calle - + Ciudad - + Código postal - + Provincia - + Comunidad autónoma - + País Contenido - + Datos personales - + Dirección - + Documentos @@ -249,10 +252,10 @@ Detalles de la tarjeta - + Guardar - + Eliminar @@ -264,9 +267,6 @@ Cambiar contraseña maestra - - Generar y copiar - Importar @@ -305,10 +305,10 @@ Versión - + Identidades - + Notas seguras @@ -321,16 +321,16 @@ Conjuntos de caracteres - + DNI - + Tarjeta sanitaria - + Permiso de conducir - + Pasaporte @@ -423,7 +423,7 @@ Buscar contraseñas... - Nueva contraseña + Añadir contraseña Título @@ -449,6 +449,15 @@ Contraseña * + + Campo requerido + + + Campo requerido + + + Campo requerido + Notas @@ -477,8 +486,11 @@ Buscar tarjetas... + + Buscar identidades... + - Nueva tarjeta + Añadir tarjeta Etiqueta @@ -504,6 +516,15 @@ CVV/CVV2 * + + Campo requerido + + + Campo requerido + + + Campo requerido + PIN @@ -843,7 +864,7 @@ Omitidos (duplicados): {0} - CSV generico + CSV generico (.csv) Cancelar @@ -900,7 +921,7 @@ Contrasena maestra KeePass - KeePass (.kdbx) + KeePass 2.x (.kdbx) Contrasena de respaldo @@ -924,7 +945,7 @@ El archivo no es un respaldo PassKey valido. - Bitwarden (JSON) + Bitwarden (.json / .zip) Mantener ambos @@ -1001,127 +1022,127 @@ ¡Guardado! - + Ayuda - + Atajos de teclado - + Navegación - + Navegar entre secciones (Panel, Contraseñas, Tarjetas…, Configuración) - + Buscar (en el Panel) - + Bloquear el cofre - + Abrir ayuda - + Acciones - + Nuevo elemento - + Editar elemento seleccionado - + Supr - + Eliminar elemento seleccionado - + Cancelar / Volver - + Preguntas frecuentes - + ¿Cómo cambio la contraseña maestra? - + Vaya a Configuración y desplácese hasta la sección Seguridad. Haga clic en 'Cambiar contraseña maestra': se le pedirá la contraseña actual (para confirmar su identidad) y luego la nueva contraseña dos veces. La nueva contraseña maestra se aplica de inmediato. - + ¿Cómo creo una copia de seguridad del cofre? - + Vaya a Configuración y desplácese hasta la sección Copia de seguridad e importación. Haga clic en 'Crear copia de seguridad': se genera un archivo cifrado con su contraseña maestra. Guárdelo en un lugar seguro. Para restaurar, use 'Restaurar copia de seguridad' en la misma sección e introduzca la contraseña maestra utilizada al crear la copia. - + ¿Dónde se guardan mis datos? - + Todos los datos de PassKey se almacenan exclusivamente en su dispositivo, en una base de datos SQLite cifrada con AES-GCM de 256 bits. No se transmite ninguna información por Internet ni a servidores externos. La ruta del archivo se muestra a continuación. - + ¿Cómo importo contraseñas desde otro gestor? - - Vaya a Configuración y desplácese hasta la sección Copia de seguridad e importación. Haga clic en 'Importar datos' y seleccione el formato de origen: CSV genérico, Bitwarden JSON, 1Password (.1pux) y KeePass (.kdbx). PassKey muestra un resumen de los elementos detectados antes de la confirmación final. + + Ve a Configuración, sección Copia de seguridad e importación, y haz clic en 'Importar datos', luego elige el formato de origen. Formatos compatibles: CSV genérico (.csv), compatible con la mayoría de los gestores y con las exportaciones de Chrome, Firefox y NordPass; Bitwarden, como .json sin cifrar o como .zip con adjuntos (PassKey extrae los datos automáticamente); 1Password (.1pux); y KeePass 2.x (.kdbx), para el que se solicita la contraseña de la base de datos. No se pueden importar las exportaciones de Bitwarden cifradas (vuelve a exportarlas sin cifrar) ni las bases de datos de KeePass 1.x (.kdb), un formato más antiguo que debes convertir primero a .kdbx con KeePass 2.x. Antes de la confirmación final, PassKey siempre muestra un resumen de los elementos detectados. - + Acerca de - + Gestor de contraseñas seguro para Windows - + Guía de uso - + Panel principal - + El panel principal es la pantalla de inicio de PassKey. Muestra un resumen de los elementos guardados por tipo (contraseñas, tarjetas, identidades, notas), estadísticas del almacén y los elementos modificados recientemente. Use la barra de búsqueda (Ctrl+F) para encontrar rápidamente cualquier elemento. - + Contraseñas - + Visualice y gestione todas las contraseñas guardadas en el almacén. Haga clic en una entrada para abrir el panel de detalles: desde allí puede copiar el nombre de usuario o la contraseña con un clic, ver la URL asociada, editar los campos o eliminar la entrada con confirmación. - + Tarjetas de crédito - + Gestione las tarjetas de crédito guardadas en el almacén. El número de tarjeta, titular, fecha de vencimiento y CVV se almacenan completamente cifrados. En el panel de detalles puede copiar el número de tarjeta con un clic y mantener pulsado el botón CVV para revelarlo. El color de la tarjeta es personalizable. - + Identidades - + Almacene documentos de identidad e información personal: nombre y apellidos, dirección, tipo y número de documento, número fiscal, fecha de nacimiento, teléfono y correo electrónico. Útil para copiar rápidamente campos individuales al rellenar formularios sin buscar documentos físicos. - + Notas seguras - + Conserve información sensible como notas de texto libre, cifradas como todos los demás elementos del almacén. Cada nota tiene un título, un color personalizable y un campo de texto de longitud libre. Ideal para PINs, códigos de acceso, preguntas de seguridad o cualquier dato que no encaje en otras categorías. - + Generador - + Genere contraseñas aleatorias y criptográficamente seguras. Configure la longitud y los conjuntos de caracteres a incluir (mayúsculas, minúsculas, dígitos y símbolos). La fortaleza de la contraseña se muestra en tiempo real. Copie el resultado al portapapeles con un clic. - + Verificador de contraseñas - + Analice la seguridad de una contraseña sin guardarla en el almacén. Muestra una puntuación de 0 a 4 estrellas, una estimación del tiempo necesario para descifrarla por fuerza bruta y si aparece en bases de datos públicas de violaciones. Útil para verificar contraseñas ya en uso. - + Configuración - + Centraliza todas las opciones de configuración: idioma de la interfaz, contraseña maestra, tiempo de bloqueo automático, copia de seguridad y restauración del almacén e importación desde otros gestores de contraseñas (CSV, Bitwarden, 1Password, KeePass). @@ -1500,4 +1521,961 @@ Deseas activar esta verificacion? Debiles - \ No newline at end of file + + Cambios guardados + + + Elemento eliminado + + + Nombre de usuario copiado + + + Contrasena copiada + + + Copiado al portapapeles + + + Contrasena olvidada + + + PassKey nunca almacena la contrasena maestra. Por seguridad (modelo zero-knowledge) solo tu la conoces. Si la has olvidado, puedes restaurar una copia .pkbak o crear un vault nuevo. + + + Restaurar copia + + + Crear vault nuevo + + + Crear vault nuevo + + + El vault actual y todos sus datos quedaran permanentemente inaccesibles. Operacion irreversible. Continuar? + + + Eliminar y continuar + + + El vault se bloqueara en {0} segundos + + + Seguir activo + + + Bloqueo automatico actualizado + + + Historial + + + Actividad reciente de la caja fuerte + + + Exportar CSV + + + Sin actividad + + + Las acciones de la caja fuerte aparecerán aquí. + + + Fecha y hora + + + Tipo + + + Acción + + + Archivo CSV + + + Historial exportado + + + Error al exportar + + + Creado + + + Modificado + + + Eliminado + + + Copiado + + + Desbloqueado + + + Bloqueado + + + Contraseña + + + Tarjeta + + + Identidad + + + Nota + + + Caja fuerte + + + Historial de actividad + + + Extensión para el navegador + + + Rellena automáticamente las credenciales en los sitios web + + + Instalar la extensión para el navegador + + + La extensión PassKey rellena automáticamente usuarios y contraseñas en los sitios web. Funciona mientras PassKey está en ejecución. + + + 1. Abre la tienda de tu navegador: + + + Chrome / Edge + + + Firefox + + + 2. En la página de la tienda, haz clic en "Añadir" (o "Instalar"). + + + 3. Mantén PassKey en ejecución: la extensión se conecta automáticamente. + + + Cerrar + + + Extensión para el navegador + + + La extensión para navegador rellena automáticamente usuarios y contraseñas en los sitios web. Para instalarla, abre Configuración → General → Extensión para el navegador: un procedimiento guiado te lleva a las tiendas de Chrome/Edge o Firefox. La extensión se conecta a PassKey mientras la app está en ejecución. + + + Vaciar caja fuerte + + + Elimina todas las contraseñas, tarjetas, identidades y notas. Operación irreversible. + + + Vaciar caja fuerte + + + Vaciar caja fuerte + + + ADVERTENCIA: esta operación elimina DEFINITIVAMENTE todas las contraseñas, tarjetas, identidades y notas de la caja fuerte. Los metadatos y la contraseña maestra se conservan. La operación es IRREVERSIBLE. ¿Quieres continuar? + + + Continuar + + + Confirmar vaciado + + + Introduce la contraseña maestra para confirmar el vaciado de la caja fuerte. + + + Contraseña maestra + + + Vaciar caja fuerte + + + Introduce la contraseña maestra. + + + Contraseña maestra incorrecta. La caja fuerte no se ha vaciado. + + + Caja fuerte vaciada + + + Todas las entradas se han eliminado. La caja fuerte ahora está vacía. + + + Error + + + Restaurar una copia de seguridad existente + + + Añadir contraseña + + + Editar contraseña + + + Añadir tarjeta + + + Editar tarjeta + + + Añadir identidad + + + Editar identidad + + + Añadir nota + + + Editar nota + + + El formato del correo no parece válido + + + Segundo nombre + + + Empresa + + + Nombre de usuario + + + Añadir nota + + + Selecciona una nota o crea una nueva + + + Notas adicionales... + + + Contraseña + + + Introduce la contraseña maestra + + + Repite la contraseña maestra + + + Escribe una contraseña para verificarla... + + + La verificación se realiza completamente sin conexión; no se envía ningún dato. + + + Excluir caracteres ambiguos + + + Clave Base32 + + + * Campo obligatorio (nombre o apellido) + + + Mostrar PassKey + + + Bloquear el baúl + + + Salir + + + ¿Mantener PassKey activo en segundo plano? + + + Minimizar + + + Cerrar PassKey + + + Introducir clave 2FA + + + Pega la clave Base32 proporcionada por el sitio (el mismo texto que introducirías en Google Authenticator). Las mayúsculas y los espacios se ignoran. + + + Ej. JBSW Y3DP EHPK 3PXP + + + Guardar + + + OK + + + General + + + Personal + + + Trabajo + + + Financiero + + + Médico + + + Viaje + + + Educación + + + Legal + + + Técnico + + + Otro + + + Ahora mismo + + + hace {0} min + + + hace {0} h + + + Ayer + + + hace {0} d + + + es-ES + + + Todas las categorías + + + Filtro: {0} + + + No se encontraron resultados. + + + Fijadas + + + Notas + + + Fijada + + + {0} car · {1} palabras + + + {0} caracteres, {1} palabras + + + Cambios sin guardar + + + Fijar nota al principio de la lista + + + Quitar del principio + + + Guardando... + + + Guardado + + + Filtrar por categoría + + + Instantáneo + + + Pocos segundos + + + {0} minutos + + + {0} horas + + + {0} días + + + {0} años + + + Nueva contraseña generada + + + Contraseña copiada al portapapeles + + + {0} mil años + + + {0} millones de años + + + {0} mil millones de años + + + más de un billón de años + + + Contraseñas comprometidas ({0}) + + + Contraseñas débiles ({0}) + + + Contraseñas reutilizadas ({0}) + + + (sin título) + + + {0:N0} filtraciones + + + reutilizada + + + Usa al menos 8 caracteres + + + Usa al menos 12 caracteres para mayor protección + + + Añade letras mayúsculas + + + Añade letras minúsculas + + + Añade números + + + Añade símbolos especiales (!@#$%) + + + Evita patrones comunes (password, 123456, qwerty...) + + + Este archivo de Bitwarden está cifrado y no se puede importar. Exporta tus datos desde Bitwarden en formato no cifrado (.json) e inténtalo de nuevo. + + + El archivo ZIP de Bitwarden no contiene 'data.json'. Vuelve a exportar tus datos desde Bitwarden e inténtalo de nuevo. + + + El archivo .1pux no contiene 'export.data'. + + + Esta es una base de datos de KeePass 1.x (.kdb), que no es compatible. Ábrela en KeePass 2.x y guárdala (o expórtala) en formato .kdbx, luego inténtalo de nuevo. + + + No se pudo abrir la base de datos de KeePass. Comprueba que la contraseña sea correcta y que el archivo sea un .kdbx válido (KeePass 2.x). + + + El archivo seleccionado no contiene entradas reconocibles. Asegúrate de que la exportación de origen no esté vacía y de que el formato sea compatible. + + + Bitwarden (JSON o ZIP) + + + KeePass (.kdbx / .kdb) + + + No se reconoce ningún código QR en la imagen. + + + QR reconocido, pero no es un URI 'otpauth://' válido. + + + No hay texto en el portapapeles. + + + El texto del portapapeles no es un URI 'otpauth://' válido. + + + Clave Base32 no válida (usa solo A-Z y 2-7). + + + Imagen demasiado grande (máx. 64 KB) + + + Error al desbloquear el baúl. + + + ERROR + + + No se pudo crear el baúl. Inténtalo de nuevo. + + + PassKey {0} disponible + + + PassKey_Historial + + + Online + + + Tarjeta sin nombre + + + Identidad sin nombre + + + Nota sin título + + + Se produjo un error. Inténtalo de nuevo. + + + Volver a la configuración + + + Volver a la configuración + + + Exportar historial a CSV + + + Crear bóveda + + + Comprobar contraseñas comprometidas + + + Contraseña a verificar + + + Desbloquear + + + Añade tu primera contraseña + + + Importar desde otro gestor + + + Restaurar una copia de seguridad existente + + + Explorar ajustes + + + Continuar + + + Buscar identidades + + + Añadir identidad + + + Añadir identidad (Ctrl+N) + + + Mostrar contraseña + + + Copiar nombre de usuario + + + Copiar contraseña + + + Editar + + + Eliminar + + + Copiar número de tarjeta + + + Copiar + + + Abrir URL + + + Buscar contraseñas + + + Nueva contraseña + + + Nueva contraseña (Ctrl+N) + + + Sin notas. Crea tu primera nota segura. + + + Sin resultados. Prueba a cambiar los filtros o la búsqueda. + + + Buscar notas + + + Filtrar notas por título, contenido o categoría + + + Añadir nota (Ctrl+N) + + + Nueva nota (Ctrl+N) + + + Buscar tarjetas + + + Cambiar vista tarjetas/lista + + + Cambiar vista + + + Nueva tarjeta + + + Nueva tarjeta (Ctrl+N) + + + Salud de la bóveda — abrir Verificación de bóveda + + + Abrir Verificación de bóveda + + + Total de contraseñas + + + Total de tarjetas + + + Total de identidades + + + Total de notas + + + Panel + + + Contraseñas + + + Tarjetas de crédito + + + Identidades + + + Notas seguras + + + Generador de contraseñas + + + Verificación de contraseñas y auditoría de la bóveda + + + Bloquear bóveda + + + Ayuda + + + Contraseña generada + + + Copiar contraseña al portapapeles + + + Copiar al portapapeles (Ctrl+C) + + + Generar nueva contraseña + + + Generar nueva contraseña + + + Longitud de la contraseña + + + Incluir letras mayúsculas + + + Incluir letras minúsculas + + + Incluir números + + + Incluir símbolos especiales + + + Excluir caracteres ambiguos como cero/O y uno/l/I + + + Color de la tarjeta + + + Cambios sin guardar + + + Cambios sin guardar + + + Título de la nota + + + Categoría de la nota + + + Modo edición + + + Vista previa de Markdown + + + Contenido de la nota + + + Admite sintaxis Markdown + + + Vista previa de la nota en Markdown + + + Eliminar nota + + + Fijar nota + + + Fijar nota en la parte superior de la lista + + + Guardar nota + + + Nombre + + + Segundo nombre + + + Apellidos + + + Fecha de nacimiento + + + Correo electrónico + + + Teléfono + + + Empresa + + + Nombre de usuario + + + Calle + + + Ciudad + + + Código postal + + + Provincia + + + Comunidad autónoma + + + País + + + DNI + + + Tarjeta sanitaria + + + Permiso de conducir + + + Pasaporte + + + Notas + + + Etiqueta de la identidad + + + Eliminar identidad + + + Título + + + URL + + + Subir imagen + + + Subir imagen (PNG, JPG, ICO — máx. 64 KB) + + + Quitar icono + + + Quitar icono + + + Correo o ID de usuario + + + Generar contraseña + + + Generar contraseña + + + Escanear código QR desde imagen + + + Importar un código QR desde un archivo PNG/JPG + + + Pegar URI otpauth desde el portapapeles + + + Pegar una URI 'otpauth://' desde el portapapeles + + + Introducir semilla Base32 manualmente + + + Introducir la clave Base32 manualmente + + + Copiar código TOTP + + + Copiar el código actual al portapapeles + + + Mostrar clave secreta + + + Mostrar la clave Base32 + + + Quitar código 2FA + + + Quitar el código 2FA de esta entrada + + + Clave Base32 (solo lectura) + + + Eliminar contraseña + + + Ej. Casa, Trabajo... + + + Juan + + + Pérez + + + DD/MM/AAAA + + + juan@ejemplo.es + + + +34 612 34 56 78 + + + Calle Mayor, 1 + + + Madrid + + + Madrid + + + Comunidad de Madrid + + + España + + + Número de documento + + + Número de tarjeta + + + Número de licencia + + + Número de pasaporte + + + Ej. Google, Amazon... + + + email@ejemplo.es + + + https://... + + + PIN + + diff --git a/src/PassKey.Desktop/Strings/fr-FR/Resources.resw b/src/PassKey.Desktop/Strings/fr-FR/Resources.resw index 9a69f06..482de8d 100644 --- a/src/PassKey.Desktop/Strings/fr-FR/Resources.resw +++ b/src/PassKey.Desktop/Strings/fr-FR/Resources.resw @@ -183,8 +183,8 @@ + Nouvelle carte - - + Ajouter une identité + + Ajouter une identité + Nouvelle note @@ -196,50 +196,53 @@ Enregistrer les modifications - + Prénom - + Nom - + + Le prénom ou le nom est requis + + Date de naissance - + Email - + Téléphone - + Rue - + Ville - + Code postal - + Province - + Région - + Pays Contenu - + Données personnelles - + Adresse - + Documents @@ -249,10 +252,10 @@ Détails de la carte - + Enregistrer - + Supprimer @@ -264,9 +267,6 @@ Changer le mot de passe maître - - Générer et copier - Importer @@ -305,10 +305,10 @@ Version - + Identités - + Notes sécurisées @@ -321,16 +321,16 @@ Jeux de caractères - + Carte d'identité - + Carte vitale - + Permis de conduire - + Passeport @@ -423,7 +423,7 @@ Rechercher... - Nouveau mot de passe + Ajouter un mot de passe Titre @@ -449,6 +449,15 @@ Mot de passe * + + Champ obligatoire + + + Champ obligatoire + + + Champ obligatoire + Notes @@ -477,8 +486,11 @@ Rechercher... + + Rechercher des identités... + - Nouvelle carte + Ajouter une carte Libellé @@ -504,6 +516,15 @@ CVV/CVV2 * + + Champ obligatoire + + + Champ obligatoire + + + Champ obligatoire + Code PIN @@ -843,7 +864,7 @@ Ignores (doublons): {0} - CSV generique + CSV generique (.csv) Annuler @@ -900,7 +921,7 @@ Mot de passe maitre KeePass - KeePass (.kdbx) + KeePass 2.x (.kdbx) Mot de passe de sauvegarde @@ -924,7 +945,7 @@ Le fichier nest pas une sauvegarde PassKey valide. - Bitwarden (JSON) + Bitwarden (.json / .zip) Garder les deux @@ -1001,127 +1022,127 @@ Enregistré ! - + Aide - + Raccourcis clavier - + Navigation - + Naviguer entre les sections (Tableau de bord, Mots de passe, Cartes…, Paramètres) - + Rechercher (dans le Tableau de bord) - + Verrouiller le coffre - + Ouvrir l'aide - + Actions - + Nouvel élément - + Modifier l'élément sélectionné - + Suppr - + Supprimer l'élément sélectionné - + Annuler / Retour - + Questions fréquentes - + Comment modifier le mot de passe principal ? - + Allez dans Paramètres et faites défiler jusqu'à la section Sécurité. Cliquez sur 'Modifier le mot de passe principal' : il vous sera demandé votre mot de passe actuel (pour confirmer votre identité), puis le nouveau mot de passe deux fois. Le nouveau mot de passe maître est appliqué immédiatement. - + Comment créer une sauvegarde du coffre ? - + Allez dans Paramètres et faites défiler jusqu'à la section Sauvegarde et importation. Cliquez sur 'Créer une sauvegarde' : un fichier chiffré avec votre mot de passe maître est généré. Conservez-le en lieu sûr. Pour restaurer, utilisez 'Restaurer la sauvegarde' dans la même section et saisissez le mot de passe maître utilisé lors de la création. - + Où sont stockées mes données ? - + Toutes les données de PassKey sont stockées exclusivement sur votre appareil, dans une base de données SQLite chiffrée avec AES-GCM 256 bits. Aucune information n'est transmise sur Internet ou à des serveurs externes. Le chemin du fichier est indiqué ci-dessous. - + Comment importer des mots de passe depuis un autre gestionnaire ? - - Allez dans Paramètres et faites défiler jusqu'à la section Sauvegarde et importation. Cliquez sur 'Importer des données' et sélectionnez le format source : CSV générique, Bitwarden JSON, 1Password (.1pux) et KeePass (.kdbx). PassKey affiche un résumé des éléments détectés avant la confirmation définitive. + + Accédez à Paramètres, section Sauvegarde et importation, et cliquez sur 'Importer les données', puis choisissez le format source. Formats pris en charge : CSV générique (.csv), compatible avec la plupart des gestionnaires et avec les exports de Chrome, Firefox et NordPass ; Bitwarden, en .json non chiffré ou en .zip avec pièces jointes (PassKey en extrait automatiquement les données) ; 1Password (.1pux) ; et KeePass 2.x (.kdbx), pour lequel le mot de passe de la base est demandé. Les exports Bitwarden chiffrés ne peuvent pas être importés (réexportez-les non chiffrés), ni les bases KeePass 1.x (.kdb), un format plus ancien à convertir d'abord en .kdbx avec KeePass 2.x. Avant la confirmation finale, PassKey affiche toujours un résumé des éléments détectés. - + À propos - + Gestionnaire de mots de passe sécurisé pour Windows - + Guide d'utilisation - + Tableau de bord - + Le tableau de bord est l'écran principal de PassKey. Il affiche un résumé des éléments enregistrés par type (mots de passe, cartes, identités, notes), les statistiques du coffre et les éléments récemment modifiés. Utilisez la barre de recherche (Ctrl+F) pour trouver rapidement n'importe quel élément. - + Mots de passe - + Affichez et gérez tous les mots de passe enregistrés dans le coffre. Cliquez sur une entrée pour ouvrir le panneau de détails : vous pouvez y copier le nom d'utilisateur ou le mot de passe en un clic, afficher l'URL associée, modifier les champs ou supprimer l'entrée avec confirmation. - + Cartes de crédit - + Gérez les cartes de crédit enregistrées dans le coffre. Le numéro de carte, le titulaire, la date d'expiration et le CVV sont stockés de manière entièrement chiffrée. Dans le panneau de détails, copiez le numéro de carte en un clic et maintenez le bouton CVV pour le révéler. La couleur de la carte est personnalisable. - + Identités - + Stockez vos documents d'identité et informations personnelles : prénom et nom, adresse, type et numéro de document, numéro fiscal, date de naissance, téléphone et adresse e-mail. Utile pour copier rapidement les champs lors du remplissage de formulaires sans chercher vos documents physiques. - + Notes sécurisées - + Conservez des informations sensibles sous forme de notes textuelles libres, chiffrées comme tous les autres éléments du coffre. Chaque note a un titre, une couleur personnalisable et un champ texte de longueur libre. Idéal pour les codes PIN, codes d'accès, questions de sécurité ou toute donnée qui ne rentre pas dans les autres catégories. - + Générateur - + Générez des mots de passe aléatoires et cryptographiquement sécurisés. Configurez la longueur et les jeux de caractères à inclure (majuscules, minuscules, chiffres et symboles). La force du mot de passe est affichée en temps réel. Copiez le résultat dans le presse-papier en un clic. - + Vérificateur de mot de passe - + Analysez la sécurité d'un mot de passe sans l'enregistrer dans le coffre. Affiche un score de 0 à 4 étoiles, une estimation du temps de crack par attaque brute et une indication si le mot de passe figure dans des bases de données de violations publiques. Utile pour vérifier les mots de passe déjà utilisés. - + Paramètres - + Centralise toutes les options de configuration : langue de l'interface, mot de passe maître, délai de verrouillage automatique, sauvegarde et restauration du coffre, et importation depuis d'autres gestionnaires de mots de passe (CSV, Bitwarden, 1Password, KeePass). @@ -1499,4 +1520,961 @@ Voulez-vous activer cette verification ? Faibles - \ No newline at end of file + + Modifications enregistrees + + + Element supprime + + + Nom d'utilisateur copie + + + Mot de passe copie + + + Copie dans le presse-papiers + + + Mot de passe oublie + + + PassKey ne memorise jamais le mot de passe principal. Pour des raisons de securite (modele zero-knowledge), vous seul le connaissez. Si vous l'avez oublie, vous pouvez restaurer une sauvegarde .pkbak ou creer un nouveau coffre. + + + Restaurer une sauvegarde + + + Creer un nouveau coffre + + + Creer un nouveau coffre + + + Le coffre actuel et toutes ses donnees deviendront definitivement inaccessibles. Operation irreversible. Continuer ? + + + Supprimer et continuer + + + Le coffre se verrouillera dans {0} secondes + + + Rester actif + + + Verrouillage automatique mis a jour + + + Historique + + + Activité récente du coffre + + + Exporter CSV + + + Aucune activité + + + Les actions du coffre apparaîtront ici. + + + Date et heure + + + Type + + + Action + + + Fichier CSV + + + Historique exporté + + + Échec de l'exportation + + + Créé + + + Modifié + + + Supprimé + + + Copié + + + Déverrouillé + + + Verrouillé + + + Mot de passe + + + Carte + + + Identité + + + Note + + + Coffre + + + Historique d'activité + + + Extension pour le navigateur + + + Remplit automatiquement les identifiants sur les sites web + + + Installer l'extension pour le navigateur + + + L'extension PassKey remplit automatiquement les identifiants et mots de passe sur les sites web. Elle fonctionne lorsque PassKey est en cours d'exécution. + + + 1. Ouvrez le magasin de votre navigateur : + + + Chrome / Edge + + + Firefox + + + 2. Sur la page du magasin, cliquez sur "Ajouter" (ou "Installer"). + + + 3. Gardez PassKey ouvert : l'extension se connecte automatiquement. + + + Fermer + + + Extension pour le navigateur + + + L'extension pour navigateur remplit automatiquement les identifiants et mots de passe sur les sites web. Pour l'installer, ouvrez Paramètres → Général → Extension pour le navigateur : une procédure guidée vous amène aux magasins Chrome/Edge ou Firefox. L'extension se connecte à PassKey lorsque l'application est en cours d'exécution. + + + Vider le coffre + + + Supprime tous les mots de passe, cartes, identités et notes. Opération irréversible. + + + Vider le coffre + + + Vider le coffre + + + ATTENTION : cette opération supprime DÉFINITIVEMENT tous les mots de passe, cartes, identités et notes du coffre. Les métadonnées et le mot de passe principal sont conservés. L'opération est IRRÉVERSIBLE. Voulez-vous continuer ? + + + Continuer + + + Confirmer le vidage + + + Saisissez le mot de passe principal pour confirmer le vidage du coffre. + + + Mot de passe principal + + + Vider le coffre + + + Saisissez le mot de passe principal. + + + Mot de passe principal incorrect. Le coffre n'a pas été vidé. + + + Coffre vidé + + + Toutes les entrées ont été supprimées. Le coffre est maintenant vide. + + + Erreur + + + Restaurer une sauvegarde existante + + + Ajouter un mot de passe + + + Modifier le mot de passe + + + Ajouter une carte + + + Modifier la carte + + + Ajouter une identité + + + Modifier l'identité + + + Ajouter une note + + + Modifier la note + + + Le format de l'e-mail ne semble pas valide + + + Deuxième prénom + + + Entreprise + + + Nom d'utilisateur + + + Ajouter une note + + + Sélectionnez une note ou créez-en une nouvelle + + + Notes supplémentaires... + + + Mot de passe + + + Saisissez le mot de passe maître + + + Répétez le mot de passe maître + + + Saisissez un mot de passe à vérifier... + + + La vérification s'effectue entièrement hors ligne ; aucune donnée n'est envoyée. + + + Exclure les caractères ambigus + + + Clé Base32 + + + * Champ obligatoire (nom ou prénom) + + + Afficher PassKey + + + Verrouiller le coffre + + + Quitter + + + Garder PassKey actif en arrière-plan ? + + + Réduire + + + Fermer PassKey + + + Saisir la clé 2FA + + + Collez la clé Base32 fournie par le site (le même texte que vous saisiriez dans Google Authenticator). La casse et les espaces sont ignorés. + + + Ex. JBSW Y3DP EHPK 3PXP + + + Enregistrer + + + OK + + + Général + + + Personnel + + + Travail + + + Financier + + + Médical + + + Voyage + + + Éducation + + + Juridique + + + Technique + + + Autre + + + À l'instant + + + il y a {0} min + + + il y a {0} h + + + Hier + + + il y a {0} j + + + fr-FR + + + Toutes les catégories + + + Filtre : {0} + + + Aucun résultat trouvé. + + + Épinglées + + + Notes + + + Épinglée + + + {0} car · {1} mots + + + {0} caractères, {1} mots + + + Modifications non enregistrées + + + Épingler la note en haut de la liste + + + Détacher du haut + + + Enregistrement... + + + Enregistré + + + Filtrer par catégorie + + + Instantané + + + Quelques secondes + + + {0} minutes + + + {0} heures + + + {0} jours + + + {0} ans + + + Nouveau mot de passe généré + + + Mot de passe copié dans le presse-papiers + + + {0} mille ans + + + {0} millions d'années + + + {0} milliards d'années + + + plus de mille milliards d'années + + + Mots de passe compromis ({0}) + + + Mots de passe faibles ({0}) + + + Mots de passe réutilisés ({0}) + + + (sans titre) + + + {0:N0} fuites + + + réutilisé + + + Utilisez au moins 8 caractères + + + Utilisez au moins 12 caractères pour une meilleure protection + + + Ajoutez des lettres majuscules + + + Ajoutez des lettres minuscules + + + Ajoutez des chiffres + + + Ajoutez des symboles spéciaux (!@#$%) + + + Évitez les motifs courants (password, 123456, qwerty...) + + + Ce fichier Bitwarden est chiffré et ne peut pas être importé. Exportez vos données depuis Bitwarden au format non chiffré (.json) et réessayez. + + + Le fichier ZIP Bitwarden ne contient pas « data.json ». Exportez à nouveau vos données depuis Bitwarden et réessayez. + + + Le fichier .1pux ne contient pas « export.data ». + + + Il s'agit d'une base de données KeePass 1.x (.kdb), non prise en charge. Ouvrez-la dans KeePass 2.x et enregistrez-la (ou exportez-la) au format .kdbx, puis réessayez. + + + Impossible d'ouvrir la base de données KeePass. Vérifiez que le mot de passe est correct et que le fichier est un .kdbx valide (KeePass 2.x). + + + Le fichier sélectionné ne contient aucune entrée reconnaissable. Vérifiez que l'exportation source n'est pas vide et que le format est pris en charge. + + + Bitwarden (JSON ou ZIP) + + + KeePass (.kdbx / .kdb) + + + Aucun code QR reconnaissable dans l'image. + + + QR reconnu mais ce n'est pas un URI « otpauth:// » valide. + + + Aucun texte dans le presse-papiers. + + + Le texte du presse-papiers n'est pas un URI « otpauth:// » valide. + + + Clé Base32 non valide (utilisez seulement A-Z et 2-7). + + + Image trop grande (max 64 Ko) + + + Erreur lors du déverrouillage du coffre. + + + ERREUR + + + Impossible de créer le coffre. Réessayez. + + + PassKey {0} disponible + + + PassKey_Historique + + + Online + + + Carte sans nom + + + Identité sans nom + + + Note sans titre + + + Une erreur s'est produite. Réessayez. + + + Retour aux paramètres + + + Retour aux paramètres + + + Exporter l'historique en CSV + + + Créer le coffre + + + Vérifier les mots de passe compromis + + + Mot de passe à vérifier + + + Déverrouiller + + + Ajoutez votre premier mot de passe + + + Importer depuis un autre gestionnaire + + + Restaurer une sauvegarde existante + + + Explorer les paramètres + + + Continuer + + + Rechercher des identités + + + Ajouter une identité + + + Ajouter une identité (Ctrl+N) + + + Afficher le mot de passe + + + Copier le nom d'utilisateur + + + Copier le mot de passe + + + Modifier + + + Supprimer + + + Copier le numéro de carte + + + Copier + + + Ouvrir l'URL + + + Rechercher des mots de passe + + + Nouveau mot de passe + + + Nouveau mot de passe (Ctrl+N) + + + Aucune note. Créez votre première note sécurisée. + + + Aucun résultat. Essayez de modifier les filtres ou la recherche. + + + Rechercher des notes + + + Filtrer les notes par titre, contenu ou catégorie + + + Ajouter une note (Ctrl+N) + + + Nouvelle note (Ctrl+N) + + + Rechercher des cartes + + + Basculer vue cartes/liste + + + Changer de vue + + + Nouvelle carte + + + Nouvelle carte (Ctrl+N) + + + Santé du coffre — ouvrir la vérification du coffre + + + Ouvrir la vérification du coffre + + + Total des mots de passe + + + Total des cartes + + + Total des identités + + + Total des notes + + + Tableau de bord + + + Mots de passe + + + Cartes de crédit + + + Identités + + + Notes sécurisées + + + Générateur de mots de passe + + + Vérification des mots de passe et audit du coffre + + + Verrouiller le coffre + + + Guide + + + Mot de passe généré + + + Copier le mot de passe dans le presse-papiers + + + Copier dans le presse-papiers (Ctrl+C) + + + Générer un nouveau mot de passe + + + Générer un nouveau mot de passe + + + Longueur du mot de passe + + + Inclure les majuscules + + + Inclure les minuscules + + + Inclure les chiffres + + + Inclure les symboles spéciaux + + + Exclure les caractères ambigus comme zéro/O et un/l/I + + + Couleur de la carte + + + Modifications non enregistrées + + + Modifications non enregistrées + + + Titre de la note + + + Catégorie de la note + + + Mode édition + + + Aperçu Markdown + + + Contenu de la note + + + Prend en charge la syntaxe Markdown + + + Aperçu de la note en Markdown + + + Supprimer la note + + + Épingler la note + + + Épingler la note en haut de la liste + + + Enregistrer la note + + + Prénom + + + Deuxième prénom + + + Nom + + + Date de naissance + + + Email + + + Téléphone + + + Entreprise + + + Nom d'utilisateur + + + Rue + + + Ville + + + Code postal + + + Province + + + Région + + + Pays + + + Carte d'identité + + + Carte vitale + + + Permis de conduire + + + Passeport + + + Notes + + + Étiquette de l'identité + + + Supprimer l'identité + + + Titre + + + URL + + + Charger une image + + + Charger une image (PNG, JPG, ICO — max 64 Ko) + + + Supprimer l'icône + + + Supprimer l'icône + + + E-mail ou identifiant + + + Générer un mot de passe + + + Générer un mot de passe + + + Scanner un QR code depuis une image + + + Importer un QR code depuis un fichier PNG/JPG + + + Coller l'URI otpauth depuis le presse-papiers + + + Coller une URI 'otpauth://' depuis le presse-papiers + + + Saisir le seed Base32 manuellement + + + Saisir la clé Base32 manuellement + + + Copier le code TOTP + + + Copier le code actuel dans le presse-papiers + + + Afficher la clé secrète + + + Afficher la clé Base32 + + + Supprimer le code 2FA + + + Supprimer le code 2FA de cette entrée + + + Clé Base32 (lecture seule) + + + Supprimer le mot de passe + + + Ex. Maison, Travail... + + + Jean + + + Dupont + + + JJ/MM/AAAA + + + jean@exemple.fr + + + +33 1 23 45 67 89 + + + 1 Rue de la Paix + + + Paris + + + Paris + + + Île-de-France + + + France + + + Numéro de document + + + Numéro de carte + + + Numéro de permis + + + Numéro de passeport + + + Ex. Google, Amazon... + + + email@exemple.fr + + + https://... + + + PIN + + diff --git a/src/PassKey.Desktop/Strings/it-IT/Resources.resw b/src/PassKey.Desktop/Strings/it-IT/Resources.resw index 949f4d1..7ebe004 100644 --- a/src/PassKey.Desktop/Strings/it-IT/Resources.resw +++ b/src/PassKey.Desktop/Strings/it-IT/Resources.resw @@ -183,8 +183,8 @@ + Nuova carta - - + Aggiungi identità + + Aggiungi identità + Nuova nota @@ -196,50 +196,53 @@ Salva Modifiche - + Nome - + Cognome - + + Nome o Cognome è obbligatorio + + Data di nascita - + Email - + Telefono - + Via - + Città - + CAP - + Provincia - + Regione - + Paese Contenuto - + Dati personali - + Indirizzo - + Documenti @@ -249,10 +252,10 @@ Dettagli carta - + Salva - + Elimina @@ -264,9 +267,6 @@ Cambia Master Password - - Genera e copia - Importa @@ -305,10 +305,10 @@ Versione - + Identità - + Note Sicure @@ -321,16 +321,16 @@ Set di caratteri - + Carta d'identità - + Tessera sanitaria - + Patente - + Passaporto @@ -423,7 +423,7 @@ Cerca password... - Nuova password + Aggiungi password Titolo @@ -449,6 +449,15 @@ Password * + + Campo obbligatorio + + + Campo obbligatorio + + + Campo obbligatorio + Note @@ -477,8 +486,11 @@ Cerca carte... + + Cerca identità... + - Nuova carta + Aggiungi carta Etichetta @@ -504,6 +516,15 @@ CVV/CVV2 * + + Campo obbligatorio + + + Campo obbligatorio + + + Campo obbligatorio + PIN @@ -843,7 +864,7 @@ Saltati (duplicati): {0} - CSV generico + CSV generico (.csv) Annulla @@ -900,7 +921,7 @@ Master password KeePass - KeePass (.kdbx) + KeePass 2.x (.kdbx) Password backup @@ -924,7 +945,7 @@ Il file non e un backup PassKey valido. - Bitwarden (JSON) + Bitwarden (.json / .zip) Mantieni entrambi @@ -1001,127 +1022,127 @@ Salvato! - + Guida - + Scorciatoie da tastiera - + Navigazione - + Naviga tra le sezioni (Dashboard, Password, Carte…, Impostazioni) - + Cerca (nella Dashboard) - + Blocca vault - + Apri Guida - + Azioni - + Nuovo elemento - + Modifica elemento selezionato - + Canc - + Elimina elemento selezionato - + Annulla / Torna indietro - + Domande frequenti - + Come modifico la master password? - + Vai in Impostazioni e scorri alla sezione Sicurezza. Fai clic su 'Cambia master password': ti verrà chiesta la password attuale (per confermare la tua identità) e poi la nuova password due volte. La nuova master password viene applicata immediatamente. - + Come creo un backup del vault? - + Vai in Impostazioni e scorri alla sezione Backup e importazione. Fai clic su 'Crea backup': viene generato un file cifrato con la tua master password. Conservalo in un luogo sicuro. Per ripristinare, usa 'Ripristina backup' nella stessa sezione e inserisci la master password con cui il backup è stato creato. - + Dove sono salvati i dati? - + Tutti i dati di PassKey sono salvati esclusivamente sul tuo dispositivo, in un database SQLite cifrato con AES-GCM 256 bit. Nessuna informazione viene trasmessa su Internet o a server esterni. Il percorso del file è indicato qui sotto — puoi selezionarlo e aprire la cartella direttamente da Esplora file. - + Come importo password da un altro gestore? - - Vai in Impostazioni e scorri alla sezione Backup e importazione. Fai clic su 'Importa dati' e seleziona il formato sorgente: CSV generico (compatibile con la maggior parte dei gestori), Bitwarden JSON, 1Password (.1pux) e KeePass (.kdbx). PassKey mostra un riepilogo degli elementi rilevati prima della conferma definitiva. + + Vai in Impostazioni, sezione Backup e importazione, e fai clic su 'Importa dati', poi scegli il formato sorgente. Sono supportati: CSV generico (.csv), compatibile con la maggior parte dei gestori e con gli export di Chrome, Firefox e NordPass; Bitwarden, sia come .json non cifrato sia come .zip con allegati (PassKey ne estrae automaticamente i dati); 1Password (.1pux); e KeePass 2.x (.kdbx), per il quale viene richiesta la password del database. Non sono importabili gli export Bitwarden cifrati (riesportali in formato non cifrato) né i database KeePass 1.x (.kdb), un formato più vecchio che va prima convertito in .kdbx con KeePass 2.x. Prima della conferma definitiva PassKey mostra sempre un riepilogo degli elementi rilevati. - + Informazioni - + Password manager sicuro per Windows - + Guida all'uso - + Dashboard - + La Dashboard è la schermata principale di PassKey. Mostra un riepilogo degli elementi salvati suddivisi per tipo (password, carte, identità, note), le statistiche del vault e gli elementi modificati di recente. Usa la barra di ricerca (Ctrl+F) per trovare rapidamente qualsiasi elemento tra tutte le categorie. - + Password - + Visualizza e gestisce tutte le password salvate nel vault. Fai clic su una voce per aprire il pannello di dettaglio: da lì puoi copiare il nome utente o la password con un clic, visualizzare l'URL associato, modificare i campi con il pulsante Modifica o eliminare la voce con conferma. - + Carte di credito - + Gestisce le carte di credito salvate nel vault. Numero di carta, titolare, data di scadenza e CVV sono conservati in modo completamente cifrato. Nel pannello di dettaglio puoi copiare il numero di carta con un clic e tener premuto il pulsante CVV per rivelarlo. Il colore della carta è personalizzabile. - + Identità - + Archivia documenti e informazioni di identità personale: nome e cognome, indirizzo, tipo e numero di documento, codice fiscale, data di nascita, numero di telefono e indirizzo e-mail. Utile per copiare rapidamente i singoli campi quando si compilano moduli senza dover cercare i documenti fisici. - + Note sicure - + Conserva informazioni sensibili sotto forma di note testuali libere, cifrate come tutti gli altri elementi del vault. Ogni nota ha un titolo, un colore identificativo personalizzabile e un campo testo a lunghezza libera. Ideale per PIN, codici di accesso, domande di sicurezza o qualsiasi dato che non rientra nelle altre categorie. - + Generatore - + Genera password casuali e crittograficamente sicure. Configura la lunghezza e i set di caratteri da includere (lettere maiuscole, minuscole, cifre e simboli). La forza della password è indicata in tempo reale. Copia il risultato negli appunti con un clic. - + Verifica password - + Analizza la sicurezza di una password senza salvarla nel vault. Mostra un punteggio da 0 a 4 stelle, una stima del tempo necessario per violarla con un attacco brute-force e un'indicazione se risulta compromessa in database pubblici di violazioni. Utile per verificare le password già in uso. - + Impostazioni - + Centralizza tutte le opzioni di configurazione: lingua dell'interfaccia, master password, timeout di blocco automatico, backup e ripristino del vault, e importazione da altri gestori di password (CSV, Bitwarden, 1Password, KeePass). @@ -1500,4 +1521,961 @@ Vuoi attivare questa verifica? Deboli - \ No newline at end of file + + Modifiche salvate + + + Elemento eliminato + + + Nome utente copiato + + + Password copiata + + + Copiato negli appunti + + + Password dimenticata + + + PassKey non memorizza la master password. Per sicurezza (modello zero-knowledge) solo tu la conosci. Se l'hai dimenticata puoi ripristinare un backup .pkbak oppure creare un vault nuovo. + + + Ripristina backup + + + Crea nuovo vault + + + Crea nuovo vault + + + Il vault attuale e tutti i suoi dati diventeranno definitivamente inaccessibili. Operazione irreversibile. Continuare? + + + Elimina e continua + + + Il vault si blocchera tra {0} secondi + + + Resta attivo + + + Blocco automatico aggiornato + + + Cronologia + + + Attività recenti del vault + + + Esporta CSV + + + Nessuna attività + + + Le azioni sul vault appariranno qui. + + + Data e ora + + + Tipo + + + Azione + + + File CSV + + + Cronologia esportata + + + Esportazione non riuscita + + + Creato + + + Modificato + + + Eliminato + + + Copiato + + + Sbloccato + + + Bloccato + + + Password + + + Carta + + + Identità + + + Nota + + + Vault + + + Cronologia attività + + + Estensione per il browser + + + Compila automaticamente le credenziali nei siti web + + + Installa l'estensione per il browser + + + L'estensione PassKey compila automaticamente nome utente e password nei siti web. Funziona quando PassKey è in esecuzione. + + + 1. Apri lo store del tuo browser: + + + Chrome / Edge + + + Firefox + + + 2. Nella pagina dello store, clicca "Aggiungi" (o "Installa"). + + + 3. Tieni PassKey in esecuzione: l'estensione si collega automaticamente. + + + Chiudi + + + Estensione per il browser + + + L'estensione per browser compila automaticamente nome utente e password nei siti web. Per installarla apri Impostazioni → Generali → Estensione per il browser: una procedura guidata ti porta agli store di Chrome/Edge o Firefox. L'estensione si collega a PassKey quando l'app è in esecuzione. + + + Svuota vault + + + Elimina tutte le password, carte, identità e note. Operazione irreversibile. + + + Svuota vault + + + Svuota vault + + + ATTENZIONE: questa operazione elimina DEFINITIVAMENTE tutte le password, le carte, le identità e le note del vault. I metadati e la master password vengono mantenuti. L'operazione è IRREVERSIBILE. Vuoi continuare? + + + Continua + + + Conferma svuotamento + + + Inserisci la master password per confermare lo svuotamento del vault. + + + Master password + + + Svuota vault + + + Inserisci la master password. + + + Master password errata. Il vault non è stato svuotato. + + + Vault svuotato + + + Tutte le voci sono state eliminate. Il vault è ora vuoto. + + + Errore + + + Ripristina backup esistente + + + Aggiungi password + + + Modifica password + + + Aggiungi carta + + + Modifica carta + + + Aggiungi identità + + + Modifica identità + + + Aggiungi nota + + + Modifica nota + + + Il formato email non sembra valido + + + Secondo nome + + + Azienda + + + Nome utente + + + Aggiungi nota + + + Seleziona una nota o creane una nuova + + + Note aggiuntive... + + + Password + + + Inserisci la master password + + + Ripeti la master password + + + Digita una password per verificarla... + + + La verifica avviene interamente offline, nessun dato viene inviato. + + + Escludi caratteri ambigui + + + Chiave Base32 + + + * Campo obbligatorio (Nome o Cognome) + + + Mostra PassKey + + + Blocca Vault + + + Esci + + + Vuoi mantenere PassKey attivo in background? + + + Minimizza + + + Chiudi PassKey + + + Inserisci chiave 2FA + + + Incolla la chiave Base32 fornita dal sito (lo stesso testo che inseriresti in Google Authenticator). Maiuscole e spazi vengono ignorati. + + + Es. JBSW Y3DP EHPK 3PXP + + + Salva + + + OK + + + Generale + + + Personale + + + Lavoro + + + Finanziario + + + Medico + + + Viaggio + + + Educazione + + + Legale + + + Tecnico + + + Altro + + + Adesso + + + {0} min fa + + + {0} ore fa + + + Ieri + + + {0}g fa + + + it-IT + + + Tutte le categorie + + + Filtro: {0} + + + Nessun risultato trovato. + + + Fissate + + + Note + + + Fissata + + + {0} car · {1} parole + + + {0} caratteri, {1} parole + + + Modifiche non salvate + + + Fissa nota in cima alla lista + + + Rimuovi dalla cima + + + Salvataggio in corso... + + + Salva completato + + + Filtra per categoria + + + Istantaneo + + + Pochi secondi + + + {0} minuti + + + {0} ore + + + {0} giorni + + + {0} anni + + + Nuova password generata + + + Password copiata negli appunti + + + {0} mila anni + + + {0} milioni di anni + + + {0} miliardi di anni + + + oltre mille miliardi di anni + + + Password compromesse ({0}) + + + Password deboli ({0}) + + + Password riutilizzate ({0}) + + + (senza titolo) + + + {0:N0} violazioni + + + riutilizzata + + + Usa almeno 8 caratteri + + + Usa almeno 12 caratteri per una protezione migliore + + + Aggiungi lettere maiuscole + + + Aggiungi lettere minuscole + + + Aggiungi numeri + + + Aggiungi simboli speciali (!@#$%) + + + Evita pattern comuni (password, 123456, qwerty...) + + + Questo file Bitwarden è cifrato e non può essere importato. Esporta i dati in formato non cifrato (.json) da Bitwarden e riprova. + + + Il file ZIP di Bitwarden non contiene 'data.json'. Esporta nuovamente i dati da Bitwarden e riprova. + + + Il file .1pux non contiene 'export.data'. + + + Questo è un database KeePass 1.x (.kdb), non supportato. Aprilo in KeePass 2.x e salvalo (o esportalo) in formato .kdbx, poi riprova. + + + Impossibile aprire il database KeePass. Verifica che la password sia corretta e che il file sia un .kdbx valido (KeePass 2.x). + + + Il file selezionato non contiene alcuna voce riconoscibile. Verifica che l'esportazione di origine non sia vuota e che il formato sia supportato. + + + Bitwarden (JSON o ZIP) + + + KeePass (.kdbx / .kdb) + + + Nessun codice QR riconoscibile nell'immagine. + + + QR riconosciuto ma non è un URI 'otpauth://' valido. + + + Negli appunti non c'è testo. + + + Il testo negli appunti non è un URI 'otpauth://' valido. + + + Chiave Base32 non valida (usa solo A-Z e 2-7). + + + Immagine troppo grande (max 64 KB) + + + Errore durante lo sblocco del vault. + + + ERRORE + + + Impossibile creare il vault. Riprova. + + + PassKey {0} disponibile + + + PassKey_Cronologia + + + Online + + + Carta senza nome + + + Identità senza nome + + + Nota senza titolo + + + Si è verificato un errore. Riprova. + + + Torna alle impostazioni + + + Torna alle impostazioni + + + Esporta cronologia in CSV + + + Crea Vault + + + Controlla password compromesse + + + Password da verificare + + + Accedi + + + Aggiungi la prima password + + + Importa da un altro gestore + + + Ripristina backup esistente + + + Esplora le impostazioni + + + Continua + + + Cerca identità + + + Aggiungi identità + + + Aggiungi identità (Ctrl+N) + + + Mostra password + + + Copia username + + + Copia password + + + Modifica + + + Elimina + + + Copia numero carta + + + Copia + + + Apri URL + + + Cerca password + + + Nuova password + + + Nuova password (Ctrl+N) + + + Nessuna nota. Crea la tua prima nota sicura. + + + Nessun risultato. Prova a modificare i filtri o la ricerca. + + + Cerca note + + + Filtra note per titolo, contenuto o categoria + + + Aggiungi nota (Ctrl+N) + + + Nuova nota (Ctrl+N) + + + Cerca carte + + + Cambia vista carte/lista + + + Cambia vista + + + Nuova carta + + + Nuova carta (Ctrl+N) + + + Salute del vault — apri Verifica vault + + + Apri Verifica vault + + + Totale password + + + Totale carte + + + Totale identità + + + Totale note + + + Dashboard + + + Password + + + Carte di credito + + + Identità + + + Note sicure + + + Generatore password + + + Verifica password e audit del vault + + + Blocca vault + + + Guida + + + Password generata + + + Copia password negli appunti + + + Copia negli appunti (Ctrl+C) + + + Genera nuova password + + + Genera nuova password + + + Lunghezza password + + + Includi lettere maiuscole + + + Includi lettere minuscole + + + Includi numeri + + + Includi simboli speciali + + + Escludi caratteri ambigui come zero/O e uno/l/I + + + Colore della carta + + + Modifiche non salvate + + + Modifiche non salvate + + + Titolo nota + + + Categoria nota + + + Modalità modifica + + + Anteprima Markdown + + + Contenuto nota + + + Supporta sintassi Markdown + + + Anteprima nota in Markdown + + + Elimina nota + + + Fissa nota + + + Fissa nota in cima alla lista + + + Salva nota + + + Nome + + + Secondo nome + + + Cognome + + + Data di nascita + + + Email + + + Telefono + + + Azienda + + + Nome utente + + + Via + + + Città + + + CAP + + + Provincia + + + Regione + + + Paese + + + Carta d'identità + + + Tessera sanitaria + + + Patente + + + Passaporto + + + Note + + + Etichetta identità + + + Elimina identità + + + Titolo + + + URL + + + Carica immagine + + + Carica immagine (PNG, JPG, ICO — max 64 KB) + + + Rimuovi icona + + + Rimuovi icona + + + Email o User ID + + + Genera password + + + Genera password + + + Scansiona QR code da immagine + + + Importa un QR code da file PNG/JPG + + + Incolla URI otpauth dalla clipboard + + + Incolla un URI 'otpauth://' dalla clipboard + + + Inserisci seed Base32 manualmente + + + Inserisci la chiave Base32 manualmente + + + Copia codice TOTP + + + Copia il codice attuale negli appunti + + + Mostra chiave segreta + + + Mostra la chiave Base32 + + + Rimuovi codice 2FA + + + Rimuovi il codice 2FA da questa voce + + + Chiave Base32 (sola lettura) + + + Elimina password + + + Es. Casa, Lavoro... + + + Mario + + + Rossi + + + GG/MM/AAAA + + + mario@esempio.it + + + +39 123 45 67 890 + + + Via Roma, 1 + + + Roma + + + RM + + + Lazio + + + Italia + + + Numero documento + + + Numero tessera + + + Numero patente + + + Numero passaporto + + + Es. Google, Amazon... + + + email@esempio.it + + + https://... + + + PIN + + diff --git a/src/PassKey.Desktop/Strings/pt-PT/Resources.resw b/src/PassKey.Desktop/Strings/pt-PT/Resources.resw index 19f9770..df76b07 100644 --- a/src/PassKey.Desktop/Strings/pt-PT/Resources.resw +++ b/src/PassKey.Desktop/Strings/pt-PT/Resources.resw @@ -183,8 +183,8 @@ + Novo cartão - - + Adicionar identidade + + Adicionar identidade + Nova nota @@ -196,50 +196,53 @@ Guardar alterações - + Nome próprio - + Apelido - + + Primeiro ou último nome é obrigatório + + Data de nascimento - + Email - + Telefone - + Rua - + Cidade - + Código postal - + Distrito - + Região - + País Conteúdo - + Dados pessoais - + Morada - + Documentos @@ -249,10 +252,10 @@ Detalhes do cartão - + Guardar - + Eliminar @@ -264,9 +267,6 @@ Alterar palavra-passe mestra - - Gerar e copiar - Importar @@ -305,10 +305,10 @@ Versão - + Identidades - + Notas seguras @@ -321,16 +321,16 @@ Conjuntos de caracteres - + Cartão de cidadão - + Cartão de saúde - + Carta de condução - + Passaporte @@ -416,10 +416,10 @@ Continuar - Novo cartão + Adicionar cartão - Nova palavra-passe + Adicionar palavra-passe Cancelar @@ -445,6 +445,15 @@ CVV/CVV2 * + + Campo obrigatório + + + Campo obrigatório + + + Campo obrigatório + Validade * @@ -466,6 +475,15 @@ Palavra-passe * + + Campo obrigatório + + + Campo obrigatório + + + Campo obrigatório + PIN @@ -487,6 +505,9 @@ Pesquisar cartões... + + Pesquisar identidades... + Pesquisar palavras-passe... @@ -841,7 +862,7 @@ Pulados (duplicatas): {0} - CSV generico + CSV generico (.csv) Cancelar @@ -898,7 +919,7 @@ Senha mestre KeePass - KeePass (.kdbx) + KeePass 2.x (.kdbx) Senha de backup @@ -922,7 +943,7 @@ O arquivo nao e um backup PassKey valido. - Bitwarden (JSON) + Bitwarden (.json / .zip) Manter ambos @@ -999,127 +1020,127 @@ Guardado! - + Ajuda - + Atalhos de teclado - + Navegação - + Navegar entre secções (Dashboard, Palavras-passe, Cartões…, Definições) - + Pesquisar (no Dashboard) - + Bloquear cofre - + Abrir ajuda - + Ações - + Novo elemento - + Editar elemento selecionado - + Del - + Eliminar elemento selecionado - + Cancelar / Voltar - + Perguntas frequentes - + Como altero a palavra-passe mestra? - + Vá a Definições e desloque-se para a secção Segurança. Clique em 'Alterar palavra-passe mestra': ser-lhe-á pedida a palavra-passe atual (para confirmar a sua identidade) e depois a nova palavra-passe duas vezes. A nova palavra-passe mestra é aplicada imediatamente. - + Como crio uma cópia de segurança do cofre? - + Vá a Definições e desloque-se para a secção Cópia de segurança e importação. Clique em 'Criar cópia de segurança': é gerado um ficheiro cifrado com a sua palavra-passe mestra. Guarde-o num local seguro. Para restaurar, utilize 'Restaurar cópia de segurança' na mesma secção e introduza a palavra-passe mestra utilizada na criação. - + Onde são guardados os meus dados? - + Todos os dados do PassKey são armazenados exclusivamente no seu dispositivo, numa base de dados SQLite cifrada com AES-GCM de 256 bits. Nenhuma informação é transmitida pela Internet ou para servidores externos. O caminho do ficheiro é indicado abaixo. - + Como importo palavras-passe de outro gestor? - - Vá a Definições e desloque-se para a secção Cópia de segurança e importação. Clique em 'Importar dados' e selecione o formato de origem: CSV genérico, Bitwarden JSON, 1Password (.1pux) e KeePass (.kdbx). O PassKey mostra um resumo dos itens detetados antes da confirmação final. + + Vai a Definições, secção Cópia de segurança e importação, e clica em 'Importar dados', depois escolhe o formato de origem. Formatos suportados: CSV genérico (.csv), compatível com a maioria dos gestores e com as exportações do Chrome, Firefox e NordPass; Bitwarden, como .json não cifrado ou como .zip com anexos (o PassKey extrai os dados automaticamente); 1Password (.1pux); e KeePass 2.x (.kdbx), para o qual é pedida a palavra-passe da base de dados. Não é possível importar exportações do Bitwarden cifradas (volta a exportá-las sem cifragem) nem bases de dados do KeePass 1.x (.kdb), um formato mais antigo que deves primeiro converter para .kdbx com o KeePass 2.x. Antes da confirmação final, o PassKey mostra sempre um resumo dos elementos detetados. - + Sobre - + Gestor de palavras-passe seguro para Windows - + Guia de utilização - + Dashboard - + O Dashboard é o ecrã principal do PassKey. Mostra um resumo dos itens guardados por tipo (palavras-passe, cartões, identidades, notas), estatísticas do cofre e os itens modificados recentemente. Utilize a barra de pesquisa (Ctrl+F) para encontrar rapidamente qualquer item em todas as categorias. - + Palavras-passe - + Visualize e gira todas as palavras-passe guardadas no cofre. Clique numa entrada para abrir o painel de detalhes: pode copiar o nome de utilizador ou a palavra-passe com um clique, ver o URL associado, editar os campos com o botão Editar ou eliminar a entrada com confirmação. - + Cartões de crédito - + Gira os cartões de crédito guardados no cofre. O número do cartão, titular, data de validade e CVV são armazenados completamente cifrados. No painel de detalhes pode copiar o número do cartão com um clique e manter premido o botão CVV para o revelar. A cor do cartão é personalizável. - + Identidades - + Armazene documentos de identidade e informações pessoais: nome e apelido, morada, tipo e número de documento, número de identificação fiscal, data de nascimento, telefone e e-mail. Útil para copiar rapidamente campos individuais ao preencher formulários sem procurar documentos físicos. - + Notas seguras - + Conserve informações sensíveis como notas de texto livre, cifradas como todos os outros itens do cofre. Cada nota tem um título, uma cor personalizável e um campo de texto de comprimento livre. Ideal para PINs, códigos de acesso, perguntas de segurança ou quaisquer dados que não se enquadrem noutras categorias. - + Gerador - + Gere palavras-passe aleatórias e criptograficamente seguras. Configure o comprimento e os conjuntos de caracteres a incluir (maiúsculas, minúsculas, dígitos e símbolos). A força da palavra-passe é mostrada em tempo real. Copie o resultado para a área de transferência com um clique. - + Verificador de palavras-passe - + Analise a segurança de uma palavra-passe sem a guardar no cofre. Mostra uma pontuação de 0 a 4 estrelas, uma estimativa do tempo necessário para a violar por força bruta e se aparece em bases de dados públicas de violações. Útil para verificar palavras-passe já em uso. - + Definições - + Centraliza todas as opções de configuração: idioma da interface, palavra-passe mestra, tempo limite de bloqueio automático, cópia de segurança e restauro do cofre, e importação de outros gestores de palavras-passe (CSV, Bitwarden, 1Password, KeePass). @@ -1497,4 +1518,961 @@ Deseja ativar esta verificacao? Fracas - \ No newline at end of file + + Alteracoes guardadas + + + Item eliminado + + + Nome de utilizador copiado + + + Palavra-passe copiada + + + Copiado para a area de transferencia + + + Palavra-passe esquecida + + + O PassKey nunca guarda a palavra-passe principal. Por seguranca (modelo zero-knowledge) so o utilizador a conhece. Se a esqueceu, pode restaurar uma copia de seguranca .pkbak ou criar um novo cofre. + + + Restaurar copia de seguranca + + + Criar novo cofre + + + Criar novo cofre + + + O cofre atual e todos os seus dados ficarao permanentemente inacessiveis. Operacao irreversivel. Continuar? + + + Eliminar e continuar + + + O cofre sera bloqueado em {0} segundos + + + Manter ativo + + + Bloqueio automatico atualizado + + + Histórico + + + Atividade recente do cofre + + + Exportar CSV + + + Nenhuma atividade + + + As ações do cofre aparecerão aqui. + + + Data e hora + + + Tipo + + + Ação + + + Ficheiro CSV + + + Histórico exportado + + + Falha na exportação + + + Criado + + + Modificado + + + Eliminado + + + Copiado + + + Desbloqueado + + + Bloqueado + + + Palavra-passe + + + Cartão + + + Identidade + + + Nota + + + Cofre + + + Histórico de atividade + + + Extensão para o navegador + + + Preenche automaticamente as credenciais nos sites + + + Instalar a extensão para o navegador + + + A extensão PassKey preenche automaticamente nomes de utilizador e palavras-passe nos sites. Funciona enquanto o PassKey estiver em execução. + + + 1. Abra a loja do seu navegador: + + + Chrome / Edge + + + Firefox + + + 2. Na página da loja, clique em "Adicionar" (ou "Instalar"). + + + 3. Mantenha o PassKey em execução: a extensão liga-se automaticamente. + + + Fechar + + + Extensão para o navegador + + + A extensão para navegador preenche automaticamente nomes de utilizador e palavras-passe nos sites. Para instalá-la, abra Definições → Geral → Extensão para o navegador: um procedimento guiado leva-o às lojas do Chrome/Edge ou Firefox. A extensão liga-se ao PassKey enquanto a aplicação estiver em execução. + + + Esvaziar cofre + + + Elimina todas as palavras-passe, cartões, identidades e notas. Operação irreversível. + + + Esvaziar cofre + + + Esvaziar cofre + + + AVISO: esta operação elimina DEFINITIVAMENTE todas as palavras-passe, cartões, identidades e notas do cofre. Os metadados e a palavra-passe principal são mantidos. A operação é IRREVERSÍVEL. Deseja continuar? + + + Continuar + + + Confirmar esvaziamento + + + Introduza a palavra-passe principal para confirmar o esvaziamento do cofre. + + + Palavra-passe principal + + + Esvaziar cofre + + + Introduza a palavra-passe principal. + + + Palavra-passe principal incorreta. O cofre não foi esvaziado. + + + Cofre esvaziado + + + Todas as entradas foram eliminadas. O cofre está agora vazio. + + + Erro + + + Restaurar uma cópia de segurança existente + + + Adicionar palavra-passe + + + Editar palavra-passe + + + Adicionar cartão + + + Editar cartão + + + Adicionar identidade + + + Editar identidade + + + Adicionar nota + + + Editar nota + + + O formato do e-mail não parece válido + + + Nome do meio + + + Empresa + + + Nome de utilizador + + + Adicionar nota + + + Selecione uma nota ou crie uma nova + + + Notas adicionais... + + + Palavra-passe + + + Introduza a palavra-passe mestra + + + Repita a palavra-passe mestra + + + Escreva uma palavra-passe para verificá-la... + + + A verificação é feita totalmente offline; nenhum dado é enviado. + + + Excluir caracteres ambíguos + + + Chave Base32 + + + * Campo obrigatório (nome ou apelido) + + + Mostrar o PassKey + + + Bloquear o cofre + + + Sair + + + Manter o PassKey ativo em segundo plano? + + + Minimizar + + + Fechar o PassKey + + + Introduzir chave 2FA + + + Cole a chave Base32 fornecida pelo site (o mesmo texto que introduziria no Google Authenticator). Maiúsculas e espaços são ignorados. + + + Ex. JBSW Y3DP EHPK 3PXP + + + Guardar + + + OK + + + Geral + + + Pessoal + + + Trabalho + + + Financeiro + + + Médico + + + Viagem + + + Educação + + + Jurídico + + + Técnico + + + Outro + + + Agora mesmo + + + há {0} min + + + há {0} h + + + Ontem + + + há {0} d + + + pt-PT + + + Todas as categorias + + + Filtro: {0} + + + Nenhum resultado encontrado. + + + Fixadas + + + Notas + + + Fixada + + + {0} car · {1} palavras + + + {0} caracteres, {1} palavras + + + Alterações não guardadas + + + Fixar nota no topo da lista + + + Remover do topo + + + A guardar... + + + Guardado + + + Filtrar por categoria + + + Instantâneo + + + Poucos segundos + + + {0} minutos + + + {0} horas + + + {0} dias + + + {0} anos + + + Nova palavra-passe gerada + + + Palavra-passe copiada para a área de transferência + + + {0} mil anos + + + {0} milhões de anos + + + {0} mil milhões de anos + + + mais de um bilião de anos + + + Palavras-passe comprometidas ({0}) + + + Palavras-passe fracas ({0}) + + + Palavras-passe reutilizadas ({0}) + + + (sem título) + + + {0:N0} fugas + + + reutilizada + + + Use pelo menos 8 caracteres + + + Use pelo menos 12 caracteres para melhor proteção + + + Adicione letras maiúsculas + + + Adicione letras minúsculas + + + Adicione números + + + Adicione símbolos especiais (!@#$%) + + + Evite padrões comuns (password, 123456, qwerty...) + + + Este ficheiro do Bitwarden está cifrado e não pode ser importado. Exporte os seus dados do Bitwarden em formato não cifrado (.json) e tente novamente. + + + O ficheiro ZIP do Bitwarden não contém 'data.json'. Exporte novamente os seus dados do Bitwarden e tente novamente. + + + O ficheiro .1pux não contém 'export.data'. + + + Esta é uma base de dados do KeePass 1.x (.kdb), que não é suportada. Abra-a no KeePass 2.x e guarde-a (ou exporte-a) no formato .kdbx, depois tente novamente. + + + Não foi possível abrir a base de dados do KeePass. Verifique se a palavra-passe está correta e se o ficheiro é um .kdbx válido (KeePass 2.x). + + + O ficheiro selecionado não contém entradas reconhecíveis. Certifique-se de que a exportação de origem não está vazia e de que o formato é suportado. + + + Bitwarden (JSON ou ZIP) + + + KeePass (.kdbx / .kdb) + + + Nenhum código QR reconhecível na imagem. + + + QR reconhecido, mas não é um URI 'otpauth://' válido. + + + Não há texto na área de transferência. + + + O texto da área de transferência não é um URI 'otpauth://' válido. + + + Chave Base32 inválida (use apenas A-Z e 2-7). + + + Imagem demasiado grande (máx. 64 KB) + + + Erro ao desbloquear o cofre. + + + ERRO + + + Não foi possível criar o cofre. Tente novamente. + + + PassKey {0} disponível + + + PassKey_Historico + + + Online + + + Cartão sem nome + + + Identidade sem nome + + + Nota sem título + + + Ocorreu um erro. Tente novamente. + + + Voltar às definições + + + Voltar às definições + + + Exportar histórico para CSV + + + Criar cofre + + + Verificar palavras-passe comprometidas + + + Palavra-passe a verificar + + + Desbloquear + + + Adicione a sua primeira palavra-passe + + + Importar de outro gestor + + + Restaurar uma cópia de segurança existente + + + Explorar definições + + + Continuar + + + Pesquisar identidades + + + Adicionar identidade + + + Adicionar identidade (Ctrl+N) + + + Mostrar palavra-passe + + + Copiar nome de utilizador + + + Copiar palavra-passe + + + Editar + + + Eliminar + + + Copiar número do cartão + + + Copiar + + + Abrir URL + + + Pesquisar palavras-passe + + + Nova palavra-passe + + + Nova palavra-passe (Ctrl+N) + + + Sem notas. Crie a sua primeira nota segura. + + + Sem resultados. Tente alterar os filtros ou a pesquisa. + + + Pesquisar notas + + + Filtrar notas por título, conteúdo ou categoria + + + Adicionar nota (Ctrl+N) + + + Nova nota (Ctrl+N) + + + Pesquisar cartões + + + Alternar vista cartões/lista + + + Mudar vista + + + Novo cartão + + + Novo cartão (Ctrl+N) + + + Saúde do cofre — abrir Verificação do cofre + + + Abrir Verificação do cofre + + + Total de palavras-passe + + + Total de cartões + + + Total de identidades + + + Total de notas + + + Painel + + + Palavras-passe + + + Cartões de crédito + + + Identidades + + + Notas seguras + + + Gerador de palavras-passe + + + Verificação de palavras-passe e auditoria do cofre + + + Bloquear cofre + + + Ajuda + + + Palavra-passe gerada + + + Copiar palavra-passe para a área de transferência + + + Copiar para a área de transferência (Ctrl+C) + + + Gerar nova palavra-passe + + + Gerar nova palavra-passe + + + Comprimento da palavra-passe + + + Incluir letras maiúsculas + + + Incluir letras minúsculas + + + Incluir números + + + Incluir símbolos especiais + + + Excluir caracteres ambíguos como zero/O e um/l/I + + + Cor do cartão + + + Alterações não guardadas + + + Alterações não guardadas + + + Título da nota + + + Categoria da nota + + + Modo de edição + + + Pré-visualização Markdown + + + Conteúdo da nota + + + Suporta sintaxe Markdown + + + Pré-visualização da nota em Markdown + + + Eliminar nota + + + Fixar nota + + + Fixar nota no topo da lista + + + Guardar nota + + + Nome próprio + + + Nome do meio + + + Apelido + + + Data de nascimento + + + Email + + + Telefone + + + Empresa + + + Nome de utilizador + + + Rua + + + Cidade + + + Código postal + + + Distrito + + + Região + + + País + + + Cartão de cidadão + + + Cartão de saúde + + + Carta de condução + + + Passaporte + + + Notas + + + Etiqueta da identidade + + + Eliminar identidade + + + Título + + + URL + + + Carregar imagem + + + Carregar imagem (PNG, JPG, ICO — máx. 64 KB) + + + Remover ícone + + + Remover ícone + + + E-mail ou ID de utilizador + + + Gerar palavra-passe + + + Gerar palavra-passe + + + Ler código QR a partir de imagem + + + Importar um código QR de um ficheiro PNG/JPG + + + Colar URI otpauth da área de transferência + + + Colar uma URI 'otpauth://' da área de transferência + + + Introduzir seed Base32 manualmente + + + Introduzir a chave Base32 manualmente + + + Copiar código TOTP + + + Copiar o código atual para a área de transferência + + + Mostrar chave secreta + + + Mostrar a chave Base32 + + + Remover código 2FA + + + Remover o código 2FA desta entrada + + + Chave Base32 (apenas leitura) + + + Eliminar palavra-passe + + + Ex. Casa, Trabalho... + + + João + + + Silva + + + DD/MM/AAAA + + + joao@exemplo.pt + + + +351 912 345 678 + + + Rua Principal, 1 + + + Lisboa + + + Lisboa + + + Lisboa + + + Portugal + + + Número do documento + + + Número do cartão + + + Número da carta + + + Número do passaporte + + + Ex. Google, Amazon... + + + email@exemplo.pt + + + https://... + + + PIN + + diff --git a/src/PassKey.Desktop/Themes/ThemeColors.xaml b/src/PassKey.Desktop/Themes/ThemeColors.xaml index 41bd4d3..ddd13af 100644 --- a/src/PassKey.Desktop/Themes/ThemeColors.xaml +++ b/src/PassKey.Desktop/Themes/ThemeColors.xaml @@ -11,6 +11,10 @@ + + + @@ -39,6 +43,9 @@ + + + @@ -51,6 +58,10 @@ + + + @@ -79,6 +90,9 @@ + + + diff --git a/src/PassKey.Desktop/ViewModels/ActivityLogViewModel.cs b/src/PassKey.Desktop/ViewModels/ActivityLogViewModel.cs new file mode 100644 index 0000000..cb99a43 --- /dev/null +++ b/src/PassKey.Desktop/ViewModels/ActivityLogViewModel.cs @@ -0,0 +1,152 @@ +using System.Collections.ObjectModel; +using System.Text; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Windows.ApplicationModel.Resources; +using PassKey.Core.Interfaces; +using PassKey.Desktop.Services; + +namespace PassKey.Desktop.ViewModels; + +/// +/// ViewModel for the activity-log ("Cronologia") viewer: loads the most recent vault +/// activity entries newest-first and supports exporting the full list to a CSV file. +/// +public partial class ActivityLogViewModel : ObservableObject +{ + /// Upper bound on entries loaded into the viewer. The log is capped so a very + /// long history never stalls the UI; the newest are shown. + private const int MaxEntries = 1000; + + private readonly IVaultRepository _repository; + private readonly IFilePickerService _filePicker; + private readonly IToastService _toast; + private readonly ResourceLoader _res = new(); + + /// Activity entries shown in the list, ordered newest-first. + public ObservableCollection Entries { get; } = []; + + /// when there are no entries — drives the empty-state placeholder. + [ObservableProperty] + public partial bool IsEmpty { get; set; } + + public ActivityLogViewModel( + IVaultRepository repository, + IFilePickerService filePicker, + IToastService toast) + { + _repository = repository; + _filePicker = filePicker; + _toast = toast; + } + + /// Loads the most recent activity entries from the repository into . + public async Task LoadAsync() + { + Entries.Clear(); + var raw = await _repository.GetRecentActivityAsync(MaxEntries); + foreach (var e in raw) + { + Entries.Add(new ActivityLogItem + { + FormattedTime = e.Timestamp.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss"), + EntityLabel = ResolveEntityLabel(e.EntityType), + ActionLabel = ResolveActionLabel(e.Action), + Action = e.Action, + IconGlyph = ResolveIcon(e.EntityType) + }); + } + IsEmpty = Entries.Count == 0; + } + + /// Exports every loaded entry to a UTF-8 (BOM) CSV file chosen via the save dialog. + [RelayCommand] + private async Task ExportCsvAsync() + { + if (Entries.Count == 0) return; + + var path = await _filePicker.PickSaveFileAsync( + $"{_res.GetString("ActivityCsvFileName")}_{DateTime.Now:yyyyMMdd}", + ".csv", + _res.GetString("ActivityCsvFileType")); + if (path is null) return; + + var sb = new StringBuilder(); + sb.Append(CsvField(_res.GetString("ActivityColTime"))).Append(',') + .Append(CsvField(_res.GetString("ActivityColType"))).Append(',') + .Append(CsvField(_res.GetString("ActivityColAction"))).Append('\n'); + foreach (var item in Entries) + { + sb.Append(CsvField(item.FormattedTime)).Append(',') + .Append(CsvField(item.EntityLabel)).Append(',') + .Append(CsvField(item.ActionLabel)).Append('\n'); + } + + try + { + await File.WriteAllTextAsync(path, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); + _toast.Show(ToastSeverity.Success, _res.GetString("ActivityExportSuccess")); + } + catch + { + _toast.Show(ToastSeverity.Error, _res.GetString("ActivityExportError")); + } + } + + /// Quotes a CSV field when it contains a comma, quote, or newline (RFC 4180). + private static string CsvField(string value) => + value.Contains(',') || value.Contains('"') || value.Contains('\n') || value.Contains('\r') + ? $"\"{value.Replace("\"", "\"\"")}\"" + : value; + + private string ResolveEntityLabel(string entityType) => entityType switch + { + "PasswordEntry" => _res.GetString("ActivityEntityPassword"), + "CreditCardEntry" => _res.GetString("ActivityEntityCard"), + "IdentityEntry" => _res.GetString("ActivityEntityIdentity"), + "SecureNoteEntry" => _res.GetString("ActivityEntityNote"), + "Vault" => _res.GetString("ActivityEntityVault"), + _ => entityType + }; + + private string ResolveActionLabel(string action) => action switch + { + "Created" => _res.GetString("ActivityActionCreated"), + "Modified" => _res.GetString("ActivityActionModified"), + "Updated" => _res.GetString("ActivityActionModified"), + "Deleted" => _res.GetString("ActivityActionDeleted"), + "Copied" => _res.GetString("ActivityActionCopied"), + "Unlocked" => _res.GetString("ActivityActionUnlocked"), + "Locked" => _res.GetString("ActivityActionLocked"), + _ => action + }; + + private static string ResolveIcon(string entityType) => entityType switch + { + "PasswordEntry" => "", + "CreditCardEntry" => "", + "IdentityEntry" => "", + "SecureNoteEntry" => "", + "Vault" => "", + _ => "" + }; +} + +/// Display model for a single row in the activity-log viewer. +public sealed class ActivityLogItem +{ + /// Local timestamp formatted for display and CSV export. + public string FormattedTime { get; init; } = string.Empty; + + /// Localised entity-type label (e.g. "Password", "Carta"). + public string EntityLabel { get; init; } = string.Empty; + + /// Localised action label (e.g. "Creato", "Eliminato"). + public string ActionLabel { get; init; } = string.Empty; + + /// Raw action string ("Created", "Modified", "Deleted", …) — drives the action indicator. + public string Action { get; init; } = string.Empty; + + /// Segoe MDL2 glyph representing the entity type. + public string IconGlyph { get; init; } = string.Empty; +} diff --git a/src/PassKey.Desktop/ViewModels/Base/BaseDetailViewModel.cs b/src/PassKey.Desktop/ViewModels/Base/BaseDetailViewModel.cs index 4ae1601..84e903e 100644 --- a/src/PassKey.Desktop/ViewModels/Base/BaseDetailViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/Base/BaseDetailViewModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Core.Models; using PassKey.Desktop.Services; @@ -34,6 +35,9 @@ public abstract partial class BaseDetailViewModel : ObservableObject /// Dialog queue used to serialize instances. protected readonly IDialogQueueService DialogQueue; + /// Localized string resources (delete dialog + subclass fallback display names). + protected readonly ResourceLoader _res = new(); + /// The entry currently being edited, or when creating a new entry. protected TEntry? EditingEntry; @@ -42,10 +46,6 @@ public abstract partial class BaseDetailViewModel : ObservableObject /// Indicates whether the panel is currently in "create new" mode (vs editing an existing entry). public bool IsNew => _isNew; - /// Localized title displayed at the top of the editor panel ("Aggiungi…" or "Modifica…"). - [ObservableProperty] - public partial string PanelTitle { get; set; } = string.Empty; - /// Whether the current field values satisfy the type-specific validation rules. [ObservableProperty] public partial bool CanSave { get; set; } @@ -90,28 +90,9 @@ protected BaseDetailViewModel(IVaultStateService vaultState, IDialogQueueService /// Recompute based on the type-specific required-field validation. protected abstract void UpdateCanSave(); - /// Localised panel title used when creating a new entry (e.g., "Aggiungi password"). - protected abstract string GetPanelTitleForNew(); - - /// Localised panel title used when editing an existing entry (e.g., "Modifica password"). - protected abstract string GetPanelTitleForEdit(); - - /// Localised title shown in the delete-confirmation dialog (e.g., "Elimina password"). - protected abstract string GetDeleteDialogTitle(); - - /// Best-effort display name used inside the delete-confirmation dialog body for the supplied entry. + /// Best-effort display name shown inside the delete-confirmation dialog for the supplied entry. protected abstract string GetDeleteDisplayName(TEntry entry); - /// Localised body template for the delete dialog. Default mirrors the original copy across all four VMs. - protected virtual string GetDeleteDialogContent(string displayName) - => $"Eliminare \"{displayName}\"?\nQuesta azione è irreversibile."; - - /// Localised primary-button text on the delete dialog. - protected virtual string GetDeletePrimaryButtonText() => "Elimina"; - - /// Localised close-button text on the delete dialog. - protected virtual string GetDeleteCloseButtonText() => "Annulla"; - /// Hook invoked after a successful save of a new entry (default: no-op). /// Subclasses can override to transition the panel state in-place (e.g., notes editor). protected virtual void OnSavedNew(TEntry entry) { } @@ -127,7 +108,6 @@ public virtual void StartNew() { EditingEntry = null; _isNew = true; - PanelTitle = GetPanelTitleForNew(); ResetFieldsForNew(); UpdateCanSave(); } @@ -137,7 +117,6 @@ public virtual void StartEdit(TEntry entry) { EditingEntry = entry; _isNew = false; - PanelTitle = GetPanelTitleForEdit(); LoadFromEntry(entry); UpdateCanSave(); } @@ -193,11 +172,12 @@ protected virtual async Task DeleteAsync() { if (EditingEntry is null || _isNew) return; + var displayName = GetDeleteDisplayName(EditingEntry); var confirmed = await DialogQueue.ConfirmAsync( - title: GetDeleteDialogTitle(), - content: GetDeleteDialogContent(GetDeleteDisplayName(EditingEntry)), - primaryButtonText: GetDeletePrimaryButtonText(), - closeButtonText: GetDeleteCloseButtonText()); + title: string.Format(_res.GetString("DeleteConfirmTitle"), displayName), + content: string.Format(_res.GetString("DeleteConfirmMessage"), displayName), + primaryButtonText: _res.GetString("DeleteButton"), + closeButtonText: _res.GetString("CancelButton")); if (confirmed) { diff --git a/src/PassKey.Desktop/ViewModels/CreditCardDetailViewModel.cs b/src/PassKey.Desktop/ViewModels/CreditCardDetailViewModel.cs index c99cb64..62314ea 100644 --- a/src/PassKey.Desktop/ViewModels/CreditCardDetailViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/CreditCardDetailViewModel.cs @@ -53,6 +53,17 @@ public partial class CreditCardDetailViewModel : BaseDetailViewModel "Aggiungi carta"; - protected override string GetPanelTitleForEdit() => "Modifica carta"; - protected override string GetDeleteDialogTitle() => "Elimina carta"; protected override string GetDeleteDisplayName(CreditCardEntry entry) - => entry.Label is { Length: > 0 } l ? l : "Carta senza nome"; + => entry.Label is { Length: > 0 } l ? l : _res.GetString("CardNoName"); protected override IList GetVaultCollection(Vault vault) => vault.CreditCards; @@ -85,6 +93,9 @@ protected override void ResetFieldsForNew() DetectedCardType = CardType.Unknown; IsLuhnValid = false; FormattedCardNumber = string.Empty; + IsCardNumberEmpty = true; + IsCardholderNameEmpty = true; + IsCvvEmpty = true; } protected override void LoadFromEntry(CreditCardEntry entry) @@ -146,16 +157,27 @@ protected override void UpdateCanSave() partial void OnCardNumberChanged(string value) { + IsCardNumberEmpty = string.IsNullOrWhiteSpace(value); DetectedCardType = CardTypeDetector.Detect(value); IsLuhnValid = CardTypeDetector.ValidateLuhn(value); UpdateCardNumberDisplay(); UpdateCanSave(); } - partial void OnCardholderNameChanged(string value) => UpdateCanSave(); + partial void OnCardholderNameChanged(string value) + { + IsCardholderNameEmpty = string.IsNullOrWhiteSpace(value); + UpdateCanSave(); + } + partial void OnExpiryMonthChanged(int value) => UpdateCanSave(); partial void OnExpiryYearChanged(int value) => UpdateCanSave(); - partial void OnCvvChanged(string value) => UpdateCanSave(); + + partial void OnCvvChanged(string value) + { + IsCvvEmpty = string.IsNullOrWhiteSpace(value); + UpdateCanSave(); + } private void UpdateCardNumberDisplay() { diff --git a/src/PassKey.Desktop/ViewModels/CreditCardsListViewModel.cs b/src/PassKey.Desktop/ViewModels/CreditCardsListViewModel.cs index 5072c8d..503c031 100644 --- a/src/PassKey.Desktop/ViewModels/CreditCardsListViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/CreditCardsListViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Core.Constants; using PassKey.Core.Interfaces; using PassKey.Core.Models; @@ -18,6 +19,8 @@ public partial class CreditCardsListViewModel : ObservableObject, IDisposable private readonly IClipboardService _clipboard; private readonly IDialogQueueService _dialogQueue; private readonly IVaultRepository _repository; + private readonly IToastService _toast; + private readonly ResourceLoader _resourceLoader = new(); private bool _disposed; private List _allEntries = []; @@ -58,12 +61,14 @@ public CreditCardsListViewModel( IClipboardService clipboard, IDialogQueueService dialogQueue, IVaultRepository repository, + IToastService toast, CreditCardDetailViewModel detailViewModel) { _vaultState = vaultState; _clipboard = clipboard; _dialogQueue = dialogQueue; _repository = repository; + _toast = toast; _detailVm = detailViewModel; _vaultState.VaultLocked += OnVaultLocked; @@ -180,14 +185,11 @@ private void CopyCardNumber(CreditCardEntry? entry) { if (entry is not null && !string.IsNullOrEmpty(entry.CardNumber)) { - var masked = CardTypeDetector.MaskCardNumber(entry.CardNumber, entry.CardType); _clipboard.Copy(entry.CardNumber, CopyType.Sensitive); + _toast.Show(ToastSeverity.Info, _resourceLoader.GetString("ToastCopied")); } } - /// Raised after a successful save, for the View to show a toast. - public event Action? SaveCompleted; - public void CloseDetail() { IsDetailOpen = false; @@ -195,21 +197,23 @@ public void CloseDetail() } [RelayCommand] - private async Task DeleteSelectedAsync() + private async Task DeleteSelectedAsync(CreditCardEntry? entry) { - if (SelectedEntry is null) return; + // entry != null → quick-delete from a list row; entry == null → keyboard Delete on the selection. + var target = entry ?? SelectedEntry; + if (target is null) return; var confirmed = await _dialogQueue.ConfirmAsync( - title: "Elimina carta", - content: $"Eliminare \"{SelectedEntry.Label}\"?\nQuesta azione è irreversibile.", - primaryButtonText: "Elimina", - closeButtonText: "Annulla"); + title: string.Format(_resourceLoader.GetString("DeleteConfirmTitle"), target.Label), + content: string.Format(_resourceLoader.GetString("DeleteConfirmMessage"), target.Label), + primaryButtonText: _resourceLoader.GetString("DeleteButton"), + closeButtonText: _resourceLoader.GetString("CancelButton")); if (confirmed) { var vault = _vaultState.CurrentVault; - var entryId = SelectedEntry.Id; - vault?.CreditCards.Remove(SelectedEntry); + var entryId = target.Id; + vault?.CreditCards.Remove(target); await _vaultState.SaveVaultAsync(); await _repository.LogActivityAsync(new ActivityLogEntry { @@ -220,6 +224,7 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseDetail(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastDeleted")); } } @@ -235,7 +240,7 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseDetail(); - SaveCompleted?.Invoke(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastSaved")); } private async void OnEntryDeleted(Guid entryId) @@ -250,5 +255,6 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseDetail(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastDeleted")); } } diff --git a/src/PassKey.Desktop/ViewModels/DashboardViewModel.cs b/src/PassKey.Desktop/ViewModels/DashboardViewModel.cs index c7d9bba..7d572db 100644 --- a/src/PassKey.Desktop/ViewModels/DashboardViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/DashboardViewModel.cs @@ -18,21 +18,20 @@ public partial class DashboardViewModel : ObservableObject, IDisposable private readonly IPasswordStrengthAnalyzer _strengthAnalyzer; private readonly IClipboardService _clipboard; - // Localized greeting resources (set by View code-behind) - private string _greetingMorning = "Buongiorno!"; - private string _greetingAfternoon = "Bentornato!"; - private string _greetingEvening = "Buonasera!"; - - // Localized action labels (set by View code-behind) - private string _labelCreated = "Aggiunto"; - private string _labelModified = "Modificato"; - private string _labelDeleted = "Eliminato"; - - // Localized "deleted entity" fallback labels - private string _deletedPassword = "Password eliminata"; - private string _deletedCard = "Carta eliminata"; - private string _deletedIdentity = "Identità eliminata"; - private string _deletedNote = "Nota eliminata"; + // All set by the View code-behind (SetGreetingResources/SetActionLabels/SetDeletedLabels) + // before display, so they start empty rather than carrying hardcoded Italian defaults. + private string _greetingMorning = string.Empty; + private string _greetingAfternoon = string.Empty; + private string _greetingEvening = string.Empty; + + private string _labelCreated = string.Empty; + private string _labelModified = string.Empty; + private string _labelDeleted = string.Empty; + + private string _deletedPassword = string.Empty; + private string _deletedCard = string.Empty; + private string _deletedIdentity = string.Empty; + private string _deletedNote = string.Empty; // ── Greeting ───────────────────────────────────────────────────────────── diff --git a/src/PassKey.Desktop/ViewModels/GeneratorViewModel.cs b/src/PassKey.Desktop/ViewModels/GeneratorViewModel.cs index ce83172..9ac5442 100644 --- a/src/PassKey.Desktop/ViewModels/GeneratorViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/GeneratorViewModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Core.Models; using PassKey.Core.Services; using PassKey.Desktop.Services; @@ -18,6 +19,8 @@ public partial class GeneratorViewModel : ObservableObject private readonly IClipboardService _clipboard; private readonly ISettingsService _settings; private readonly IVaultStateService _vaultState; + private readonly IToastService _toast; + private readonly ResourceLoader _resourceLoader = new(); [ObservableProperty] public partial string GeneratedPassword { get; set; } = string.Empty; @@ -54,13 +57,15 @@ public GeneratorViewModel( IPasswordStrengthAnalyzer analyzer, IClipboardService clipboard, ISettingsService settings, - IVaultStateService vaultState) + IVaultStateService vaultState, + IToastService toast) { _generator = generator; _analyzer = analyzer; _clipboard = clipboard; _settings = settings; _vaultState = vaultState; + _toast = toast; // Load persisted settings Length = _settings.PasswordGeneratorLength; @@ -116,13 +121,7 @@ private void CopyPassword() _clipboard.Copy(GeneratedPassword, CopyType.Sensitive); ShowCopiedFeedback = true; - } - - [RelayCommand] - private void GenerateAndCopy() - { - Generate(); - CopyPassword(); + _toast.Show(ToastSeverity.Info, _resourceLoader.GetString("ToastCopied")); } [RelayCommand] @@ -132,6 +131,7 @@ private void CopyHistoryEntry(HistoryEntry? entry) return; _clipboard.Copy(entry.Password, CopyType.Sensitive); + _toast.Show(ToastSeverity.Info, _resourceLoader.GetString("ToastCopied")); } /// diff --git a/src/PassKey.Desktop/ViewModels/IdentitiesListViewModel.cs b/src/PassKey.Desktop/ViewModels/IdentitiesListViewModel.cs index bd23783..22ec82f 100644 --- a/src/PassKey.Desktop/ViewModels/IdentitiesListViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/IdentitiesListViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Core.Interfaces; using PassKey.Core.Models; using PassKey.Desktop.Services; @@ -16,6 +17,8 @@ public partial class IdentitiesListViewModel : ObservableObject, IDisposable private readonly IClipboardService _clipboard; private readonly IDialogQueueService _dialogQueue; private readonly IVaultRepository _repository; + private readonly IToastService _toast; + private readonly ResourceLoader _resourceLoader = new(); private bool _disposed; private List _allEntries = []; @@ -50,12 +53,14 @@ public IdentitiesListViewModel( IClipboardService clipboard, IDialogQueueService dialogQueue, IVaultRepository repository, + IToastService toast, IdentityDetailViewModel detailViewModel) { _vaultState = vaultState; _clipboard = clipboard; _dialogQueue = dialogQueue; _repository = repository; + _toast = toast; _detailVm = detailViewModel; _vaultState.VaultLocked += OnVaultLocked; @@ -168,22 +173,6 @@ private void EditEntry(IdentityEntry? entry) IsDetailOpen = true; } - [RelayCommand] - private void CopyEmail(IdentityEntry? entry) - { - if (entry is not null && !string.IsNullOrEmpty(entry.Email)) - _clipboard.Copy(entry.Email, CopyType.Standard); - } - - [RelayCommand] - private void CopyPhone(IdentityEntry? entry) - { - if (entry is not null && !string.IsNullOrEmpty(entry.Phone)) - _clipboard.Copy(entry.Phone, CopyType.Standard); - } - - /// Raised after a successful save, for the View to show a toast. - public event Action? SaveCompleted; public void CloseDetail() { @@ -197,10 +186,10 @@ private async Task DeleteSelectedAsync() if (SelectedEntry is null) return; var confirmed = await _dialogQueue.ConfirmAsync( - title: "Elimina identità", - content: $"Eliminare \"{SelectedEntry.Label}\"?\nQuesta azione è irreversibile.", - primaryButtonText: "Elimina", - closeButtonText: "Annulla"); + title: string.Format(_resourceLoader.GetString("DeleteConfirmTitle"), SelectedEntry.Label), + content: string.Format(_resourceLoader.GetString("DeleteConfirmMessage"), SelectedEntry.Label), + primaryButtonText: _resourceLoader.GetString("DeleteButton"), + closeButtonText: _resourceLoader.GetString("CancelButton")); if (confirmed) { @@ -217,6 +206,7 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseDetail(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastDeleted")); } } @@ -232,7 +222,7 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseDetail(); - SaveCompleted?.Invoke(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastSaved")); } private async void OnEntryDeleted(Guid entryId) @@ -247,5 +237,6 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseDetail(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastDeleted")); } } diff --git a/src/PassKey.Desktop/ViewModels/IdentityDetailViewModel.cs b/src/PassKey.Desktop/ViewModels/IdentityDetailViewModel.cs index 17e9adb..0491597 100644 --- a/src/PassKey.Desktop/ViewModels/IdentityDetailViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/IdentityDetailViewModel.cs @@ -19,6 +19,9 @@ public partial class IdentityDetailViewModel : BaseDetailViewModel + /// True when an email has been entered but its format is not plausible. Drives a + /// non-blocking inline warning (FU1) — the email is optional, so this never affects + /// ; it only nudges the user to double-check. + /// + [ObservableProperty] + public partial bool IsEmailFormatSuspect { get; set; } + public IdentityDetailViewModel( IVaultStateService vaultState, IDialogQueueService dialogQueue) @@ -76,16 +98,12 @@ public IdentityDetailViewModel( // ─── Template-method overrides ──────────────────────────────────────────── - protected override string GetPanelTitleForNew() => "Aggiungi identità"; - protected override string GetPanelTitleForEdit() => "Modifica identità"; - protected override string GetDeleteDialogTitle() => "Elimina identità"; - protected override string GetDeleteDisplayName(IdentityEntry entry) { var displayName = !string.IsNullOrWhiteSpace(entry.Label) ? entry.Label : $"{entry.FirstName} {entry.LastName}".Trim(); - return string.IsNullOrWhiteSpace(displayName) ? "Identità senza nome" : displayName; + return string.IsNullOrWhiteSpace(displayName) ? _res.GetString("IdentityNoName") : displayName; } protected override IList GetVaultCollection(Vault vault) => vault.Identities; @@ -94,10 +112,13 @@ protected override void ResetFieldsForNew() { Label = string.Empty; FirstName = string.Empty; + MiddleName = string.Empty; LastName = string.Empty; BirthDate = string.Empty; Email = string.Empty; Phone = string.Empty; + Company = string.Empty; + Username = string.Empty; Street = string.Empty; City = string.Empty; Province = string.Empty; @@ -109,16 +130,21 @@ protected override void ResetFieldsForNew() DrivingLicenseNumber = string.Empty; PassportNumber = string.Empty; Notes = string.Empty; + IsFirstAndLastNameEmpty = true; + IsEmailFormatSuspect = false; } protected override void LoadFromEntry(IdentityEntry entry) { Label = entry.Label; FirstName = entry.FirstName; + MiddleName = entry.MiddleName; LastName = entry.LastName; BirthDate = entry.BirthDate; Email = entry.Email; Phone = entry.Phone; + Company = entry.Company; + Username = entry.Username; Street = entry.Street; City = entry.City; @@ -139,10 +165,13 @@ protected override void LoadFromEntry(IdentityEntry entry) { Label = Label.Trim(), FirstName = FirstName.Trim(), + MiddleName = MiddleName.Trim(), LastName = LastName.Trim(), BirthDate = BirthDate.Trim(), Email = Email.Trim(), Phone = Phone.Trim(), + Company = Company.Trim(), + Username = Username.Trim(), Street = Street.Trim(), City = City.Trim(), Province = Province.Trim(), @@ -160,10 +189,13 @@ protected override void ApplyToEntry(IdentityEntry entry) { entry.Label = Label.Trim(); entry.FirstName = FirstName.Trim(); + entry.MiddleName = MiddleName.Trim(); entry.LastName = LastName.Trim(); entry.BirthDate = BirthDate.Trim(); entry.Email = Email.Trim(); entry.Phone = Phone.Trim(); + entry.Company = Company.Trim(); + entry.Username = Username.Trim(); entry.Street = Street.Trim(); entry.City = City.Trim(); entry.Province = Province.Trim(); @@ -185,7 +217,49 @@ protected override void UpdateCanSave() // ─── Property change handlers ───────────────────────────────────────────── - partial void OnFirstNameChanged(string value) => UpdateCanSave(); - partial void OnLastNameChanged(string value) => UpdateCanSave(); - partial void OnEmailChanged(string value) => UpdateCanSave(); + partial void OnFirstNameChanged(string value) + { + UpdateValidationState(); + UpdateCanSave(); + } + + partial void OnLastNameChanged(string value) + { + UpdateValidationState(); + UpdateCanSave(); + } + + partial void OnEmailChanged(string value) + { + // Non-blocking format check (FU1): warn only when something is typed and it + // doesn't look like an email. Does NOT gate saving — email is optional. + IsEmailFormatSuspect = !string.IsNullOrWhiteSpace(value) && !IsPlausibleEmail(value); + UpdateCanSave(); + } + + private void UpdateValidationState() + { + IsFirstAndLastNameEmpty = string.IsNullOrWhiteSpace(FirstName) && string.IsNullOrWhiteSpace(LastName); + } + + /// + /// Lenient plausibility check for an email address — deliberately NOT a strict + /// RFC 5322 validator. Requires exactly one '@' (neither first nor last char) and a + /// domain part containing a dot that is neither the first nor the last character. + /// Rejects the common typos "user", "user.com", "user@host", "user@host." while + /// accepting ordinary addresses like "mario@esempio.it". + /// + private static bool IsPlausibleEmail(string email) + { + email = email.Trim(); + + int at = email.IndexOf('@'); + if (at <= 0) return false; // no '@', or '@' is the first char + if (at != email.LastIndexOf('@')) return false; // more than one '@' + if (at == email.Length - 1) return false; // nothing after '@' + + var domain = email[(at + 1)..]; + int dot = domain.IndexOf('.'); + return dot > 0 && dot < domain.Length - 1; // dot present, not first/last + } } diff --git a/src/PassKey.Desktop/ViewModels/LoginViewModel.cs b/src/PassKey.Desktop/ViewModels/LoginViewModel.cs index c96a44b..43778eb 100644 --- a/src/PassKey.Desktop/ViewModels/LoginViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/LoginViewModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using PassKey.Core.Services; using PassKey.Desktop.Services; namespace PassKey.Desktop.ViewModels; @@ -18,6 +19,8 @@ public partial class LoginViewModel : ObservableObject { private readonly IVaultStateService _vaultState; private readonly INavigationStack _navigation; + private readonly IBackupService _backupService; + private readonly IBackupFileService _backupFile; /// /// Gets or sets a value indicating whether a vault unlock operation is in progress. @@ -45,10 +48,18 @@ public partial class LoginViewModel : ObservableObject /// /// Vault state service used to attempt vault unlock. /// Navigation stack for replacing the current page with the Shell on success. - public LoginViewModel(IVaultStateService vaultState, INavigationStack navigation) + /// Backup crypto service used by the "restore backup" recovery path. + /// Backup file reader used by the "restore backup" recovery path. + public LoginViewModel( + IVaultStateService vaultState, + INavigationStack navigation, + IBackupService backupService, + IBackupFileService backupFile) { _vaultState = vaultState; _navigation = navigation; + _backupService = backupService; + _backupFile = backupFile; } /// @@ -105,4 +116,44 @@ private async Task LoginAsync(string password) IsAuthenticating = false; } } + + /// + /// Recovery path: abandons the current (inaccessible) vault and routes to first-run setup. + /// The existing vault data is overwritten only when the user completes setup — abandoning + /// the setup screen leaves the old vault untouched, so this is non-destructive until then. + /// + public void StartNewVault() => _navigation.Replace(); + + /// + /// Recovery path: restores a .pkbak backup as the new vault. The backup's own + /// password becomes the master password, so the user logs straight in afterwards. + /// + /// Full path to the selected .pkbak file. + /// The password the backup was encrypted with. + /// + /// True if the backup was decrypted and adopted as the new vault (navigation to the Shell + /// has already happened); false if the password was wrong or the file was invalid. + /// + public async Task RestoreFromBackupAsync(string path, ReadOnlyMemory backupPassword) + { + try + { + var blob = await _backupFile.ReadBackupAsync(path); + + // RestoreFromBlob runs Argon2id (CPU-bound, ~1 s) — keep it off the UI thread. + var vault = await Task.Run( + () => _backupService.RestoreFromBlob(blob, backupPassword.Span)); + + await _vaultState.InitializeWithVaultAsync(backupPassword, vault); + _navigation.Replace(); + return true; + } + catch (Exception ex) + { + // Wrong backup password (GCM tag mismatch) or corrupt/invalid file — surface a + // generic failure to the caller; details go to the debug listener only. + System.Diagnostics.Debug.WriteLine($"[LoginViewModel] Restore failed: {ex.GetType().Name}: {ex.Message}"); + return false; + } + } } diff --git a/src/PassKey.Desktop/ViewModels/PasswordDetailViewModel.cs b/src/PassKey.Desktop/ViewModels/PasswordDetailViewModel.cs index fb6027e..1cbb932 100644 --- a/src/PassKey.Desktop/ViewModels/PasswordDetailViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/PasswordDetailViewModel.cs @@ -72,6 +72,17 @@ public partial class PasswordDetailViewModel : BaseDetailViewModelTrue when is non-empty — the View flips between empty/filled states off this. public bool HasTotp => !string.IsNullOrWhiteSpace(TotpSecret); + // ── Inline validation (T5.6) ─────────────────────────────────────────────── + + [ObservableProperty] + public partial bool IsTitleEmpty { get; set; } + + [ObservableProperty] + public partial bool IsUsernameEmpty { get; set; } + + [ObservableProperty] + public partial bool IsPasswordEmpty { get; set; } + public PasswordDetailViewModel( IVaultStateService vaultState, IPasswordGenerator generator, @@ -87,9 +98,6 @@ public PasswordDetailViewModel( // ─── Template-method overrides ──────────────────────────────────────────── - protected override string GetPanelTitleForNew() => "Aggiungi password"; - protected override string GetPanelTitleForEdit() => "Modifica password"; - protected override string GetDeleteDialogTitle() => "Elimina password"; protected override string GetDeleteDisplayName(PasswordEntry entry) => entry.Title; protected override IList GetVaultCollection(Vault vault) => vault.Passwords; @@ -108,6 +116,9 @@ protected override void ResetFieldsForNew() TotpPeriod = 30; CurrentTotpCode = string.Empty; TotpRemainingSeconds = 0; + IsTitleEmpty = true; + IsUsernameEmpty = true; + IsPasswordEmpty = true; } protected override void LoadFromEntry(PasswordEntry entry) @@ -162,10 +173,21 @@ protected override void UpdateCanSave() // ─── Property change handlers ───────────────────────────────────────────── - partial void OnTitleChanged(string value) => UpdateCanSave(); - partial void OnUsernameChanged(string value) => UpdateCanSave(); + partial void OnTitleChanged(string value) + { + IsTitleEmpty = string.IsNullOrWhiteSpace(value); + UpdateCanSave(); + } + + partial void OnUsernameChanged(string value) + { + IsUsernameEmpty = string.IsNullOrWhiteSpace(value); + UpdateCanSave(); + } + partial void OnPasswordChanged(string value) { + IsPasswordEmpty = string.IsNullOrWhiteSpace(value); UpdateCanSave(); UpdatePasswordStrength(); } diff --git a/src/PassKey.Desktop/ViewModels/PasswordVerifierViewModel.cs b/src/PassKey.Desktop/ViewModels/PasswordVerifierViewModel.cs index 5e8bb40..fc91b21 100644 --- a/src/PassKey.Desktop/ViewModels/PasswordVerifierViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/PasswordVerifierViewModel.cs @@ -62,6 +62,14 @@ public partial class PasswordVerifierViewModel : ObservableObject, IDisposable [ObservableProperty] public partial double AuditProgress { get; set; } + /// Number of passwords checked so far in the running scan (live count). + [ObservableProperty] + public partial int ScannedCount { get; set; } + + /// Total number of passwords the running scan will check. + [ObservableProperty] + public partial int TotalToScan { get; set; } + [ObservableProperty] public partial bool HasAuditResults { get; set; } @@ -165,7 +173,12 @@ public void Initialize() // ─── Scan service event handlers ────────────────────────────────────────── - private void OnScanProgress(double pct) => AuditProgress = pct; + private void OnScanProgress(int scanned, int total) + { + ScannedCount = scanned; + TotalToScan = total; + AuditProgress = total > 0 ? (double)scanned / total * 100 : 0; + } private void OnScanCompleted(WatchtowerResult? result) { diff --git a/src/PassKey.Desktop/ViewModels/PasswordsListViewModel.cs b/src/PassKey.Desktop/ViewModels/PasswordsListViewModel.cs index 31b2009..7bb2190 100644 --- a/src/PassKey.Desktop/ViewModels/PasswordsListViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/PasswordsListViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Core.Interfaces; using PassKey.Core.Models; using PassKey.Desktop.Services; @@ -16,6 +17,8 @@ public partial class PasswordsListViewModel : ObservableObject, IDisposable private readonly IClipboardService _clipboard; private readonly IDialogQueueService _dialogQueue; private readonly IVaultRepository _repository; + private readonly IToastService _toast; + private readonly ResourceLoader _resourceLoader = new(); private bool _disposed; private List _allEntries = []; @@ -52,12 +55,14 @@ public PasswordsListViewModel( IClipboardService clipboard, IDialogQueueService dialogQueue, IVaultRepository repository, + IToastService toast, PasswordDetailViewModel detailViewModel) { _vaultState = vaultState; _clipboard = clipboard; _dialogQueue = dialogQueue; _repository = repository; + _toast = toast; _detailVm = detailViewModel; // Hygiene: clear in-memory entries when the vault is locked so we never @@ -168,19 +173,22 @@ private void EditEntry(PasswordEntry? entry) private void CopyUsername(PasswordEntry? entry) { if (entry is not null && !string.IsNullOrEmpty(entry.Username)) + { _clipboard.Copy(entry.Username, CopyType.Standard); + _toast.Show(ToastSeverity.Info, _resourceLoader.GetString("ToastUsernameCopied")); + } } [RelayCommand] private void CopyPassword(PasswordEntry? entry) { if (entry is not null && !string.IsNullOrEmpty(entry.Password)) + { _clipboard.Copy(entry.Password, CopyType.Sensitive); + _toast.Show(ToastSeverity.Info, _resourceLoader.GetString("ToastPasswordCopied")); + } } - /// Raised after a successful save, for the View to show a toast. - public event Action? SaveCompleted; - public void CloseDetail() { IsDetailOpen = false; @@ -188,21 +196,23 @@ public void CloseDetail() } [RelayCommand] - private async Task DeleteSelectedAsync() + private async Task DeleteSelectedAsync(PasswordEntry? entry) { - if (SelectedEntry is null) return; + // entry != null → quick-delete from a list row; entry == null → keyboard Delete on the selection. + var target = entry ?? SelectedEntry; + if (target is null) return; var confirmed = await _dialogQueue.ConfirmAsync( - title: "Elimina password", - content: $"Eliminare \"{SelectedEntry.Title}\"?\nQuesta azione è irreversibile.", - primaryButtonText: "Elimina", - closeButtonText: "Annulla"); + title: string.Format(_resourceLoader.GetString("DeleteConfirmTitle"), target.Title), + content: string.Format(_resourceLoader.GetString("DeleteConfirmMessage"), target.Title), + primaryButtonText: _resourceLoader.GetString("DeleteButton"), + closeButtonText: _resourceLoader.GetString("CancelButton")); if (confirmed) { var vault = _vaultState.CurrentVault; - var entryId = SelectedEntry.Id; - vault?.Passwords.Remove(SelectedEntry); + var entryId = target.Id; + vault?.Passwords.Remove(target); await _vaultState.SaveVaultAsync(); await _repository.LogActivityAsync(new ActivityLogEntry { @@ -213,6 +223,7 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseDetail(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastDeleted")); } } @@ -228,7 +239,7 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseDetail(); - SaveCompleted?.Invoke(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastSaved")); } private async void OnEntryDeleted(Guid entryId) @@ -243,5 +254,6 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseDetail(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastDeleted")); } } diff --git a/src/PassKey.Desktop/ViewModels/SecureNoteDetailViewModel.cs b/src/PassKey.Desktop/ViewModels/SecureNoteDetailViewModel.cs index ab5c582..ab73247 100644 --- a/src/PassKey.Desktop/ViewModels/SecureNoteDetailViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/SecureNoteDetailViewModel.cs @@ -50,6 +50,11 @@ public partial class SecureNoteDetailViewModel : BaseDetailViewModelRaised when is toggled (instant-save, no Save button needed). public Action? PinToggled { get; set; } @@ -62,11 +67,8 @@ public SecureNoteDetailViewModel( // ─── Template-method overrides ──────────────────────────────────────────── - protected override string GetPanelTitleForNew() => "Nuova nota"; - protected override string GetPanelTitleForEdit() => "Modifica nota"; - protected override string GetDeleteDialogTitle() => "Elimina nota"; protected override string GetDeleteDisplayName(SecureNoteEntry entry) - => !string.IsNullOrWhiteSpace(entry.Title) ? entry.Title : "Nota senza titolo"; + => !string.IsNullOrWhiteSpace(entry.Title) ? entry.Title : _res.GetString("NoteNoTitle"); protected override IList GetVaultCollection(Vault vault) => vault.SecureNotes; @@ -85,6 +87,7 @@ protected override void ResetFieldsForNew() _originalContent = string.Empty; _originalCategory = NoteCategory.General; _originalIsPinned = false; + IsTitleEmpty = true; } protected override void LoadFromEntry(SecureNoteEntry entry) @@ -132,7 +135,6 @@ protected override void OnSavedNew(SecureNoteEntry entry) EditingEntry = entry; SetIsNew(false); IsEditMode = true; - PanelTitle = GetPanelTitleForEdit(); UpdateSnapshotFromCurrent(); } @@ -146,6 +148,7 @@ protected override void OnSavedEdit(SecureNoteEntry entry) partial void OnTitleChanged(string value) { + IsTitleEmpty = string.IsNullOrWhiteSpace(value); UpdateCanSave(); UpdateHasUnsavedChanges(); } diff --git a/src/PassKey.Desktop/ViewModels/SecureNotesListViewModel.cs b/src/PassKey.Desktop/ViewModels/SecureNotesListViewModel.cs index 8de229d..e83c35e 100644 --- a/src/PassKey.Desktop/ViewModels/SecureNotesListViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/SecureNotesListViewModel.cs @@ -2,6 +2,7 @@ using System.Globalization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Core.Constants; using PassKey.Core.Interfaces; using PassKey.Core.Models; @@ -19,6 +20,10 @@ public partial class SecureNotesListViewModel : ObservableObject, IDisposable private readonly IVaultStateService _vaultState; private readonly IDialogQueueService _dialogQueue; private readonly IVaultRepository _repository; + private readonly IToastService _toast; + private readonly ResourceLoader _resourceLoader = new(); + // Static loader for the static GetCategoryName/GetRelativeDate helpers. + private static readonly ResourceLoader s_res = new(); private bool _disposed; private List _allEntries = []; @@ -46,20 +51,19 @@ public partial class SecureNotesListViewModel : ObservableObject, IDisposable [ObservableProperty] public partial SecureNoteDetailViewModel? DetailViewModel { get; set; } - /// Fired after a note is saved successfully (for toast notification). - public event Action? SaveCompleted; - private readonly SecureNoteDetailViewModel _detailVm; public SecureNotesListViewModel( IVaultStateService vaultState, IDialogQueueService dialogQueue, IVaultRepository repository, + IToastService toast, SecureNoteDetailViewModel detailViewModel) { _vaultState = vaultState; _dialogQueue = dialogQueue; _repository = repository; + _toast = toast; _detailVm = detailViewModel; _vaultState.VaultLocked += OnVaultLocked; @@ -185,10 +189,10 @@ private async Task DeleteSelectedAsync() if (SelectedEntry is null) return; var confirmed = await _dialogQueue.ConfirmAsync( - title: "Elimina nota", - content: $"Eliminare \"{SelectedEntry.Title}\"?\nQuesta azione è irreversibile.", - primaryButtonText: "Elimina", - closeButtonText: "Annulla"); + title: string.Format(_resourceLoader.GetString("DeleteConfirmTitle"), SelectedEntry.Title), + content: string.Format(_resourceLoader.GetString("DeleteConfirmMessage"), SelectedEntry.Title), + primaryButtonText: _resourceLoader.GetString("DeleteButton"), + closeButtonText: _resourceLoader.GetString("CancelButton")); if (confirmed) { @@ -205,6 +209,7 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseEditor(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastDeleted")); } } @@ -229,7 +234,7 @@ await _repository.LogActivityAsync(new ActivityLogEntry Timestamp = DateTime.UtcNow }); await LoadEntriesCommand.ExecuteAsync(null); - SaveCompleted?.Invoke(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastSaved")); } private async void OnEntryDeleted(Guid entryId) @@ -244,6 +249,7 @@ await _repository.LogActivityAsync(new ActivityLogEntry }); await LoadEntriesCommand.ExecuteAsync(null); CloseEditor(); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastDeleted")); } // --- Static helpers --- @@ -276,35 +282,36 @@ public static string GetCategoryName(NoteCategory category) { return category switch { - NoteCategory.General => "Generale", - NoteCategory.Personal => "Personale", - NoteCategory.Work => "Lavoro", - NoteCategory.Financial => "Finanziario", - NoteCategory.Medical => "Medico", - NoteCategory.Travel => "Viaggio", - NoteCategory.Education => "Educazione", - NoteCategory.Legal => "Legale", - NoteCategory.Technical => "Tecnico", - NoteCategory.Other => "Altro", - _ => "Generale" + NoteCategory.General => s_res.GetString("NoteCategoryGeneral"), + NoteCategory.Personal => s_res.GetString("NoteCategoryPersonal"), + NoteCategory.Work => s_res.GetString("NoteCategoryWork"), + NoteCategory.Financial => s_res.GetString("NoteCategoryFinancial"), + NoteCategory.Medical => s_res.GetString("NoteCategoryMedical"), + NoteCategory.Travel => s_res.GetString("NoteCategoryTravel"), + NoteCategory.Education => s_res.GetString("NoteCategoryEducation"), + NoteCategory.Legal => s_res.GetString("NoteCategoryLegal"), + NoteCategory.Technical => s_res.GetString("NoteCategoryTechnical"), + NoteCategory.Other => s_res.GetString("NoteCategoryOther"), + _ => s_res.GetString("NoteCategoryGeneral") }; } /// - /// Get Italian relative date string for display in note cards. + /// Get the localized relative date string for display in note cards. /// public static string GetRelativeDate(DateTime utcDate) { var local = utcDate.ToLocalTime(); var now = DateTime.Now; var diff = now - local; - - if (diff.TotalMinutes < 1) return "Adesso"; - if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes} min fa"; - if (diff.TotalHours < 24 && local.Date == now.Date) return $"{(int)diff.TotalHours} ore fa"; - if (local.Date == now.Date.AddDays(-1)) return "Ieri"; - if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}g fa"; - if (local.Year == now.Year) return local.ToString("d MMM", new CultureInfo("it-IT")); - return local.ToString("d MMM yyyy", new CultureInfo("it-IT")); + var culture = new CultureInfo(s_res.GetString("NoteDateCulture")); + + if (diff.TotalMinutes < 1) return s_res.GetString("NoteTimeNow"); + if (diff.TotalMinutes < 60) return string.Format(s_res.GetString("NoteTimeMinutes"), (int)diff.TotalMinutes); + if (diff.TotalHours < 24 && local.Date == now.Date) return string.Format(s_res.GetString("NoteTimeHours"), (int)diff.TotalHours); + if (local.Date == now.Date.AddDays(-1)) return s_res.GetString("NoteTimeYesterday"); + if (diff.TotalDays < 7) return string.Format(s_res.GetString("NoteTimeDays"), (int)diff.TotalDays); + if (local.Year == now.Year) return local.ToString("d MMM", culture); + return local.ToString("d MMM yyyy", culture); } } diff --git a/src/PassKey.Desktop/ViewModels/SettingsViewModel.cs b/src/PassKey.Desktop/ViewModels/SettingsViewModel.cs index 492ef09..47d02c7 100644 --- a/src/PassKey.Desktop/ViewModels/SettingsViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/SettingsViewModel.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input; using Microsoft.Win32; using Microsoft.UI.Xaml; +using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Core.Services; using PassKey.Desktop.Services; @@ -29,6 +30,8 @@ public partial class SettingsViewModel : ObservableObject private readonly IMergeService _merge; private readonly IImportOrchestrator _importOrchestrator; private readonly IUpdateService _updateService; + private readonly IToastService _toast; + private readonly ResourceLoader _resourceLoader = new(); private bool _initializing; private static readonly int[] AutoLockValues = [30, 60, 300, 600, 0]; @@ -88,7 +91,8 @@ public SettingsViewModel( IFilePickerService filePicker, IMergeService merge, IImportOrchestrator importOrchestrator, - IUpdateService updateService) + IUpdateService updateService, + IToastService toast) { _settings = settings; _vaultState = vaultState; @@ -98,6 +102,7 @@ public SettingsViewModel( _merge = merge; _importOrchestrator = importOrchestrator; _updateService = updateService; + _toast = toast; } public void Initialize() @@ -218,6 +223,10 @@ partial void OnSelectedAutoLockIndexChanged(int value) { _settings.AutoLockSeconds = AutoLockValues[value]; _settings.Save(); + + // Explicit confirmation that the change registered — the auto-lock effect + // itself is otherwise invisible until the timeout elapses. + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("ToastAutoLockUpdated")); } } @@ -280,6 +289,38 @@ await _vaultState.ChangeMasterPasswordAsync( } } + /// + /// Verifies the master password and, if correct, removes every entry (passwords, cards, + /// identities, secure notes) from the vault while preserving its metadata and master + /// password. Irreversible. Returns if the password is wrong. + /// + /// The master password re-entered by the user for confirmation. + /// on success; if verification failed. + public async Task PerformClearVaultAsync(string password) + { + var chars = password.ToCharArray(); + try + { + var verified = await Task.Run(async () => + await _vaultState.VerifyMasterPasswordAsync(new ReadOnlyMemory(chars))); + if (!verified) return false; + + var vault = _vaultState.CurrentVault; + if (vault is null) return false; + + vault.Passwords.Clear(); + vault.CreditCards.Clear(); + vault.Identities.Clear(); + vault.SecureNotes.Clear(); + await _vaultState.SaveVaultAsync(); + return true; + } + finally + { + Array.Clear(chars); + } + } + [RelayCommand] public async Task BackupVaultAsync() { @@ -292,8 +333,10 @@ public async Task BackupVaultAsync() var (password, confirmed) = await BackupPasswordRequested.Invoke(); if (!confirmed) return; - // 2. Pick save location - var path = await _filePicker.PickSaveFileAsync("PassKey_Backup", ".pkbak", "PassKey Backup"); + // 2. Pick save location — suggest a date/time-stamped name so multiple backups + // sort chronologically and never silently overwrite one another. + var suggestedName = $"PassKey_Backup_{DateTime.Now:yyyyMMdd_HHmm}"; + var path = await _filePicker.PickSaveFileAsync(suggestedName, ".pkbak", "PassKey Backup"); if (path is null) return; // 3. Create encrypted backup blob (KDF is slow → Task.Run) @@ -316,8 +359,9 @@ public async Task BackupVaultAsync() } catch (Exception ex) { + System.Diagnostics.Debug.WriteLine($"[Settings] Backup failed: {ex}"); if (OperationError is not null) - await OperationError.Invoke(ex.Message); + await OperationError.Invoke("OPERATION_FAILED"); } } @@ -381,8 +425,9 @@ public async Task RestoreVaultAsync() } catch (Exception ex) { + System.Diagnostics.Debug.WriteLine($"[Settings] Restore failed: {ex}"); if (OperationError is not null) - await OperationError.Invoke(ex.Message); + await OperationError.Invoke("OPERATION_FAILED"); } } @@ -398,17 +443,7 @@ public async Task ImportDataAsync() var (format, formatConfirmed) = await ImportFormatRequested.Invoke(); if (!formatConfirmed) return; - // 2. For KDBX, ask for password - string? importPassword = null; - if (format == ImportFormat.Kdbx) - { - if (ImportPasswordRequested is null) return; - var (pw, pwConfirmed) = await ImportPasswordRequested.Invoke(format); - if (!pwConfirmed) return; - importPassword = pw; - } - - // 3. Pick file + // 2. Pick file var extension = format switch { ImportFormat.Csv => ".csv", @@ -425,9 +460,32 @@ public async Task ImportDataAsync() ImportFormat.Bitwarden => "Bitwarden JSON", _ => "All Files" }; - var path = await _filePicker.PickOpenFileAsync(extension, description); + // Multi-extension filters (FU3): + // • Bitwarden exports as plain .json or a .zip with attachments — accept both + // so the ZIP can be unwrapped on import. + // • KeePass: also allow .kdb so a legacy KeePass 1.x file is selectable and the + // importer can show the dedicated "convert to .kdbx" message instead of the + // file being invisible in the picker. + var path = format switch + { + ImportFormat.Bitwarden => await _filePicker.PickOpenFileAsync(new[] { ".json", ".zip" }, _resourceLoader.GetString("ImportPickerBitwarden")), + ImportFormat.Kdbx => await _filePicker.PickOpenFileAsync(new[] { ".kdbx", ".kdb" }, _resourceLoader.GetString("ImportPickerKeepass")), + _ => await _filePicker.PickOpenFileAsync(extension, description) + }; if (path is null) return; + // 3. For KDBX, ask for the database password — AFTER the file is chosen, so the + // prompt refers to a file the user has actually selected (FU3 UX), and a + // cancelled picker doesn't waste a password entry. + string? importPassword = null; + if (format == ImportFormat.Kdbx) + { + if (ImportPasswordRequested is null) return; + var (pw, pwConfirmed) = await ImportPasswordRequested.Invoke(format); + if (!pwConfirmed) return; + importPassword = pw; + } + // 4. Parse file var importedVault = await _importOrchestrator.ParseFileAsync(path, format, importPassword); @@ -443,7 +501,7 @@ public async Task ImportDataAsync() if (totalImported == 0) { if (OperationError is not null) - await OperationError.Invoke("Il file selezionato non contiene alcuna voce riconoscibile. Verifica che l'esportazione di origine non sia vuota e che il formato sia supportato."); + await OperationError.Invoke("IMPORT_NO_ENTRIES"); return; } @@ -466,10 +524,17 @@ public async Task ImportDataAsync() if (ImportCompleted is not null) await ImportCompleted.Invoke(result); } + catch (ImportFileException ifx) + { + // Carries an error CODE (e.g. IMPORT_BW_ZIP) mapped to a localized message by the View. + if (OperationError is not null) + await OperationError.Invoke(ifx.Message); + } catch (Exception ex) { + System.Diagnostics.Debug.WriteLine($"[Settings] Import failed: {ex}"); if (OperationError is not null) - await OperationError.Invoke(ex.Message); + await OperationError.Invoke("OPERATION_FAILED"); } } diff --git a/src/PassKey.Desktop/ViewModels/ShellViewModel.cs b/src/PassKey.Desktop/ViewModels/ShellViewModel.cs index 35c3439..5b39bcc 100644 --- a/src/PassKey.Desktop/ViewModels/ShellViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/ShellViewModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Desktop.Services; namespace PassKey.Desktop.ViewModels; @@ -13,6 +14,7 @@ public partial class ShellViewModel : ObservableObject, IDisposable { private readonly IVaultStateService _vaultState; private readonly INavigationStack _navigation; + private readonly ResourceLoader _resourceLoader = new(); private readonly IUpdateService _updateService; private readonly ISettingsService _settings; private readonly DashboardViewModel _dashboardViewModel; @@ -24,6 +26,7 @@ public partial class ShellViewModel : ObservableObject, IDisposable private readonly PasswordVerifierViewModel _verifierViewModel; private readonly SettingsViewModel _settingsViewModel; private readonly HelpViewModel _helpViewModel; + private readonly ActivityLogViewModel _activityLogViewModel; private UpdateCheckResult? _currentUpdate; @@ -76,7 +79,8 @@ public ShellViewModel( GeneratorViewModel generatorViewModel, PasswordVerifierViewModel verifierViewModel, SettingsViewModel settingsViewModel, - HelpViewModel helpViewModel) + HelpViewModel helpViewModel, + ActivityLogViewModel activityLogViewModel) { _vaultState = vaultState; _navigation = navigation; @@ -91,6 +95,7 @@ public ShellViewModel( _verifierViewModel = verifierViewModel; _settingsViewModel = settingsViewModel; _helpViewModel = helpViewModel; + _activityLogViewModel = activityLogViewModel; // Handle race: background check may have completed before this VM was constructed if (_updateService.PendingUpdate is { UpdateAvailable: true } pending) @@ -151,6 +156,12 @@ public void NavigateToHelp() CurrentPage = _helpViewModel; } + /// Navigates to the activity-log ("Cronologia") page (outside indexed sidebar items). + public void NavigateToActivityLog() + { + CurrentPage = _activityLogViewModel; + } + // ── Vault lock ──────────────────────────────────────────────────────────── /// Locks the vault. MainViewModel detects the event and navigates to LoginViewModel. @@ -171,7 +182,7 @@ private void OnUpdateDetected(UpdateCheckResult result) private void ShowUpdateInfoBar(UpdateCheckResult result) { _currentUpdate = result; - UpdateInfoBarTitle = $"PassKey {result.NewVersion} disponibile"; + UpdateInfoBarTitle = string.Format(_resourceLoader.GetString("UpdateAvailableTitle"), result.NewVersion); IsUpdateInfoBarOpen = true; IsDownloading = false; DownloadProgress = 0; diff --git a/src/PassKey.Desktop/ViewModels/WelcomeViewModel.cs b/src/PassKey.Desktop/ViewModels/WelcomeViewModel.cs index bcae930..a469ddd 100644 --- a/src/PassKey.Desktop/ViewModels/WelcomeViewModel.cs +++ b/src/PassKey.Desktop/ViewModels/WelcomeViewModel.cs @@ -1,71 +1,145 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using PassKey.Core.Services; using PassKey.Desktop.Services; namespace PassKey.Desktop.ViewModels; /// /// ViewModel for the Welcome view shown immediately after first-run vault creation. -/// Offers four quick-start actions to the new user before entering the main shell. +/// Offers quick-start actions to the new user before entering the main shell, including +/// restoring an existing .pkbak backup into the freshly-created vault. /// /// -/// Dependency injected via constructor: . -/// All four commands currently navigate to ; deeper routing -/// (e.g., directly to the password-add form) is planned for a later phase. +/// The three "quick action" commands (add password / import / settings) currently just enter +/// the shell; deeper routing is planned for a later phase. +/// is fully functional: it picks a backup file, decrypts it, and replaces the vault content. /// public partial class WelcomeViewModel : ObservableObject { private readonly INavigationStack _navigation; + private readonly IFilePickerService _filePicker; + private readonly IBackupService _backup; + private readonly IBackupFileService _backupFile; + private readonly IVaultStateService _vaultState; - /// - /// Initializes a new instance of . - /// - /// Navigation stack used to replace the current page with the Shell. - public WelcomeViewModel(INavigationStack navigation) + // Restore dialogs are delegated to the code-behind via these callbacks. + public event Func>? RestoreWarningRequested; + public event Func>? RestorePasswordRequested; + public event Func? RestoreCompleted; + public event Func? OperationError; + + public WelcomeViewModel( + INavigationStack navigation, + IFilePickerService filePicker, + IBackupService backup, + IBackupFileService backupFile, + IVaultStateService vaultState) { _navigation = navigation; + _filePicker = filePicker; + _backup = backup; + _backupFile = backupFile; + _vaultState = vaultState; } - /// - /// Navigates to the Shell. Intended to open the password-add form directly (planned for a future phase). - /// + /// Navigates to the Shell (direct password-page routing planned for a future phase). [RelayCommand] private Task AddPasswordAsync() { - // Navigate to Shell (Dashboard for now; Phase 5 will add direct password page nav) _navigation.Replace(); return Task.CompletedTask; } - /// - /// Navigates to the Shell. Intended to open the Settings import page directly (planned for a future phase). - /// + /// Navigates to the Shell (direct Settings → Import routing planned for a future phase). [RelayCommand] private Task ImportDataAsync() { - // Phase 11: Navigate to Shell → Settings → Import _navigation.Replace(); return Task.CompletedTask; } - /// - /// Navigates to the Shell. Intended to open the Settings page directly (planned for a future phase). - /// + /// Navigates to the Shell (direct Settings routing planned for a future phase). [RelayCommand] private Task OpenSettingsAsync() { - // Phase 11: Navigate to Shell → Settings _navigation.Replace(); return Task.CompletedTask; } - /// - /// Navigates to the Shell and begins the normal authenticated session. - /// + /// Navigates to the Shell and begins the normal authenticated session. [RelayCommand] private Task ContinueAsync() { _navigation.Replace(); return Task.CompletedTask; } + + /// + /// Restores an existing .pkbak backup into the freshly-created vault, then enters + /// the shell. The vault's master password (chosen during setup) is preserved — only the + /// vault content is replaced. The current (empty) vault is auto-backed-up first. + /// + [RelayCommand] + private async Task RestoreBackupAsync() + { + if (!_vaultState.IsUnlocked) return; + + try + { + // 1. Warning dialog. + if (RestoreWarningRequested is null) return; + var warningConfirmed = await RestoreWarningRequested.Invoke(); + if (!warningConfirmed) return; + + // 2. Pick the backup file. + var path = await _filePicker.PickOpenFileAsync(".pkbak", "PassKey Backup"); + if (path is null) return; + + // 3. Read the backup blob. + var blob = await _backupFile.ReadBackupAsync(path); + + // 4. Ask for the backup password. + if (RestorePasswordRequested is null) return; + var (password, confirmed) = await RestorePasswordRequested.Invoke(); + if (!confirmed) return; + + // 5. Auto-backup the current vault before replacing it. + var currentBlob = await _vaultState.GetEncryptedBlobAsync(); + if (currentBlob is not null) + await _backupFile.WriteAutoBackupAsync(currentBlob); + + // 6. Decrypt the backup (KDF is slow → off the UI thread). + var passwordChars = password.ToCharArray(); + Core.Models.Vault restoredVault; + try + { + restoredVault = await Task.Run(() => _backup.RestoreFromBlob(blob, passwordChars)); + } + finally + { + Array.Clear(passwordChars); + } + + // 7. Replace the vault content and enter the shell. + await _vaultState.RestoreVaultAsync(restoredVault); + + if (RestoreCompleted is not null) + await RestoreCompleted.Invoke(); + + _navigation.Replace(); + } + catch (System.Security.Cryptography.CryptographicException) + { + if (OperationError is not null) await OperationError.Invoke("WRONG_PASSWORD"); + } + catch (System.IO.InvalidDataException) + { + if (OperationError is not null) await OperationError.Invoke("INVALID_FILE"); + } + catch (Exception ex) + { + if (OperationError is not null) await OperationError.Invoke(ex.Message); + } + } } diff --git a/src/PassKey.Desktop/Views/ActivityLogView.xaml b/src/PassKey.Desktop/Views/ActivityLogView.xaml new file mode 100644 index 0000000..bc1540e --- /dev/null +++ b/src/PassKey.Desktop/Views/ActivityLogView.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/PassKey.Desktop/Views/ActivityLogView.xaml.cs b/src/PassKey.Desktop/Views/ActivityLogView.xaml.cs new file mode 100644 index 0000000..ca9bf80 --- /dev/null +++ b/src/PassKey.Desktop/Views/ActivityLogView.xaml.cs @@ -0,0 +1,60 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.ApplicationModel.Resources; +using PassKey.Desktop.ViewModels; + +namespace PassKey.Desktop.Views; + +/// +/// Activity-log ("Cronologia") viewer: lists recent vault activity and offers CSV export. +/// +public sealed partial class ActivityLogView : UserControl +{ + private ActivityLogViewModel? _viewModel; + private readonly ResourceLoader _resourceLoader = new(); + + /// Raised when the user clicks the back button to return to the Settings page. + public event Action? BackRequested; + + public ActivityLogView() + { + InitializeComponent(); + EmptyState.Title = _resourceLoader.GetString("ActivityEmptyTitle"); + EmptyState.Subtitle = _resourceLoader.GetString("ActivityEmptySubtitle"); + } + + public async void SetViewModel(ActivityLogViewModel vm) + { + _viewModel = vm; + DataContext = vm; + EntriesList.ItemsSource = vm.Entries; + + await vm.LoadAsync(); + UpdateEmptyState(); + } + + private void UpdateEmptyState() + { + var empty = _viewModel?.IsEmpty ?? true; + EmptyState.Visibility = empty ? Visibility.Visible : Visibility.Collapsed; + EntriesList.Visibility = empty ? Visibility.Collapsed : Visibility.Visible; + ExportButton.IsEnabled = !empty; + } + + private void BackButton_Click(object sender, RoutedEventArgs e) + => BackRequested?.Invoke(); + + private async void ExportButton_Click(object sender, RoutedEventArgs e) + { + if (_viewModel is null) return; + ExportButton.IsEnabled = false; + try + { + await _viewModel.ExportCsvCommand.ExecuteAsync(null); + } + finally + { + ExportButton.IsEnabled = _viewModel.Entries.Count > 0; + } + } +} diff --git a/src/PassKey.Desktop/Views/CreditCardDetailView.xaml b/src/PassKey.Desktop/Views/CreditCardDetailView.xaml index 15dc44f..82f6a8f 100644 --- a/src/PassKey.Desktop/Views/CreditCardDetailView.xaml +++ b/src/PassKey.Desktop/Views/CreditCardDetailView.xaml @@ -30,7 +30,6 @@ x:Uid="LabelBox" PlaceholderText="E.g. Main card, Shopping..." TextChanged="LabelBox_TextChanged" - AutomationProperties.Name="Card label" TabIndex="10" /> @@ -47,7 +46,6 @@ x:Uid="CategoryCombo" HorizontalAlignment="Stretch" SelectionChanged="CategoryCombo_SelectionChanged" - AutomationProperties.Name="Card category" TabIndex="20"> @@ -59,8 +57,8 @@ @@ -79,8 +77,12 @@ InputScope="NumericPin" MaxLength="23" TextChanged="CardNumberBox_TextChanged" - AutomationProperties.Name="Card number" TabIndex="30" /> + + @@ -122,7 +128,6 @@ PlaceholderText="Month" HorizontalAlignment="Stretch" SelectionChanged="MonthCombo_SelectionChanged" - AutomationProperties.Name="Expiry month" TabIndex="50" /> @@ -148,11 +152,17 @@ PlaceholderText="123" MaxLength="4" TabIndex="60" /> + @@ -171,7 +181,6 @@ ScrollViewer.VerticalScrollBarVisibility="Auto" PlaceholderText="Additional notes..." TextChanged="NotesBox_TextChanged" - AutomationProperties.Name="Notes" TabIndex="80" /> @@ -200,7 +209,6 @@ Visibility="Collapsed" Click="DeleteButton_Click" x:Uid="CardDeleteButton" - AutomationProperties.Name="Delete card" TabIndex="100"> @@ -213,7 +221,6 @@ x:Uid="ButtonCancel" Content="Cancel" Click="CancelButton_Click" - AutomationProperties.Name="Cancel" TabIndex="110" /> @@ -222,7 +229,6 @@ Style="{StaticResource AccentButtonStyle}" IsEnabled="False" Click="SaveButton_Click" - AutomationProperties.Name="Save" TabIndex="120"> + TextChanged="SearchBox_TextChanged" /> + + + @@ -180,6 +190,7 @@ Grid.Row="2" Visibility="Collapsed" Icon="" + AccentBrush="{ThemeResource CardsIconBrush}" Title="Nessuna carta salvata" Subtitle="Aggiungi la tua prima carta di credito per averla sempre al sicuro." /> @@ -193,12 +204,5 @@ BorderThickness="1,0,0,0"> - - - diff --git a/src/PassKey.Desktop/Views/CreditCardsListView.xaml.cs b/src/PassKey.Desktop/Views/CreditCardsListView.xaml.cs index cecd707..6f6fac0 100644 --- a/src/PassKey.Desktop/Views/CreditCardsListView.xaml.cs +++ b/src/PassKey.Desktop/Views/CreditCardsListView.xaml.cs @@ -1,12 +1,13 @@ +using System; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media.Animation; using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Core.Constants; using PassKey.Core.Models; using PassKey.Core.Services; using PassKey.Desktop.Controls; -using PassKey.Desktop.Helpers; using PassKey.Desktop.ViewModels; namespace PassKey.Desktop.Views; @@ -26,6 +27,10 @@ public CreditCardsListView() public async void SetViewModel(CreditCardsListViewModel vm) { + // Drop any handler attached to a previous VM to avoid subscription leaks if + // SetViewModel is ever called twice on the same view instance. + if (_viewModel is not null) _viewModel.PropertyChanged -= OnViewModelPropertyChanged; + _viewModel = vm; DataContext = vm; @@ -33,13 +38,22 @@ public async void SetViewModel(CreditCardsListViewModel vm) EmptyState.Title = _resourceLoader.GetString("EmptyCardsTitle"); EmptyState.Subtitle = _resourceLoader.GetString("EmptyCardsSubtitle"); vm.PropertyChanged += OnViewModelPropertyChanged; - vm.SaveCompleted += ShowSavedToast; await vm.LoadEntriesCommand.ExecuteAsync(null); UpdateCardRepeater(); UpdateListView(); UpdateEmptyState(); UpdateViewToggle(); + + // Sync the detail panel from VM state. ShellView recreates this view on every + // navigation, but the CreditCardsListViewModel persists — so a detail panel + // left open before navigating away survives in the VM. Without an explicit + // sync the new view shows no detail panel, and subsequent Edit clicks become + // silent no-ops (IsDetailOpen is already true → PropertyChanged doesn't fire + // → UpdateDetailPanel/UpdateDetailContent are never called) — the UI looks + // frozen. Calling them once here restores the user's in-progress edit. + UpdateDetailPanel(); + UpdateDetailContent(); } private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -153,9 +167,68 @@ private void CardRepeater_ElementPrepared(ItemsRepeater sender, ItemsRepeaterEle // Prevent duplicate handlers on element recycling cardControl.Tapped -= CardControl_Tapped; cardControl.Tapped += CardControl_Tapped; + + // Fade-in: the ItemsRepeater has no native entrance transition (unlike the + // ListView, which gets one for free). Animate the prepared element so the + // card view matches the list view's appearance behaviour. + AnimateFadeIn(cardControl); + } + } + + /// + /// Runs a short opacity fade-in on a freshly prepared card element so the card view + /// gains the same entrance feel the ListView provides natively. + /// + /// + /// Both an XAML Storyboard and a Composition-layer animation failed when started + /// directly inside ElementPrepared — the element is not yet attached to the + /// live visual tree at that point, so the animation silently no-ops and the user + /// sees the cards appear instantly at full opacity. + /// The reliable pattern: + /// + /// Set Opacity = 0 immediately so the first paint is invisible. + /// Defer the actual animation start to the Loaded event (or fire it + /// right away if the element is recycled and already loaded). + /// + /// At Loaded time the element is in the tree and a Storyboard targeting its + /// Opacity works as expected. + /// + private static void AnimateFadeIn(FrameworkElement element) + { + element.Opacity = 0; + + if (element.IsLoaded) + { + StartFadeStoryboard(element); + } + else + { + void OnLoaded(object sender, RoutedEventArgs e) + { + element.Loaded -= OnLoaded; + StartFadeStoryboard(element); + } + element.Loaded += OnLoaded; } } + private static void StartFadeStoryboard(UIElement element) + { + var fade = new DoubleAnimation + { + From = 0, + To = 1, + Duration = new Duration(TimeSpan.FromMilliseconds(300)), + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } + }; + Storyboard.SetTarget(fade, element); + Storyboard.SetTargetProperty(fade, "Opacity"); + + var storyboard = new Storyboard(); + storyboard.Children.Add(fade); + storyboard.Begin(); + } + private void CardControl_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e) { if (sender is CreditCardControl cardControl) @@ -179,10 +252,13 @@ private void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChan } } - private void AddButton_Click(object sender, RoutedEventArgs e) - { - _viewModel?.AddNewCommand.Execute(null); - } + private void AddButton_Click(object sender, RoutedEventArgs e) => InvokeAddNew(); + + /// + /// Opens the "new item" editor. Public so the Ctrl+N accelerator handled by + /// can route the shortcut to whichever list page is shown. + /// + public void InvokeAddNew() => _viewModel?.AddNewCommand.Execute(null); private void ViewToggle_Click(object sender, RoutedEventArgs e) { @@ -207,9 +283,11 @@ private void EditEntry_Click(object sender, RoutedEventArgs e) _viewModel?.EditEntryCommand.Execute(entry); } - // --- Toast --- - - public void ShowSavedToast() => ListViewHelpers.ShowSavedToast(SavedTip); + private void DeleteEntry_Click(object sender, RoutedEventArgs e) + { + if (sender is Button btn && btn.Tag is CreditCardEntry entry) + _ = _viewModel?.DeleteSelectedCommand.ExecuteAsync(entry); + } // Hover effects — show/hide action buttons private void Row_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e) diff --git a/src/PassKey.Desktop/Views/DashboardView.xaml b/src/PassKey.Desktop/Views/DashboardView.xaml index af81d9e..14c6bff 100644 --- a/src/PassKey.Desktop/Views/DashboardView.xaml +++ b/src/PassKey.Desktop/Views/DashboardView.xaml @@ -31,8 +31,7 @@ VerticalAlignment="Center" TextChanged="DashSearchBox_TextChanged" SuggestionChosen="DashSearchBox_SuggestionChosen" - QuerySubmitted="DashSearchBox_QuerySubmitted" - AutomationProperties.Name="Cerca nel vault"> + QuerySubmitted="DashSearchBox_QuerySubmitted"> @@ -90,8 +89,7 @@ PointerPressed="HealthBadge_PointerPressed" PointerEntered="HealthBadge_PointerEntered" PointerExited="HealthBadge_PointerExited" - ToolTipService.ToolTip="Apri Verifica vault" - AutomationProperties.Name="Salute del vault — apri Verifica vault"> + x:Uid="DashHealthBadge"> @@ -217,8 +215,8 @@ VerticalAlignment="Center" /> + IsClosable="False" Visibility="Collapsed" + AutomationProperties.LiveSetting="Polite" /> - + + + + diff --git a/src/PassKey.Desktop/Views/DashboardView.xaml.cs b/src/PassKey.Desktop/Views/DashboardView.xaml.cs index b04ebb6..107f765 100644 --- a/src/PassKey.Desktop/Views/DashboardView.xaml.cs +++ b/src/PassKey.Desktop/Views/DashboardView.xaml.cs @@ -290,7 +290,7 @@ private void OpenUrlAction_Click(object sender, RoutedEventArgs e) { _ = Windows.System.Launcher.LaunchUriAsync(new Uri(item.Url!)); } - catch { /* invalid URL */ } + catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[Dashboard] Could not open recent-item URL: {ex}"); } } } @@ -338,7 +338,12 @@ private void DashSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQ { if (args.ChosenSuggestion is SearchResultItem item) { - _viewModel?.NavigateToItem(item.EntityType, item.EntityId); + // Defer the navigation: navigating synchronously inside this event tears down + // the Dashboard (and this AutoSuggestBox) before the control closes its + // suggestion popup. The orphaned popup then overlays the destination page and + // silently swallows all pointer input. Enqueueing lets the AutoSuggestBox + // finish closing its popup before the view is replaced. + DispatcherQueue.TryEnqueue(() => _viewModel?.NavigateToItem(item.EntityType, item.EntityId)); } } diff --git a/src/PassKey.Desktop/Views/GeneratorView.xaml b/src/PassKey.Desktop/Views/GeneratorView.xaml index 2a95214..efbd593 100644 --- a/src/PassKey.Desktop/Views/GeneratorView.xaml +++ b/src/PassKey.Desktop/Views/GeneratorView.xaml @@ -3,6 +3,21 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + + + + + + @@ -13,7 +28,10 @@ AutomationProperties.HeadingLevel="Level1" /> + - - - + + + + + + @@ -136,10 +161,10 @@ Foreground="{ThemeResource TextFillColorSecondaryBrush}" /> @@ -154,10 +179,10 @@ Foreground="{ThemeResource TextFillColorSecondaryBrush}" /> @@ -172,10 +197,10 @@ Foreground="{ThemeResource TextFillColorSecondaryBrush}" /> @@ -190,10 +215,10 @@ Foreground="{ThemeResource TextFillColorSecondaryBrush}" /> @@ -208,10 +233,10 @@ Foreground="{ThemeResource TextFillColorSecondaryBrush}" /> @@ -220,17 +245,17 @@ - diff --git a/src/PassKey.Desktop/Views/GeneratorView.xaml.cs b/src/PassKey.Desktop/Views/GeneratorView.xaml.cs index c21b7ad..30c4e8d 100644 --- a/src/PassKey.Desktop/Views/GeneratorView.xaml.cs +++ b/src/PassKey.Desktop/Views/GeneratorView.xaml.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Xaml.Documents; using Microsoft.UI.Xaml.Media; using Microsoft.Windows.ApplicationModel.Resources; +using PassKey.Desktop.Helpers; using PassKey.Desktop.ViewModels; namespace PassKey.Desktop.Views; @@ -25,10 +26,23 @@ public sealed partial class GeneratorView : UserControl { private GeneratorViewModel? _viewModel; private bool _updatingFromVm; + // Shared loader (also used by the static crack-time helpers). + private static readonly ResourceLoader s_res = new(); public GeneratorView() { InitializeComponent(); + // Re-render colour-dependent UI when the app theme changes at runtime, so the + // code-built colours (password syntax, strength bar, history) follow the new theme. + ActualThemeChanged += OnActualThemeChanged; + } + + private void OnActualThemeChanged(FrameworkElement sender, object args) + { + if (_viewModel is null) return; + UpdatePasswordDisplay(_viewModel.GeneratedPassword); + UpdateStrengthUI(); + UpdateHistoryUI(); } public void SetViewModel(GeneratorViewModel vm) @@ -59,7 +73,7 @@ private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.Pr { case nameof(GeneratorViewModel.GeneratedPassword): UpdatePasswordDisplay(_viewModel?.GeneratedPassword ?? string.Empty); - Announce("Nuova password generata"); + Announce(s_res.GetString("GeneratorPwGenerated")); break; case nameof(GeneratorViewModel.StrengthResult): @@ -92,9 +106,9 @@ private void UpdatePasswordDisplay(string password) if (string.IsNullOrEmpty(password)) return; - var letterBrush = (Brush)Application.Current.Resources["PasswordCharLetterBrush"]; - var digitBrush = (Brush)Application.Current.Resources["PasswordCharDigitBrush"]; - var symbolBrush = (Brush)Application.Current.Resources["PasswordCharSymbolBrush"]; + var letterBrush = ThemeBrush("PasswordCharLetterBrush"); + var digitBrush = ThemeBrush("PasswordCharDigitBrush"); + var symbolBrush = ThemeBrush("PasswordCharSymbolBrush"); // Group consecutive characters of the same type into a single Run var currentType = ClassifyChar(password[0]); @@ -171,7 +185,7 @@ private void UpdateStrengthUI() StrengthLabel.Foreground = brush; // Crack time - CrackTimeText.Text = GetCrackTimeLabel(result.EstimatedCrackTime); + CrackTimeText.Text = CrackTimeFormatter.Localize(result.EstimatedCrackTime); // Segmented bar UpdateStrengthBar(result.Score, brush); @@ -180,7 +194,6 @@ private void UpdateStrengthUI() private void UpdateStrengthBar(int score, Brush? activeBrush) { var segments = new[] { StrengthSeg0, StrengthSeg1, StrengthSeg2, StrengthSeg3, StrengthSeg4 }; - var inactiveBrush = (Brush)Application.Current.Resources["ControlStrongFillColorDisabledBrush"]; int filledCount; if (score == 0) filledCount = 0; @@ -192,7 +205,12 @@ private void UpdateStrengthBar(int score, Brush? activeBrush) for (int i = 0; i < segments.Length; i++) { - segments[i].Background = i < filledCount ? (activeBrush ?? inactiveBrush) : inactiveBrush; + if (i < filledCount && activeBrush is not null) + segments[i].Background = activeBrush; + else + // Revert to the XAML-declared {ThemeResource ControlStrongFillColorDisabledBrush} + // so the inactive colour stays theme-aware. + segments[i].ClearValue(Border.BackgroundProperty); } } @@ -220,13 +238,8 @@ private void UpdateHistoryUI() private Grid CreateHistoryItem(GeneratorViewModel.HistoryEntry entry) { - var grid = new Grid - { - Padding = new Thickness(10, 8, 10, 8), - ColumnSpacing = 8, - CornerRadius = new CornerRadius(6), - Background = (Brush)Application.Current.Resources["CardBackgroundFillColorDefaultBrush"] - }; + // Style (theme-aware ThemeResource background) defined in GeneratorView.xaml. + var grid = new Grid { Style = (Style)Resources["HistoryItemGridStyle"] }; grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // strength dot grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // password grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // timestamp @@ -260,9 +273,7 @@ private Grid CreateHistoryItem(GeneratorViewModel.HistoryEntry entry) var timeText = new TextBlock { Text = GetRelativeTime(entry.GeneratedAt), - VerticalAlignment = VerticalAlignment.Center, - Foreground = (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"], - FontSize = 12 + Style = (Style)Resources["HistoryTimeTextStyle"] }; Grid.SetColumn(timeText, 2); grid.Children.Add(timeText); @@ -273,7 +284,7 @@ private Grid CreateHistoryItem(GeneratorViewModel.HistoryEntry entry) Padding = new Thickness(6, 4, 6, 4), Content = new FontIcon { Glyph = "\uE8C8", FontSize = 12 } }; - ToolTipService.SetToolTip(copyBtn, "Copia"); + ToolTipService.SetToolTip(copyBtn, s_res.GetString("ButtonCopy")); copyBtn.Click += (_, _) => _viewModel?.CopyHistoryEntryCommand.Execute(entry); Grid.SetColumn(copyBtn, 3); grid.Children.Add(copyBtn); @@ -295,7 +306,7 @@ private string GetRelativeTime(DateTime dt) private async void ShowCopyFeedback() { CopyIcon.Glyph = "\uE73E"; // Checkmark - Announce("Password copiata negli appunti"); + Announce(s_res.GetString("GeneratorPwCopied")); await Task.Delay(2000); @@ -362,34 +373,7 @@ private string GetStrengthLabel(string label) }; } - private static string GetCrackTimeLabel(string time) => time switch - { - "instant" => "Istantaneo", - "seconds" => "Pochi secondi", - "centuries" => "Secoli", - "millennia" => "Millenni", - _ => LocalizeCrackTimeString(time) - }; - - private static string LocalizeCrackTimeString(string time) - { - var parts = time.Split(' ', 2); - if (parts.Length != 2) return time; - - var number = parts[0]; - var unit = parts[1].ToLowerInvariant(); - - return unit switch - { - "minutes" or "minute" => $"{number} minuti", - "hours" or "hour" => $"{number} ore", - "days" or "day" => $"{number} giorni", - "years" or "year" => $"{number} anni", - _ => time - }; - } - - private static Brush GetStrengthBrush(int score) + private Brush GetStrengthBrush(int score) { var key = score switch { @@ -399,6 +383,23 @@ private static Brush GetStrengthBrush(int score) < 80 => "StrengthStrongBrush", _ => "StrengthVeryStrongBrush" }; + return ThemeBrush(key); + } + + /// + /// Resolves a brush that lives inside ThemeColors' ThemeDictionaries for the control's + /// current ActualTheme. Needed because Application.Current.Resources[key] is NOT theme-aware + /// for keys declared inside ThemeDictionaries (it returns the wrong theme's value). + /// + private Brush ThemeBrush(string key) + { + var dictKey = ActualTheme == ElementTheme.Dark ? "Default" : "Light"; + foreach (var md in Application.Current.Resources.MergedDictionaries) + { + if (md.ThemeDictionaries.TryGetValue(dictKey, out var obj) && + obj is ResourceDictionary td && td.TryGetValue(key, out var b) && b is Brush brush) + return brush; + } return (Brush)Application.Current.Resources[key]; } } diff --git a/src/PassKey.Desktop/Views/HelpView.xaml b/src/PassKey.Desktop/Views/HelpView.xaml index 99adf02..772c457 100644 --- a/src/PassKey.Desktop/Views/HelpView.xaml +++ b/src/PassKey.Desktop/Views/HelpView.xaml @@ -214,6 +214,17 @@ TextWrapping="Wrap" Margin="0,8,0,4" /> + + + + + + + @@ -282,7 +293,7 @@ Style="{StaticResource BodyStrongTextBlockStyle}" /> diff --git a/src/PassKey.Desktop/Views/IdentitiesListView.xaml b/src/PassKey.Desktop/Views/IdentitiesListView.xaml index 6c58b9e..7f33bdf 100644 --- a/src/PassKey.Desktop/Views/IdentitiesListView.xaml +++ b/src/PassKey.Desktop/Views/IdentitiesListView.xaml @@ -35,16 +35,14 @@ x:Name="SearchBox" PlaceholderText="Cerca identità..." QueryIcon="Find" - MaxWidth="300" + Width="280" HorizontalAlignment="Right" - TextChanged="SearchBox_TextChanged" - AutomationProperties.Name="Cerca identità" /> + TextChanged="SearchBox_TextChanged" /> + private void RefreshListContainers() + { + IdentityList.ItemsSource = null; + IdentityList.ItemsSource = _viewModel?.Entries; + } + private void UpdateEmptyState() { if (_viewModel is null) return; @@ -108,10 +135,13 @@ private void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChan } } - private void AddButton_Click(object sender, RoutedEventArgs e) - { - _viewModel?.AddNewCommand.Execute(null); - } + private void AddButton_Click(object sender, RoutedEventArgs e) => InvokeAddNew(); + + /// + /// Opens the "new item" editor. Public so the Ctrl+N accelerator handled by + /// can route the shortcut to whichever list page is shown. + /// + public void InvokeAddNew() => _viewModel?.AddNewCommand.Execute(null); private void IdentityList_ItemClick(object sender, ItemClickEventArgs e) { @@ -181,9 +211,6 @@ private void Card_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRo card.BorderBrush = (Brush)brush; } - // Save confirmation toast - public void ShowSavedToast() => ListViewHelpers.ShowSavedToast(SavedTip); - // ── Formatters ──────────────────────────────────────────────────────────── /// Formats a phone number with spaces (e.g. "+393518584980" → "+39 351 858 4980"). diff --git a/src/PassKey.Desktop/Views/IdentityDetailView.xaml b/src/PassKey.Desktop/Views/IdentityDetailView.xaml index 3fe0acb..49b93b7 100644 --- a/src/PassKey.Desktop/Views/IdentityDetailView.xaml +++ b/src/PassKey.Desktop/Views/IdentityDetailView.xaml @@ -26,9 +26,9 @@ @@ -47,29 +47,43 @@ + + + + + + + @@ -77,23 +91,47 @@ + + + + + + + + + + + + + @@ -112,9 +150,9 @@ @@ -127,17 +165,17 @@ @@ -151,17 +189,17 @@ @@ -170,9 +208,9 @@ @@ -193,9 +231,9 @@ @@ -203,9 +241,9 @@ @@ -213,9 +251,9 @@ @@ -223,9 +261,9 @@ @@ -235,6 +273,7 @@ - @@ -268,9 +306,9 @@ - @@ -118,10 +120,15 @@ + @@ -134,19 +141,24 @@ + @@ -156,9 +168,9 @@ @@ -169,9 +181,8 @@ @@ -281,17 +287,18 @@ + Header="Chiave Base32" /> @@ -325,9 +331,9 @@ - - + @@ -185,6 +191,7 @@ Grid.Row="2" Visibility="Collapsed" Icon="" + AccentBrush="{ThemeResource PasswordsIconBrush}" Title="Nessuna password salvata" Subtitle="Aggiungi la tua prima password per iniziare a proteggere i tuoi account." /> @@ -198,12 +205,5 @@ BorderThickness="1,0,0,0"> - - - diff --git a/src/PassKey.Desktop/Views/PasswordsListView.xaml.cs b/src/PassKey.Desktop/Views/PasswordsListView.xaml.cs index cc25a6c..d4ec655 100644 --- a/src/PassKey.Desktop/Views/PasswordsListView.xaml.cs +++ b/src/PassKey.Desktop/Views/PasswordsListView.xaml.cs @@ -5,7 +5,6 @@ using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.Windows.ApplicationModel.Resources; using PassKey.Core.Models; -using PassKey.Desktop.Helpers; using PassKey.Desktop.ViewModels; using Windows.Storage.Streams; @@ -27,6 +26,9 @@ public PasswordsListView() public async void SetViewModel(PasswordsListViewModel vm) { + // Drop any handler attached to a previous VM to avoid subscription leaks. + if (_viewModel is not null) _viewModel.PropertyChanged -= OnViewModelPropertyChanged; + _viewModel = vm; DataContext = vm; @@ -34,11 +36,17 @@ public async void SetViewModel(PasswordsListViewModel vm) EmptyState.Title = _resourceLoader.GetString("EmptyPasswordsTitle"); EmptyState.Subtitle = _resourceLoader.GetString("EmptyPasswordsSubtitle"); vm.PropertyChanged += OnViewModelPropertyChanged; - vm.SaveCompleted += ShowSavedToast; await vm.LoadEntriesCommand.ExecuteAsync(null); UpdateList(); UpdateEmptyState(); + + // Sync the detail panel from VM state — see CreditCardsListView for the full + // rationale. Without this, navigating away with a detail open and coming back + // leaves the UI frozen (Edit clicks become no-ops because IsDetailOpen never + // changes value, so PropertyChanged doesn't fire). + UpdateDetailPanel(); + UpdateDetailContent(); } private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) @@ -100,10 +108,6 @@ private void UpdateDetailContent() } } - // --- Toast --- - - public void ShowSavedToast() => ListViewHelpers.ShowSavedToast(SavedTip); - // --- Search --- private void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) @@ -117,10 +121,13 @@ private void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChan // --- Add --- - private void AddButton_Click(object sender, RoutedEventArgs e) - { - _viewModel?.AddNewCommand.Execute(null); - } + private void AddButton_Click(object sender, RoutedEventArgs e) => InvokeAddNew(); + + /// + /// Opens the "new item" editor. Public so the Ctrl+N accelerator handled by + /// can route the shortcut to whichever list page is shown. + /// + public void InvokeAddNew() => _viewModel?.AddNewCommand.Execute(null); // --- Item click --- @@ -150,6 +157,12 @@ private void EditEntry_Click(object sender, RoutedEventArgs e) _viewModel?.EditEntryCommand.Execute(entry); } + private void DeleteEntry_Click(object sender, RoutedEventArgs e) + { + if (sender is Button btn && btn.Tag is PasswordEntry entry) + _ = _viewModel?.DeleteSelectedCommand.ExecuteAsync(entry); + } + // --- Column header sort (Step A) --- private void ColumnHeader_Tapped(object sender, TappedRoutedEventArgs e) diff --git a/src/PassKey.Desktop/Views/SecureNoteDetailView.xaml b/src/PassKey.Desktop/Views/SecureNoteDetailView.xaml index 470c121..73e0258 100644 --- a/src/PassKey.Desktop/Views/SecureNoteDetailView.xaml +++ b/src/PassKey.Desktop/Views/SecureNoteDetailView.xaml @@ -32,17 +32,20 @@ Style="{StaticResource CaptionTextBlockStyle}" AutomationProperties.HeadingLevel="Level2" /> + VerticalAlignment="Center" /> + @@ -50,16 +53,11 @@ - - - - + @@ -73,20 +71,20 @@ - - - - - - + Subtitle="Crea la tua prima nota sicura per salvare informazioni riservate." /> + Subtitle="Prova a modificare i filtri o la ricerca." /> - - - - + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + { @@ -207,10 +227,13 @@ private void SearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChan } } - private void AddButton_Click(object sender, RoutedEventArgs e) - { - _viewModel?.AddNewCommand.Execute(null); - } + private void AddButton_Click(object sender, RoutedEventArgs e) => InvokeAddNew(); + + /// + /// Opens the "new item" editor. Public so the Ctrl+N accelerator handled by + /// can route the shortcut to whichever list page is shown. + /// + public void InvokeAddNew() => _viewModel?.AddNewCommand.Execute(null); private void NotesList_ItemClick(object sender, ItemClickEventArgs e) { @@ -248,14 +271,14 @@ private void NotesList_ContainerContentChanging(ListViewBase sender, } else if (entry.IsPinned && (idx == 0 || !entries[idx - 1].IsPinned)) { - // Primo pinnato → "Fissate" - sectionHeader.Text = "Fissate"; + // Primo pinnato → header "Fissate" + sectionHeader.Text = _resourceLoader.GetString("NoteSectionPinned"); sectionHeader.Visibility = Visibility.Visible; } else if (!entry.IsPinned && idx > 0 && entries[idx - 1].IsPinned) { - // Primo non-pinnato dopo pinnati → "Note" - sectionHeader.Text = "Note"; + // Primo non-pinnato dopo pinnati → header "Note" + sectionHeader.Text = _resourceLoader.GetString("NoteSectionOthers"); sectionHeader.Visibility = Visibility.Visible; } else @@ -306,13 +329,9 @@ private void NotesList_ContainerContentChanging(ListViewBase sender, // Accessibility: ItemStatus per note pinnate Microsoft.UI.Xaml.Automation.AutomationProperties.SetItemStatus( - args.ItemContainer, entry.IsPinned ? "Fissata" : ""); + args.ItemContainer, entry.IsPinned ? _resourceLoader.GetString("NotePinnedStatus") : ""); } - // --- Toast conferma salvataggio --- - - public void ShowSavedToast() => ListViewHelpers.ShowSavedToast(SavedTip, () => Announce("Nota salvata.")); - // --- Helpers --- private void Announce(string message) diff --git a/src/PassKey.Desktop/Views/SettingsView.xaml b/src/PassKey.Desktop/Views/SettingsView.xaml index 3d3383c..3588e00 100644 --- a/src/PassKey.Desktop/Views/SettingsView.xaml +++ b/src/PassKey.Desktop/Views/SettingsView.xaml @@ -1,37 +1,66 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:tk="using:CommunityToolkit.WinUI.Controls"> - - + + + + + + + + + + + + - + - - - - - - + + + + + diff --git a/src/PassKey.Desktop/Views/SettingsView.xaml.cs b/src/PassKey.Desktop/Views/SettingsView.xaml.cs index 1ff538c..1023de6 100644 --- a/src/PassKey.Desktop/Views/SettingsView.xaml.cs +++ b/src/PassKey.Desktop/Views/SettingsView.xaml.cs @@ -15,20 +15,58 @@ public sealed partial class SettingsView : UserControl private bool _updatingFromVm; private readonly ResourceLoader _resourceLoader = new(); private IDialogQueueService _dialogQueue = null!; + private IToastService _toast = null!; /// Raised when the user clicks "Guida e scorciatoie" to navigate to HelpView. public event Action? NavigateToHelpRequested; + /// Raised when the user clicks "Cronologia attività" to navigate to the activity-log viewer. + public event Action? NavigateToActivityLogRequested; + public SettingsView() { InitializeComponent(); } + /// + /// Returns the current vertical scroll offset so the shell can capture it before + /// navigating to the activity-log viewer and restore it on the way back (FU8). + /// + public double GetScrollOffset() => RootScroller.VerticalOffset; + + /// + /// Restores a previously captured vertical scroll offset (FU8). Deferred via the + /// dispatcher (and the Loaded event when needed) because a freshly-created view has + /// not completed its layout pass yet — an immediate ChangeView would be clamped to + /// zero since the ScrollViewer's extent is not known at that point. + /// + public void RestoreScrollOffset(double offset) + { + if (offset <= 0) return; + + void Apply() => RootScroller.ChangeView(null, offset, null, disableAnimation: true); + + if (IsLoaded) + { + DispatcherQueue.TryEnqueue(Apply); + } + else + { + void OnLoaded(object sender, RoutedEventArgs e) + { + Loaded -= OnLoaded; + DispatcherQueue.TryEnqueue(Apply); + } + Loaded += OnLoaded; + } + } + public void SetViewModel(SettingsViewModel vm) { _viewModel = vm; DataContext = vm; _dialogQueue = App.Services.GetRequiredService(); + _toast = App.Services.GetRequiredService(); _updatingFromVm = true; @@ -48,11 +86,28 @@ public void SetViewModel(SettingsViewModel vm) _updatingFromVm = false; - // Subscribe to VM changes + // Subscribe VM -> View events. The SettingsViewModel is a persistent singleton in + // ShellViewModel while this view is recreated on every navigation. If these handlers + // were never detached, each return to the Settings page would stack another set on + // the same VM, and VM-raised dialogs (master-password prompt, import format, merge + // strategy, …) would fire once per accumulated handler — the "dialogs multiply" bug + // (FU9). We detach on Unloaded so a discarded view releases its handlers; the + // defensive unsubscribe first guards against a repeated SetViewModel on the same view. + UnsubscribeVmEvents(vm); + SubscribeVmEvents(vm); + Unloaded += OnUnloaded; + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + Unloaded -= OnUnloaded; + if (_viewModel is not null) UnsubscribeVmEvents(_viewModel); + } + + private void SubscribeVmEvents(SettingsViewModel vm) + { vm.PropertyChanged += OnViewModelPropertyChanged; vm.ThemeChangeRequested += OnThemeChangeRequested; - - // Backup/Restore/Import event subscriptions vm.BackupPasswordRequested += OnBackupPasswordRequested; vm.RestoreWarningRequested += OnRestoreWarningRequested; vm.RestorePasswordRequested += OnRestorePasswordRequested; @@ -65,6 +120,22 @@ public void SetViewModel(SettingsViewModel vm) vm.OperationError += OnOperationError; } + private void UnsubscribeVmEvents(SettingsViewModel vm) + { + vm.PropertyChanged -= OnViewModelPropertyChanged; + vm.ThemeChangeRequested -= OnThemeChangeRequested; + vm.BackupPasswordRequested -= OnBackupPasswordRequested; + vm.RestoreWarningRequested -= OnRestoreWarningRequested; + vm.RestorePasswordRequested -= OnRestorePasswordRequested; + vm.ImportFormatRequested -= OnImportFormatRequested; + vm.ImportPasswordRequested -= OnImportPasswordRequested; + vm.MergeStrategyRequested -= OnMergeStrategyRequested; + vm.ImportCompleted -= OnImportCompleted; + vm.BackupCompleted -= OnBackupCompleted; + vm.RestoreCompleted -= OnRestoreCompleted; + vm.OperationError -= OnOperationError; + } + private void OnThemeChangeRequested(ElementTheme theme) { App.MainWindow?.ApplyTheme(theme); @@ -113,8 +184,17 @@ private void RestartNowButton_Click(object sender, RoutedEventArgs e) { var exePath = Environment.ProcessPath; if (exePath is not null) + // The "--restart" flag tells the new instance it was spawned by a language + // restart: instead of bailing out under the single-instance guard (this + // process is still alive for a few moments), it waits for us to exit and + // then claims ownership. Without it the new instance would see "another + // instance running", exit immediately, and leave nothing running. System.Diagnostics.Process.Start( - new System.Diagnostics.ProcessStartInfo(exePath) { UseShellExecute = true }); + new System.Diagnostics.ProcessStartInfo(exePath) + { + UseShellExecute = true, + Arguments = "--restart" + }); Application.Current.Exit(); } @@ -138,6 +218,70 @@ private void HelpButton_Click(object sender, RoutedEventArgs e) NavigateToHelpRequested?.Invoke(); } + private void ActivityLogButton_Click(object sender, RoutedEventArgs e) + { + NavigateToActivityLogRequested?.Invoke(); + } + + /// + /// Shows a step-by-step guide for installing the PassKey browser extension, + /// with direct links to the Chrome Web Store and Firefox Add-ons. + /// + private async void BrowserExtensionButton_Click(object sender, RoutedEventArgs e) + { + const string chromeUrl = "https://chromewebstore.google.com/detail/passkey/jadfnbfppmcpbfiickiolonfldkphmfb"; + const string firefoxUrl = "https://addons.mozilla.org/firefox/addon/passkey/"; + + var panel = new StackPanel { Spacing = 10 }; + + panel.Children.Add(new TextBlock + { + Text = _resourceLoader.GetString("BrowserExtIntro"), + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 0, 0, 4) + }); + + panel.Children.Add(new TextBlock + { + Text = _resourceLoader.GetString("BrowserExtStep1"), + TextWrapping = TextWrapping.Wrap, + FontWeight = Microsoft.UI.Text.FontWeights.SemiBold + }); + + var chromeButton = new Button { Content = _resourceLoader.GetString("BrowserExtChromeBtn") }; + chromeButton.Click += (_, _) => _ = Windows.System.Launcher.LaunchUriAsync(new Uri(chromeUrl)); + + var firefoxButton = new Button { Content = _resourceLoader.GetString("BrowserExtFirefoxBtn") }; + firefoxButton.Click += (_, _) => _ = Windows.System.Launcher.LaunchUriAsync(new Uri(firefoxUrl)); + + var buttonRow = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + buttonRow.Children.Add(chromeButton); + buttonRow.Children.Add(firefoxButton); + panel.Children.Add(buttonRow); + + panel.Children.Add(new TextBlock + { + Text = _resourceLoader.GetString("BrowserExtStep2"), + TextWrapping = TextWrapping.Wrap + }); + panel.Children.Add(new TextBlock + { + Text = _resourceLoader.GetString("BrowserExtStep3"), + TextWrapping = TextWrapping.Wrap + }); + + var dialog = new ContentDialog + { + Title = _resourceLoader.GetString("BrowserExtDialogTitle"), + Content = panel, + CloseButtonText = _resourceLoader.GetString("BrowserExtClose"), + DefaultButton = ContentDialogButton.Close, + XamlRoot = XamlRoot + }; + + await _dialogQueue.EnqueueAndWait(() => dialog.ShowAsync().AsTask()); + } + private async void ChangePwButton_Click(object sender, RoutedEventArgs e) { if (_viewModel is null) return; @@ -256,6 +400,84 @@ private Task ShowInfoDialogAsync(string title, string message) return _dialogQueue.EnqueueAndWait(() => dialog.ShowAsync().AsTask()); } + /// + /// Clears the whole vault after a two-step confirmation: an irreversible-action warning, + /// then a master-password re-entry prompt. Metadata and master password are preserved. + /// + private async void ClearVaultButton_Click(object sender, RoutedEventArgs e) + { + if (_viewModel is null) return; + + // Step 1 — irreversible-action warning. + var proceed = await _dialogQueue.ConfirmAsync( + title: _resourceLoader.GetString("ClearVaultWarnTitle"), + content: _resourceLoader.GetString("ClearVaultWarnMessage"), + primaryButtonText: _resourceLoader.GetString("ClearVaultWarnContinue"), + closeButtonText: _resourceLoader.GetString("RestoreWarningCancel"), + defaultButton: ContentDialogButton.Close); + if (!proceed) return; + + // Step 2 — master-password re-entry. + var pwBox = new SecureInputBox + { + PlaceholderText = _resourceLoader.GetString("ClearVaultPwPlaceholder"), + ShowRevealButton = Visibility.Visible, + Width = 320 + }; + var errorText = new TextBlock + { + Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["SystemFillColorCriticalBrush"], + Visibility = Visibility.Collapsed, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 8, 0, 0) + }; + var descText = new TextBlock + { + Text = _resourceLoader.GetString("ClearVaultPwDesc"), + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 0, 0, 12) + }; + var panel = new StackPanel { Spacing = 12 }; + panel.Children.Add(descText); + panel.Children.Add(pwBox); + panel.Children.Add(errorText); + + var dialog = new ContentDialog + { + Title = _resourceLoader.GetString("ClearVaultPwTitle"), + Content = panel, + PrimaryButtonText = _resourceLoader.GetString("ClearVaultPwConfirm"), + CloseButtonText = _resourceLoader.GetString("RestoreWarningCancel"), + DefaultButton = ContentDialogButton.Close, + XamlRoot = XamlRoot + }; + dialog.PrimaryButtonClick += (_, args) => + { + if (string.IsNullOrEmpty(pwBox.Password)) + { + errorText.Text = _resourceLoader.GetString("ClearVaultPwErrEmpty"); + errorText.Visibility = Visibility.Visible; + args.Cancel = true; + } + }; + + var result = await _dialogQueue.EnqueueAndWait(() => dialog.ShowAsync().AsTask()); + if (result != ContentDialogResult.Primary) return; + + ClearVaultButton.IsEnabled = false; + try + { + var ok = await _viewModel.PerformClearVaultAsync(pwBox.Password); + await ShowInfoDialogAsync( + ok ? _resourceLoader.GetString("ClearVaultSuccessTitle") : _resourceLoader.GetString("ClearVaultErrorTitle"), + ok ? _resourceLoader.GetString("ClearVaultSuccessMessage") : _resourceLoader.GetString("ClearVaultErrWrong")); + } + finally + { + ClearVaultButton.IsEnabled = true; + } + } + // ═══ AGGIORNAMENTI ═══ private void AutoUpdateToggle_Toggled(object sender, RoutedEventArgs e) @@ -505,6 +727,11 @@ private async Task OnRestoreWarningRequested() XamlRoot = XamlRoot }; + // FU3 UX: a KDBX cannot be opened without a password, so keep "OK" disabled until + // the user types something instead of letting them proceed with an empty password. + dialog.IsPrimaryButtonEnabled = false; + pwBox.PasswordChanged += (_, pw) => dialog.IsPrimaryButtonEnabled = !string.IsNullOrEmpty(pw); + var result = await _dialogQueue.EnqueueAndWait(() => dialog.ShowAsync().AsTask()); return result == ContentDialogResult.Primary ? (pwBox.Password, true) @@ -570,18 +797,17 @@ private async Task OnImportCompleted(ImportResult result) await ShowInfoDialogAsync(_resourceLoader.GetString("ImportSummaryTitle"), lines.ToString().TrimEnd()); } - private async Task OnBackupCompleted(string path) + private Task OnBackupCompleted(string path) { - await ShowInfoDialogAsync( - _resourceLoader.GetString("BackupSuccessTitle"), - _resourceLoader.GetString("BackupSuccessMessage")); + // A modal dialog is too heavy for a simple "done" confirmation — use a toast. + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("BackupSuccessMessage")); + return Task.CompletedTask; } - private async Task OnRestoreCompleted() + private Task OnRestoreCompleted() { - await ShowInfoDialogAsync( - _resourceLoader.GetString("RestoreSuccessTitle"), - _resourceLoader.GetString("RestoreSuccessMessage")); + _toast.Show(ToastSeverity.Success, _resourceLoader.GetString("RestoreSuccessMessage")); + return Task.CompletedTask; } private async Task OnOperationError(string errorCode) @@ -590,6 +816,13 @@ private async Task OnOperationError(string errorCode) { "WRONG_PASSWORD" => _resourceLoader.GetString("RestoreErrorWrongPw"), "INVALID_FILE" => _resourceLoader.GetString("RestoreErrorInvalid"), + "OPERATION_FAILED" => _resourceLoader.GetString("OperationGenericError"), + "IMPORT_BW_ENCRYPTED" => _resourceLoader.GetString("ImportErrBwEncrypted"), + "IMPORT_BW_ZIP" => _resourceLoader.GetString("ImportErrBwZip"), + "IMPORT_1PUX" => _resourceLoader.GetString("ImportErr1pux"), + "IMPORT_KEEPASS_1X" => _resourceLoader.GetString("ImportErrKeepass1x"), + "IMPORT_KEEPASS_OPEN" => _resourceLoader.GetString("ImportErrKeepassOpen"), + "IMPORT_NO_ENTRIES" => _resourceLoader.GetString("ImportErrNoEntries"), _ => errorCode }; await ShowInfoDialogAsync(_resourceLoader.GetString("ImportErrorTitle"), message); diff --git a/src/PassKey.Desktop/Views/SetupView.xaml b/src/PassKey.Desktop/Views/SetupView.xaml index 3665670..91697c0 100644 --- a/src/PassKey.Desktop/Views/SetupView.xaml +++ b/src/PassKey.Desktop/Views/SetupView.xaml @@ -34,6 +34,7 @@ @@ -44,8 +45,8 @@ @@ -97,12 +98,12 @@ + + diff --git a/src/PassKey.Desktop/Views/WelcomeView.xaml.cs b/src/PassKey.Desktop/Views/WelcomeView.xaml.cs index 9075d21..5506a59 100644 --- a/src/PassKey.Desktop/Views/WelcomeView.xaml.cs +++ b/src/PassKey.Desktop/Views/WelcomeView.xaml.cs @@ -1,16 +1,24 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.ApplicationModel.Resources; +using PassKey.Desktop.Controls; +using PassKey.Desktop.Services; using PassKey.Desktop.ViewModels; namespace PassKey.Desktop.Views; /// /// Welcome view shown after first-run vault creation. -/// Offers quick actions: add password, import, settings, or continue. +/// Offers quick actions: add password, import, restore backup, settings, or continue. /// public sealed partial class WelcomeView : UserControl { private WelcomeViewModel? _viewModel; + private readonly ResourceLoader _resourceLoader = new(); + private IDialogQueueService _dialogQueue = null!; public WelcomeView() { @@ -21,6 +29,13 @@ public void SetViewModel(WelcomeViewModel vm) { _viewModel = vm; DataContext = vm; + _dialogQueue = App.Services.GetRequiredService(); + + // Restore-flow dialogs handled here in the code-behind. + vm.RestoreWarningRequested += OnRestoreWarningRequested; + vm.RestorePasswordRequested += OnRestorePasswordRequested; + vm.RestoreCompleted += OnRestoreCompleted; + vm.OperationError += OnOperationError; } private void AddPasswordButton_Click(object sender, RoutedEventArgs e) @@ -29,9 +44,82 @@ private void AddPasswordButton_Click(object sender, RoutedEventArgs e) private void ImportButton_Click(object sender, RoutedEventArgs e) => _viewModel?.ImportDataCommand.Execute(null); + private void RestoreBackupButton_Click(object sender, RoutedEventArgs e) + => _viewModel?.RestoreBackupCommand.Execute(null); + private void SettingsButton_Click(object sender, RoutedEventArgs e) => _viewModel?.OpenSettingsCommand.Execute(null); private void ContinueButton_Click(object sender, RoutedEventArgs e) => _viewModel?.ContinueCommand.Execute(null); + + // ─── Restore dialogs (strings reused from the Settings restore flow) ────── + + private async Task OnRestoreWarningRequested() + { + var dialog = new ContentDialog + { + Title = _resourceLoader.GetString("RestoreWarningTitle"), + Content = _resourceLoader.GetString("RestoreWarningMessage"), + PrimaryButtonText = _resourceLoader.GetString("RestoreWarningConfirm"), + CloseButtonText = _resourceLoader.GetString("RestoreWarningCancel"), + DefaultButton = ContentDialogButton.Close, + XamlRoot = XamlRoot + }; + var result = await _dialogQueue.EnqueueAndWait(() => dialog.ShowAsync().AsTask()); + return result == ContentDialogResult.Primary; + } + + private async Task<(string password, bool confirmed)> OnRestorePasswordRequested() + { + var pwBox = new SecureInputBox + { + PlaceholderText = _resourceLoader.GetString("RestorePwPlaceholder"), + ShowRevealButton = Visibility.Visible, + Width = 320 + }; + var dialog = new ContentDialog + { + Title = _resourceLoader.GetString("RestorePwDialogTitle"), + Content = pwBox, + PrimaryButtonText = _resourceLoader.GetString("ButtonOk"), + CloseButtonText = _resourceLoader.GetString("RestoreWarningCancel"), + DefaultButton = ContentDialogButton.Primary, + XamlRoot = XamlRoot + }; + var result = await _dialogQueue.EnqueueAndWait(() => dialog.ShowAsync().AsTask()); + return result == ContentDialogResult.Primary + ? (pwBox.Password, true) + : (string.Empty, false); + } + + private Task OnRestoreCompleted() + => ShowInfoDialogAsync( + _resourceLoader.GetString("RestoreSuccessTitle"), + _resourceLoader.GetString("RestoreSuccessMessage")); + + private Task OnOperationError(string errorCode) + { + var message = errorCode switch + { + "WRONG_PASSWORD" => _resourceLoader.GetString("RestoreErrorWrongPw"), + "INVALID_FILE" => _resourceLoader.GetString("RestoreErrorInvalid"), + "OPERATION_FAILED" => _resourceLoader.GetString("OperationGenericError"), + _ => errorCode + }; + return ShowInfoDialogAsync(_resourceLoader.GetString("ImportErrorTitle"), message); + } + + private Task ShowInfoDialogAsync(string title, string message) + { + var dialog = new ContentDialog + { + Title = title, + Content = message, + CloseButtonText = _resourceLoader.GetString("ButtonOk"), + DefaultButton = ContentDialogButton.Close, + XamlRoot = XamlRoot + }; + return _dialogQueue.EnqueueAndWait(() => dialog.ShowAsync().AsTask()); + } } diff --git a/src/PassKey.Tests/BitwardenImporterTests.cs b/src/PassKey.Tests/BitwardenImporterTests.cs index 4826833..4997d56 100644 --- a/src/PassKey.Tests/BitwardenImporterTests.cs +++ b/src/PassKey.Tests/BitwardenImporterTests.cs @@ -6,6 +6,47 @@ public class BitwardenImporterTests { private readonly BitwardenImporter _importer = new(); + [Fact] + public void ParseBitwarden_EncryptedExport_ThrowsImportFileException() + { + // FU3: an encrypted Bitwarden export carries "encrypted": true and no plaintext + // items. It must raise a clear ImportFileException, not silently return an empty + // vault. + var json = """ + { + "encrypted": true, + "passwordProtected": true, + "salt": "AiRlaTQEUwdu32IgAQ7wZA==", + "kdfType": 0, + "kdfIterations": 600000, + "data": "2.76nphWCd+C3AzRNx+hpTRw==|RPj5GQEHwakg..." + } + """; + + var ex = Assert.Throws(() => _importer.ParseBitwarden(json)); + // The importer now throws an error CODE (localized by the Desktop layer), not a literal message. + Assert.Equal("IMPORT_BW_ENCRYPTED", ex.Message); + } + + [Fact] + public void ParseBitwarden_PlaintextExport_NotTreatedAsEncrypted() + { + // A normal plaintext export sets "encrypted": false and must import normally. + var json = """ + { + "encrypted": false, + "items": [{ + "type": 1, + "name": "GitHub", + "login": { "username": "u", "password": "p" } + }] + } + """; + + var vault = _importer.ParseBitwarden(json); + Assert.Single(vault.Passwords); + } + [Fact] public void ParseBitwarden_LoginItem_MapsToPasswordEntry() { @@ -103,6 +144,55 @@ public void ParseBitwarden_IdentityItem_MapsToIdentityEntry() Assert.Equal("Springfield", id.City); } + [Fact] + public void ParseBitwarden_IdentityItem_MapsAllExtendedFields() + { + // FU4: the DTO previously dropped middleName, company, username, ssn, + // passportNumber, licenseNumber and address3 on import — they must now be mapped. + var json = """ + { + "items": [{ + "type": 4, + "name": "Full Identity", + "identity": { + "title": "Mr", + "firstName": "Joseph", + "middleName": "Q", + "lastName": "Public", + "company": "Acme Srl", + "username": "jpublic", + "ssn": "JHSROS92H10H264C", + "passportNumber": "AA1234BB", + "licenseNumber": "CC1234DD", + "email": "j@acme.it", + "phone": "+39000", + "address1": "Via Roma 1", + "address2": "Scala B", + "address3": "Interno 3", + "city": "Roma", + "state": "Lazio", + "postalCode": "00100", + "country": "Italia" + } + }] + } + """; + + var vault = _importer.ParseBitwarden(json); + + Assert.Single(vault.Identities); + var id = vault.Identities[0]; + Assert.Equal("Q", id.MiddleName); + Assert.Equal("Acme Srl", id.Company); + Assert.Equal("jpublic", id.Username); + // ssn -> codice fiscale / tessera sanitaria for Italian users + Assert.Equal("JHSROS92H10H264C", id.HealthCardNumber); + Assert.Equal("AA1234BB", id.PassportNumber); + Assert.Equal("CC1234DD", id.DrivingLicenseNumber); + // address1 + address2 + address3 combined + Assert.Equal("Via Roma 1, Scala B, Interno 3", id.Street); + } + [Fact] public void ParseBitwarden_SecureNoteItem_MapsToSecureNoteEntry() { diff --git a/src/PassKey.Tests/CsvImporterTests.cs b/src/PassKey.Tests/CsvImporterTests.cs index 2df1b52..3a43b87 100644 --- a/src/PassKey.Tests/CsvImporterTests.cs +++ b/src/PassKey.Tests/CsvImporterTests.cs @@ -137,4 +137,54 @@ public void ParseCsv_NoteSingularAlias_ImportedAsNotes() Assert.Single(vault.Passwords); Assert.Equal("My singular note", vault.Passwords[0].Notes); } + + [Fact] + public void ParseCsv_FirefoxFormat_TitleFallsBackToUrlHost() + { + // FU6a: Firefox exports have no title/name column — logins are identified by URL + // only. The importer must fall back to the URL host so entries aren't anonymous. + var csv = + "url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged\n" + + "https://accounts.google.com/,me@gmail.com,secret,,,{guid},1,2,3"; + + var vault = _importer.ParseCsv(csv); + + Assert.Single(vault.Passwords); + Assert.Equal("accounts.google.com", vault.Passwords[0].Title); + Assert.Equal("me@gmail.com", vault.Passwords[0].Username); + Assert.Equal("secret", vault.Passwords[0].Password); + } + + [Fact] + public void ParseCsv_KeePassFormat_SpacedHeadersMapped() + { + // FU6b: KeePass CSV uses spaced headers ("Account", "Login Name", "Web Site") + // that the legacy exact-match mapper ignored, importing entries almost empty. + var csv = + "\"Account\",\"Login Name\",\"Password\",\"Web Site\",\"Comments\"\n" + + "\"My Bank\",\"johndoe\",\"hunter2\",\"https://bank.example\",\"a note\""; + + var vault = _importer.ParseCsv(csv); + + Assert.Single(vault.Passwords); + var pw = vault.Passwords[0]; + Assert.Equal("My Bank", pw.Title); + Assert.Equal("johndoe", pw.Username); + Assert.Equal("hunter2", pw.Password); + Assert.Equal("https://bank.example", pw.Url); + Assert.Equal("a note", pw.Notes); + } + + [Fact] + public void ParseCsv_TitlelessRowWithoutUrl_StaysEmptyTitle() + { + // A surviving row (has credentials) but neither title nor URL: title stays empty, + // no spurious fallback. + var csv = "url,username,password\n,user,pass"; + var vault = _importer.ParseCsv(csv); + + Assert.Single(vault.Passwords); + Assert.Equal(string.Empty, vault.Passwords[0].Title); + Assert.Equal("user", vault.Passwords[0].Username); + } } diff --git a/src/PassKey.Tests/OnePuxImporterTests.cs b/src/PassKey.Tests/OnePuxImporterTests.cs index 2939a1b..b761bf8 100644 --- a/src/PassKey.Tests/OnePuxImporterTests.cs +++ b/src/PassKey.Tests/OnePuxImporterTests.cs @@ -160,6 +160,108 @@ public void ParseOnePux_NullAccounts_ReturnsEmptyVault() Assert.Empty(vault.Passwords); } + [Fact] + public void ParseOnePux_EmailAsObject_DoesNotCrashAndMapsEmail() + { + // FU7a: current 1Password exports store the email as an object + // { "email_address": ..., "provider": null } instead of a plain string. The legacy + // string deserialiser threw, aborting the ENTIRE import (every recent export has + // this in its default Starter Kit identity). Must now parse cleanly. + var json = """ + { + "accounts": [{ + "vaults": [{ + "items": [{ + "overview": { "title": "Sei tu" }, + "details": { + "sections": [{ + "fields": [ + { "id": "firstname", "title": "Nome", "value": { "string": "Mario" } }, + { "id": "email", "title": "Indirizzo e-mail", "value": { "email": { "email_address": "mario@esempio.it", "provider": null } } } + ] + }] + } + }] + }] + }] + } + """; + + var vault = _importer.ParseOnePux(json); + + Assert.Single(vault.Identities); + Assert.Equal("Mario", vault.Identities[0].FirstName); + Assert.Equal("mario@esempio.it", vault.Identities[0].Email); + } + + [Fact] + public void ParseOnePux_LocalizedIdentityTitlesMappedViaStableId() + { + // FU7b: a non-English 1Password export has localized titles ("Nome", "Cognome") + // but stable, language-independent ids ("firstname", "lastname"). Mapping must + // follow the id, not the title. + var json = """ + { + "accounts": [{ + "vaults": [{ + "items": [{ + "overview": { "title": "Identita IT" }, + "details": { + "sections": [{ + "fields": [ + { "id": "firstname", "title": "Nome", "value": { "string": "Giuseppe" } }, + { "id": "lastname", "title": "Cognome", "value": { "string": "Verdi" } } + ] + }] + } + }] + }] + }] + } + """; + + var vault = _importer.ParseOnePux(json); + + Assert.Single(vault.Identities); + Assert.Equal("Giuseppe", vault.Identities[0].FirstName); + Assert.Equal("Verdi", vault.Identities[0].LastName); + } + + [Fact] + public void ParseOnePux_LocalizedCreditCardMappedViaStableId() + { + // FU7b for cards: italian titles ("Titolare", "Numero", "Codice di verifica") + // carry no English keywords; the stable ids (cardholder, ccnum, cvv) drive mapping. + var json = """ + { + "accounts": [{ + "vaults": [{ + "items": [{ + "overview": { "title": "Carta IT" }, + "details": { + "sections": [{ + "fields": [ + { "id": "cardholder", "title": "Titolare", "value": { "string": "Mario Rossi" } }, + { "id": "ccnum", "title": "Numero", "value": { "creditCardNumber": "4111111111111111" } }, + { "id": "cvv", "title": "Codice di verifica", "value": { "concealed": "123" } } + ] + }] + } + }] + }] + }] + } + """; + + var vault = _importer.ParseOnePux(json); + + Assert.Single(vault.CreditCards); + var card = vault.CreditCards[0]; + Assert.Equal("Mario Rossi", card.CardholderName); + Assert.Equal("4111111111111111", card.CardNumber); + Assert.Equal("123", card.Cvv); + } + [Fact] public void ParseOnePux_MultipleVaults_AllProcessed() { diff --git a/src/PassKey.Tests/PasswordStrengthAnalyzerTests.cs b/src/PassKey.Tests/PasswordStrengthAnalyzerTests.cs index fb6f8e4..38d56e3 100644 --- a/src/PassKey.Tests/PasswordStrengthAnalyzerTests.cs +++ b/src/PassKey.Tests/PasswordStrengthAnalyzerTests.cs @@ -151,11 +151,11 @@ public void Analyze_CrackTime_ShortIsInstant() [Fact] public void Analyze_CrackTime_LongComplexIsExtreme() { - // 22 chars with all character sets → should be centuries or millennia + // 22 chars with all character sets → astronomically large, top "trillionyears" bucket var result = _analyzer.Analyze("Xy7$kQm2rPw9@NzLqJf!8Wv".AsSpan()); Assert.True( - result.EstimatedCrackTime == "centuries" || result.EstimatedCrackTime == "millennia", - $"Expected 'centuries' or 'millennia', got '{result.EstimatedCrackTime}'"); + result.EstimatedCrackTime is "billionyears" or "trillionyears", + $"Expected a high-end bucket, got '{result.EstimatedCrackTime}'"); } }