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):