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
This commit is contained in:
@@ -66,8 +66,6 @@ final class UnlockViewModel: ObservableObject {
|
|||||||
try session.unlock(url: url, password: password)
|
try session.unlock(url: url, password: password)
|
||||||
try keychainStore.save(password: password, for: keychainAccount)
|
try keychainStore.save(password: password, for: keychainAccount)
|
||||||
password = ""
|
password = ""
|
||||||
} catch KDBXError.invalidPassword {
|
|
||||||
errorMessage = "Incorrect password."
|
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,9 +59,7 @@ struct UnlockView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureField("Master Password", text: $vm.password)
|
PasswordField(text: $vm.password, onSubmit: vm.unlockWithPassword)
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
.onSubmit(vm.unlockWithPassword)
|
|
||||||
|
|
||||||
Button("Unlock", action: vm.unlockWithPassword)
|
Button("Unlock", action: vm.unlockWithPassword)
|
||||||
.buttonStyle(.bordered)
|
.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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,15 @@ public struct KDBXDocument {
|
|||||||
let tree = try KPKTree(contentsOf: url, key: key)
|
let tree = try KPKTree(contentsOf: url, key: key)
|
||||||
return KDBXMapper.database(from: tree)
|
return KDBXMapper.database(from: tree)
|
||||||
} catch let err as NSError {
|
} catch let err as NSError {
|
||||||
if err.domain == KPKErrorDomain && err.code == KPKErrorCode.passwordAndOrKeyfileWrong.rawValue {
|
let wrongKeyCodes: Set<Int> = [
|
||||||
|
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.invalidPassword
|
||||||
}
|
}
|
||||||
throw KDBXError.parseError(err.localizedDescription)
|
throw KDBXError.parseError(err.localizedDescription)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ extension KDBXError: LocalizedError {
|
|||||||
public var errorDescription: String? {
|
public var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .invalidPassword:
|
case .invalidPassword:
|
||||||
return "Incorrect password."
|
return "Incorrect password, or this vault file is corrupted."
|
||||||
case .fileNotFound:
|
case .fileNotFound:
|
||||||
return "The vault file could not be found."
|
return "The vault file could not be found."
|
||||||
case .parseError(let message):
|
case .parseError(let message):
|
||||||
|
|||||||
Reference in New Issue
Block a user