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 @@
NonefalsePassKey.Desktop
- 1.0.17
- 1.0.17.0
- 1.0.17.0
+ 2.0.0
+ 2.0.0.0
+ 2.0.0.0app.manifestAssets\PassKey.icoDISABLE_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
-
+
LandInhalt
-
+
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ügenAbbrechen
@@ -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
-
+
CountryContent
-
+
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 passwordTitle
@@ -437,6 +437,15 @@
Password *
+
+ Required field
+
+
+ Required field
+
+
+ Required field
+
Notes
@@ -465,8 +474,11 @@
Search cards...
+
+ Search identities...
+
- New card
+ Add cardLabel
@@ -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ísContenido
-
+
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ñaTí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 tarjetaEtiqueta
@@ -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
-
+