From ee350c4c027681f13a959bb5f8eb518802afe6ab Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Sat, 19 Sep 2026 16:17:17 +0200 Subject: [PATCH] feat: add password reveal toggle, friendlier KDBX4 wrong-password message Adds an eye-icon toggle to the master password field (UnlockView) so users can verify what they typed before submitting. Also maps KDBX4's ERROR_HEADER_HASH_VERIFICATION_FAILED -- surfaced via the header HMAC check, which is password-derived -- to KDBXError.invalidPassword alongside the existing passwordAndOrKeyfileWrong check. KeePassKit uses this same error code for both a wrong password/keyfile and genuine file corruption (it doesn't distinguish the two), so the message hedges: "Incorrect password, or this vault file is corrupted." Found while testing against a real KDBX4 vault, which was surfacing this as a raw, unreadable NSError string before the previous commit's LocalizedError fix, and as *no* friendly message at all before this one (the error code wasn't in the recognized set yet). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf --- MyPass/ViewModels/UnlockViewModel.swift | 2 - MyPass/Views/UnlockView.swift | 43 +++++++++++++++++-- .../MyPassCore/KDBX/KDBXDocument.swift | 10 ++++- .../Sources/MyPassCore/KDBX/KDBXError.swift | 2 +- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/MyPass/ViewModels/UnlockViewModel.swift b/MyPass/ViewModels/UnlockViewModel.swift index 0bce4a2..42209f2 100644 --- a/MyPass/ViewModels/UnlockViewModel.swift +++ b/MyPass/ViewModels/UnlockViewModel.swift @@ -66,8 +66,6 @@ final class UnlockViewModel: ObservableObject { try session.unlock(url: url, password: password) try keychainStore.save(password: password, for: keychainAccount) password = "" - } catch KDBXError.invalidPassword { - errorMessage = "Incorrect password." } catch { errorMessage = error.localizedDescription } diff --git a/MyPass/Views/UnlockView.swift b/MyPass/Views/UnlockView.swift index d8a2124..8ce357a 100644 --- a/MyPass/Views/UnlockView.swift +++ b/MyPass/Views/UnlockView.swift @@ -59,9 +59,7 @@ struct UnlockView: View { .foregroundStyle(.secondary) } - SecureField("Master Password", text: $vm.password) - .textFieldStyle(.roundedBorder) - .onSubmit(vm.unlockWithPassword) + PasswordField(text: $vm.password, onSubmit: vm.unlockWithPassword) Button("Unlock", action: vm.unlockWithPassword) .buttonStyle(.bordered) @@ -87,3 +85,42 @@ struct UnlockView: View { } } } + +/// A password field with a trailing eye button to reveal/hide the typed value. +private struct PasswordField: View { + @Binding var text: String + var onSubmit: () -> Void + + @State private var isRevealed = false + + var body: some View { + HStack { + Group { + if isRevealed { + TextField("Master Password", text: $text) + } else { + SecureField("Master Password", text: $text) + } + } + .textFieldStyle(.plain) + .autocorrectionDisabled() + #if os(iOS) + .textInputAutocapitalization(.never) + #endif + .onSubmit(onSubmit) + + Button { + isRevealed.toggle() + } label: { + Image(systemName: isRevealed ? "eye.slash" : "eye") + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + } + .padding(8) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(Color.secondary.opacity(0.3), lineWidth: 1) + ) + } +} diff --git a/MyPassCore/Sources/MyPassCore/KDBX/KDBXDocument.swift b/MyPassCore/Sources/MyPassCore/KDBX/KDBXDocument.swift index 693a55f..e2a2477 100644 --- a/MyPassCore/Sources/MyPassCore/KDBX/KDBXDocument.swift +++ b/MyPassCore/Sources/MyPassCore/KDBX/KDBXDocument.swift @@ -18,7 +18,15 @@ public struct KDBXDocument { let tree = try KPKTree(contentsOf: url, key: key) return KDBXMapper.database(from: tree) } catch let err as NSError { - if err.domain == KPKErrorDomain && err.code == KPKErrorCode.passwordAndOrKeyfileWrong.rawValue { + let wrongKeyCodes: Set = [ + Int(KPKErrorCode.passwordAndOrKeyfileWrong.rawValue), + // KDBX4's header HMAC check (which is password-derived) uses this same + // error code for both a wrong password/keyfile and true file corruption -- + // KeePassKit doesn't distinguish the two. Wrong password is by far the + // more common cause, so we surface it as such. + Int(KPKErrorCode.kdbxHeaderHashVerificationFailed.rawValue), + ] + if err.domain == KPKErrorDomain && wrongKeyCodes.contains(err.code) { throw KDBXError.invalidPassword } throw KDBXError.parseError(err.localizedDescription) diff --git a/MyPassCore/Sources/MyPassCore/KDBX/KDBXError.swift b/MyPassCore/Sources/MyPassCore/KDBX/KDBXError.swift index 5d4c079..2f13e5f 100644 --- a/MyPassCore/Sources/MyPassCore/KDBX/KDBXError.swift +++ b/MyPassCore/Sources/MyPassCore/KDBX/KDBXError.swift @@ -11,7 +11,7 @@ extension KDBXError: LocalizedError { public var errorDescription: String? { switch self { case .invalidPassword: - return "Incorrect password." + return "Incorrect password, or this vault file is corrupted." case .fileNotFound: return "The vault file could not be found." case .parseError(let message):