- FileBookmarkService: hold a security scope while creating the bookmark, fixing "file doesn't exist" on iCloud Drive-backed vaults (NSCocoaErrorDomain Code=4), which only surfaced on a real device since the Simulator strips entitlements needed to reproduce this. - Fix Keychain access group missing the Team ID prefix, which silently broke saving the master password so Face ID was never offered after backgrounding. - Add the autofill-credential-provider entitlement to the main app target (previously only on the extension) and declare ProvidesPasswords in the extension's Info.plist, so MyPass now registers as a selectable AutoFill Passwords provider. - CredentialMatcher: fall back to a scheme-prefixed re-parse when extracting a host, since KDBX entries commonly store bare domains (e.g. "allocine.fr") that URL(string:).host can't parse without an authority component. Fixes AutoFill suggestions being unranked/wrong for such entries.
86 lines
2.7 KiB
Swift
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: "F2KB6W8N4R.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
|
|
}
|
|
}
|
|
}
|