Files
keevault/MyPass/ViewModels/UnlockViewModel.swift
T
krissandClaude Sonnet 5 ee350c4c02 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
2026-09-19 16:17:17 +02:00

86 lines
2.7 KiB
Swift

//
// UnlockViewModel.swift
// MyPass
//
import Foundation
import Combine
import MyPassCore
@MainActor
final class UnlockViewModel: ObservableObject {
@Published var password: String = ""
@Published var errorMessage: String?
@Published var isUnlocking: Bool = false
@Published var showFilePicker: Bool = false
private let session: VaultSession
private let bookmarkService: FileBookmarkService
private let keychainStore: KeychainStore
private let biometricService: BiometricAuthService
private let keychainAccount = "masterPassword"
init(
session: VaultSession,
bookmarkService: FileBookmarkService = .init(),
keychainStore: KeychainStore = .init(accessGroup: "org.antiloop222.mypass"),
biometricService: BiometricAuthService = .init()
) {
self.session = session
self.bookmarkService = bookmarkService
self.keychainStore = keychainStore
self.biometricService = biometricService
}
var canUseBiometrics: Bool {
biometricService.isAvailable && keychainStore.exists(for: keychainAccount)
}
var hasVault: Bool { bookmarkService.hasBookmark }
func unlockWithBiometrics() {
Task {
isUnlocking = true
errorMessage = nil
do {
try await biometricService.authenticate(reason: "Unlock MyPass")
let storedPassword = try keychainStore.load(for: keychainAccount)
let url = try bookmarkService.resolveURL()
defer { bookmarkService.stopAccess(url: url) }
try session.unlock(url: url, password: storedPassword)
} catch {
errorMessage = error.localizedDescription
}
isUnlocking = false
}
}
func unlockWithPassword() {
Task {
isUnlocking = true
errorMessage = nil
do {
let url = try bookmarkService.resolveURL()
defer { bookmarkService.stopAccess(url: url) }
try session.unlock(url: url, password: password)
try keychainStore.save(password: password, for: keychainAccount)
password = ""
} catch {
errorMessage = error.localizedDescription
}
isUnlocking = false
}
}
func openFile(url: URL) {
do {
try bookmarkService.save(url: url)
// Don't attempt to unlock yet -- picking a file only saves the bookmark.
// The view now shows the password field for the user to enter their password.
} catch {
errorMessage = error.localizedDescription
}
}
}