From a8d612f7455142d5d21c379f410701b46723647b Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Sun, 20 Sep 2026 12:39:45 +0200 Subject: [PATCH] fix: resolve real-device blockers for vault open, biometrics, and AutoFill - 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. --- AutoFill/CredentialProviderViewController.swift | 2 +- AutoFill/Info.plist | 8 ++++++++ MyPass/MyPass.entitlements | 2 ++ MyPass/Services/FileBookmarkService.swift | 5 +++++ MyPass/ViewModels/UnlockViewModel.swift | 2 +- .../MyPassCore/AutoFill/CredentialMatcher.swift | 9 ++++++++- .../MyPassCoreTests/CredentialMatcherTests.swift | 15 +++++++++++++++ 7 files changed, 40 insertions(+), 3 deletions(-) diff --git a/AutoFill/CredentialProviderViewController.swift b/AutoFill/CredentialProviderViewController.swift index 7070df6..6565ca1 100644 --- a/AutoFill/CredentialProviderViewController.swift +++ b/AutoFill/CredentialProviderViewController.swift @@ -13,7 +13,7 @@ final class CredentialProviderViewController: ASCredentialProviderViewController private let session = VaultSession() private let bookmarkService = FileBookmarkService() - private let keychainStore = KeychainStore(accessGroup: "org.antiloop222.mypass") + private let keychainStore = KeychainStore(accessGroup: "F2KB6W8N4R.org.antiloop222.mypass") private let biometricService = BiometricAuthService() // Called when the user selects MyPass from the QuickType bar. diff --git a/AutoFill/Info.plist b/AutoFill/Info.plist index c0339b2..ae54af0 100644 --- a/AutoFill/Info.plist +++ b/AutoFill/Info.plist @@ -4,6 +4,14 @@ NSExtension + NSExtensionAttributes + + ASCredentialProviderExtensionCapabilities + + ProvidesPasswords + + + NSExtensionMainStoryboard MainInterface NSExtensionPointIdentifier diff --git a/MyPass/MyPass.entitlements b/MyPass/MyPass.entitlements index 38cff67..7e0ef10 100644 --- a/MyPass/MyPass.entitlements +++ b/MyPass/MyPass.entitlements @@ -2,6 +2,8 @@ + com.apple.developer.authentication-services.autofill-credential-provider + com.apple.security.application-groups group.org.antiloop222.mypass diff --git a/MyPass/Services/FileBookmarkService.swift b/MyPass/Services/FileBookmarkService.swift index c648a80..c8f2fdb 100644 --- a/MyPass/Services/FileBookmarkService.swift +++ b/MyPass/Services/FileBookmarkService.swift @@ -33,6 +33,11 @@ public final class FileBookmarkService { } public func save(url: URL) throws { + // For File Provider-backed URLs (e.g. iCloud Drive), creating a bookmark without + // an active security scope produces one that resolves later with + // NSCocoaErrorDomain Code=4 ("The file doesn't exist"), even immediately after picking. + let accessing = url.startAccessingSecurityScopedResource() + defer { if accessing { url.stopAccessingSecurityScopedResource() } } let data = try url.bookmarkData( options: Self.creationOptions, includingResourceValuesForKeys: nil, diff --git a/MyPass/ViewModels/UnlockViewModel.swift b/MyPass/ViewModels/UnlockViewModel.swift index 42209f2..aa1ac22 100644 --- a/MyPass/ViewModels/UnlockViewModel.swift +++ b/MyPass/ViewModels/UnlockViewModel.swift @@ -24,7 +24,7 @@ final class UnlockViewModel: ObservableObject { init( session: VaultSession, bookmarkService: FileBookmarkService = .init(), - keychainStore: KeychainStore = .init(accessGroup: "org.antiloop222.mypass"), + keychainStore: KeychainStore = .init(accessGroup: "F2KB6W8N4R.org.antiloop222.mypass"), biometricService: BiometricAuthService = .init() ) { self.session = session diff --git a/MyPassCore/Sources/MyPassCore/AutoFill/CredentialMatcher.swift b/MyPassCore/Sources/MyPassCore/AutoFill/CredentialMatcher.swift index 02acbf6..67b3d2e 100644 --- a/MyPassCore/Sources/MyPassCore/AutoFill/CredentialMatcher.swift +++ b/MyPassCore/Sources/MyPassCore/AutoFill/CredentialMatcher.swift @@ -25,7 +25,14 @@ public enum CredentialMatcher { } private static func host(from urlString: String) -> String? { - URL(string: urlString)?.host + // KDBX entries and AutoFill service identifiers commonly omit the scheme + // (e.g. "allocine.fr"), but URL(string:) only populates `.host` when an + // authority component ("//") is present -- without it, the whole string + // is parsed as a relative path and `.host` is nil. + if let host = URL(string: urlString)?.host, !host.isEmpty { + return host + } + return URL(string: "https://" + urlString)?.host } private static func hostsMatch(_ a: String, _ b: String) -> Bool { diff --git a/MyPassCore/Tests/MyPassCoreTests/CredentialMatcherTests.swift b/MyPassCore/Tests/MyPassCoreTests/CredentialMatcherTests.swift index 49d0fbd..123ae9c 100644 --- a/MyPassCore/Tests/MyPassCoreTests/CredentialMatcherTests.swift +++ b/MyPassCore/Tests/MyPassCoreTests/CredentialMatcherTests.swift @@ -31,6 +31,21 @@ final class CredentialMatcherTests: XCTestCase { XCTAssertFalse(CredentialMatcher.matches(entry: e, serviceIdentifier: "https://github.com")) } + func test_bareDomainSubdomainMatch() { + let e = makeEntry(url: "allocine.fr") + XCTAssertTrue(CredentialMatcher.matches(entry: e, serviceIdentifier: "mon.allocine.fr")) + } + + func test_bareDomainExactMatch() { + let e = makeEntry(url: "allocine.fr") + XCTAssertTrue(CredentialMatcher.matches(entry: e, serviceIdentifier: "allocine.fr")) + } + + func test_bareDomainDifferentDomain_noMatch() { + let e = makeEntry(url: "allocine.fr") + XCTAssertFalse(CredentialMatcher.matches(entry: e, serviceIdentifier: "notallocine.fr")) + } + func test_filter_suggestedAndRest() { let entries = [ makeEntry(url: "https://github.com"),