Compare commits

...
10 Commits
Author SHA1 Message Date
kriss b6d113edcd feat: switch accent to Plum, finalize vault-door app icon, group filter Clear button
- Replace Copper accent with Plum (light #6B4577 / dark #A67FB0) across the main
  app and AutoFill extension.
- Replace the padlock app icon with a vault-door design (ring, hub, locking
  handle) in Plum, plus all macOS sizes.
- Add an AppIconImage asset (separate from the AppIcon icon-set, which SwiftUI
  can't reference directly at runtime) and show it on the unlock screen instead
  of a generic SF Symbol.
- GroupFilterView: move "Clear Filter" out of the list and into a leading
  toolbar button ("Clear", disabled when no filter is active), mirroring the
  trailing "Done" button.
2026-09-20 18:25:41 +02:00
kriss 8bba64076f feat: copper accent theme, always-expanded group filter, app icon; rename MyPass to KeeVault
- Custom copper AccentColor and warm VaultBackground color assets (light/dark),
  applied to both the main app and the AutoFill extension (which needed its own
  copy of the asset catalog plus ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME
  and an explicit .tint() modifier, since extensions don't pick up the app-wide
  accent color the same way a full SwiftUI App/Scene does).
- GroupFilterView no longer uses DisclosureGroup for subgroups -- the whole tree
  renders always-expanded (indented per depth) so active filters are visible at
  a glance instead of requiring tap-to-expand.
- Added a placeholder app icon (padlock glyph, light/dark/tinted variants plus
  macOS sizes) -- previously the icon slot existed but had no actual images,
  which blocks App Store/TestFlight archive validation.
- Added ITSAppUsesNonExemptEncryption = NO (standard encryption only, via
  KeePassKit) to skip the export-compliance prompt on each upload.
- Renamed the app from MyPass to KeeVault throughout: Xcode project/targets/
  schemes, the MyPassCore package (now KeeVaultCore) and every import site,
  folder and file names, bundle identifiers (org.antiloop222.keevault) and
  their App Group/Keychain-group entitlements, and remaining UI/string
  references -- MyPass was already taken as an App Store app name.
2026-09-20 15:05:19 +02:00
kriss a8d612f745 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.
2026-09-20 12:39:45 +02:00
krissandClaude Sonnet 5 5a59867d48 fix: align AutoFill keychain-access-group with main app, lowercase bundle IDs
Root-caused the "MyPass doesn't appear in Settings > AutoFill Passwords"
issue: entitlements embed correctly on a real device build but are
silently stripped to empty on every Simulator build in this
environment (confirmed identically via CLI xcodebuild and Xcode's own
GUI build/run, before and after multiple rounds of removing/re-adding
capabilities in Signing & Capabilities). This is a Simulator/SDK-level
limitation in this environment, not a project misconfiguration --
verified by comparing against a real iphoneos device build, which
embeds the full App Group / Keychain Sharing / AutoFill Credential
Provider entitlements correctly.

While diagnosing, found and fixed a real, separate bug: AutoFill's
Keychain Sharing group had been set (via Xcode's capability re-add
flow, which defaults to the target's own bundle ID) to
org.antiloop222.mypass.autofill, while the main app uses
org.antiloop222.mypass -- a mismatch that would have prevented the
extension from ever reading the shared master password from Keychain,
even on a real device where entitlements embed correctly. Now aligned
to org.antiloop222.mypass on both targets, matching what
KeychainStore's hardcoded accessGroup expects in both AutoFill/
CredentialProviderViewController.swift and MyPass/ViewModels/
UnlockViewModel.swift.

Also includes the user's own Signing & Capabilities re-add (repopulated
App Group selections) and a bundle-identifier lowercase normalization
(org.antiloop222.MyPass -> org.antiloop222.mypass, matching the
already-lowercase App Group/Keychain group strings) made via Xcode's
GUI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
2026-09-19 22:03:34 +02:00
krissandClaude Sonnet 5 dfb728e4ab chore: explicitly require entitlements for MyPass and AutoFill targets
Sets ENTITLEMENTS_REQUIRED = YES for both targets' build configurations.
Found while investigating why Settings > AutoFill & Passwords doesn't
list MyPass: Xcode's automatic-signing build step (ProcessProductPackaging)
silently produces an EMPTY entitlements blob for both the AutoFill
extension and the main app on this environment's Simulator builds,
despite AutoFill.entitlements/MyPass.entitlements being valid and
correctly referenced (verified with plutil, and confirmed manually
invoking codesign with the same file embeds the entitlements
correctly). This setting alone doesn't fix the underlying issue --
still investigating -- but is a correct, harmless hardening on its own
(surfaces an error if entitlements are ever genuinely missing, instead
of silently signing without them).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
2026-09-19 19:49:45 +02:00
krissandClaude Sonnet 5 5281ce4895 fix: address AutoFill extension code review findings
- Remove dead provideCredentialWithoutUserInteraction override and
  unlockSilently() helper (nothing registers credential identities,
  so it could never run, and it was a footgun for future inline
  QuickType work).
- Surface non-cancellation biometric unlock failures into
  errorMessage instead of swallowing them, matching
  UnlockViewModel.unlockWithBiometrics().
- Forward VaultSession.objectWillChange into ExtensionViewModel via
  Combine so the lock/unlock UI transition no longer depends on an
  accidental isUnlocking side effect, matching VaultViewModel's
  pattern.
- Show the "Open MyPass" deep-link escape hatch whenever unlock
  fails (errorMessage set), not only when there's no bookmark yet,
  per the extension constraints spec.
- Add INFOPLIST_KEY_NSFaceIDUsageDescription to the MyPass and
  AutoFill targets' Debug/Release build configs.

Also folds in pre-existing alphabetical reordering of two
PBXBuildFile/PBXFileReference entries in project.pbxproj from an
earlier task, since this same file is already being touched here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
2026-09-19 19:24:35 +02:00
krissandClaude Haiku 4.5 e52cf3f39b feat: add CredentialListView for AutoFill extension
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
2026-09-19 19:15:16 +02:00
krissandClaude Sonnet 5 a70ea40acd feat: add ExtensionRootView and ExtensionViewModel
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
2026-09-19 19:12:49 +02:00
krissandClaude Sonnet 5 3ee729070a feat: add CredentialProviderViewController skeleton, strip unused storyboard UI
Replace the Xcode boilerplate in AutoFill/CredentialProviderViewController.swift
with real logic: prepareCredentialList(for:) builds ExtensionRootView (added in
Task 19) inside a UIHostingController, provideCredentialWithoutUserInteraction(for:)
silently unlocks via the shared Keychain token and completes/cancels the request.
Strip the storyboard's now-unused static nav-bar/button/actions, keeping the same
customClass so the system still instantiates this exact class.

Also add AutoFill target membership for MyPass/Services/FileBookmarkService.swift
and BiometricAuthService.swift (app-target files, referenced directly by the
extension per the plan) via explicit PBXFileReference/PBXBuildFile entries, since
Xcode's synchronized-group file system only auto-includes them in the MyPass
target by default.

Expected build state: exactly one error, "cannot find 'ExtensionRootView' in
scope" (reported once per simulator architecture), until Task 19 adds that type.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
2026-09-19 19:08:54 +02:00
krissandClaude Sonnet 5 da5ad145b0 fix: make search bar always visible instead of toggle-behind-icon
Removed the magnifying-glass toolbar button and the isSearching state
it drove -- .searchable() now shows the search field unconditionally,
which is simpler and was requested after hands-on use showed the extra
tap added no value. Updated the spec to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
2026-09-19 19:08:36 +02:00
80 changed files with 750 additions and 272 deletions
@@ -0,0 +1,38 @@
{
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.420",
"green" : "0.271",
"blue" : "0.467",
"alpha" : "1.000"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.651",
"green" : "0.498",
"blue" : "0.690",
"alpha" : "1.000"
}
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,38 @@
{
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.980",
"green" : "0.957",
"blue" : "0.925",
"alpha" : "1.000"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.141",
"green" : "0.125",
"blue" : "0.094",
"alpha" : "1.000"
}
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
+12 -12
View File
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.antiloop222.mypass</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)org.antiloop222.mypass</string>
</array>
</dict>
<dict>
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.antiloop222.keevault</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)org.antiloop222.keevault</string>
</array>
</dict>
</plist>
@@ -17,36 +17,7 @@
<view key="view" contentMode="scaleToFill" id="BuU-Ak-iZz">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3wq-kG-lGu">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<items>
<navigationItem id="cbj-pk-SYj">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="bEZ-MG-jDy">
<connections>
<action selector="cancel:" destination="Xki-Si-B7m" id="6ap-3Q-iEX"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
</navigationBar>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="a7v-ug-QzG">
<rect key="frame" x="87.5" y="327" width="199" height="33"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<state key="normal" title="Return Example Password"/>
<connections>
<action selector="passwordSelected:" destination="Xki-Si-B7m" eventType="touchUpInside" id="ODd-lr-mud"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="Ky8-vK-JVj" firstAttribute="top" secondItem="3wq-kG-lGu" secondAttribute="top" id="BIN-jb-uNd"/>
<constraint firstItem="3wq-kG-lGu" firstAttribute="width" secondItem="BuU-Ak-iZz" secondAttribute="width" id="UkD-v4-BcH"/>
<constraint firstItem="a7v-ug-QzG" firstAttribute="centerY" secondItem="Ky8-vK-JVj" secondAttribute="centerY" id="fAC-0v-NFE"/>
<constraint firstItem="a7v-ug-QzG" firstAttribute="centerX" secondItem="3wq-kG-lGu" secondAttribute="centerX" id="io1-ZS-gwn"/>
<constraint firstItem="3wq-kG-lGu" firstAttribute="centerX" secondItem="BuU-Ak-iZz" secondAttribute="centerX" id="rtV-5c-0bl"/>
</constraints>
<viewLayoutGuide key="safeArea" id="Ky8-vK-JVj"/>
</view>
</viewController>
+35 -43
View File
@@ -6,53 +6,45 @@
//
import AuthenticationServices
import SwiftUI
import KeeVaultCore
class CredentialProviderViewController: ASCredentialProviderViewController {
final class CredentialProviderViewController: ASCredentialProviderViewController {
/*
Prepare your UI to list available credentials for the user to choose from. The items in
'serviceIdentifiers' describe the service the user is logging in to, so your extension can
prioritize the most relevant credentials in the list.
*/
private let session = VaultSession()
private let bookmarkService = FileBookmarkService()
private let keychainStore = KeychainStore(accessGroup: "F2KB6W8N4R.org.antiloop222.keevault")
private let biometricService = BiometricAuthService()
// Called when the user selects KeeVault from the QuickType bar.
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
let ids = serviceIdentifiers.map(\.identifier)
showUI(serviceIdentifiers: ids)
}
/*
Implement this method if your extension supports showing credentials in the QuickType bar.
When the user selects a credential from your app, this method will be called with the
ASPasswordCredentialIdentity your app has previously saved to the ASCredentialIdentityStore.
Provide the password by completing the extension request with the associated ASPasswordCredential.
If using the credential would require showing custom UI for authenticating the user, cancel
the request with error code ASExtensionError.userInteractionRequired.
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
let databaseIsUnlocked = true
if (databaseIsUnlocked) {
let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
} else {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue))
}
private func showUI(serviceIdentifiers: [String]) {
let rootView = ExtensionRootView(
session: session,
serviceIdentifiers: serviceIdentifiers,
bookmarkService: bookmarkService,
keychainStore: keychainStore,
biometricService: biometricService,
onSelect: { [weak self] (entry: Entry) in
let credential = ASPasswordCredential(
user: entry.username,
password: entry.password.reveal()
)
self?.extensionContext.completeRequest(withSelectedCredential: credential, completionHandler: nil)
},
onCancel: { [weak self] in
self?.extensionContext.cancelRequest(withError: ASExtensionError(.userCanceled))
}
)
let host = UIHostingController(rootView: rootView)
addChild(host)
view.addSubview(host.view)
host.view.frame = view.bounds
host.view.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
host.didMove(toParent: self)
}
*/
/*
Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
ASExtensionError.userInteractionRequired. In this case, the system may present your extension's
UI and call this method. Show appropriate UI for authenticating the user then provide the password
by completing the extension request with the associated ASPasswordCredential.
override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
}
*/
@IBAction func cancel(_ sender: AnyObject?) {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
}
@IBAction func passwordSelected(_ sender: AnyObject?) {
let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
}
}
+8
View File
@@ -4,6 +4,14 @@
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>ASCredentialProviderExtensionCapabilities</key>
<dict>
<key>ProvidesPasswords</key>
<true/>
</dict>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
+64
View File
@@ -0,0 +1,64 @@
// AutoFill/Views/CredentialListView.swift
import SwiftUI
import KeeVaultCore
struct CredentialListView: View {
@ObservedObject var vm: ExtensionViewModel
@State private var searchQuery: String = ""
private var displayedSuggested: [Entry] {
searchQuery.isEmpty ? vm.suggestedEntries : []
}
private var displayedAll: [Entry] {
let entries = searchQuery.isEmpty ? vm.allEntries : vm.session.allEntries()
guard !searchQuery.isEmpty else { return entries }
let q = searchQuery.lowercased()
return entries.filter {
$0.title.lowercased().contains(q)
|| $0.username.lowercased().contains(q)
|| $0.url.lowercased().contains(q)
}
}
var body: some View {
List {
if !displayedSuggested.isEmpty {
Section("Suggested") {
ForEach(displayedSuggested) { entry in
CredentialRow(entry: entry) { vm.select(entry) }
}
}
}
Section(displayedSuggested.isEmpty ? "" : "All Entries") {
if displayedAll.isEmpty {
Text("No entries found").foregroundStyle(.secondary)
} else {
ForEach(displayedAll) { entry in
CredentialRow(entry: entry) { vm.select(entry) }
}
}
}
}
.scrollContentBackground(.hidden)
.background(Color("VaultBackground"))
.searchable(text: $searchQuery, prompt: "Search…")
}
}
private struct CredentialRow: View {
let entry: Entry
let onSelect: () -> Void
var body: some View {
Button(action: onSelect) {
VStack(alignment: .leading, spacing: 2) {
Text(entry.title).font(.body).foregroundStyle(.primary)
Text(entry.username).font(.caption).foregroundStyle(.secondary)
if !entry.url.isEmpty {
Text(entry.url).font(.caption2).foregroundStyle(.tertiary)
}
}
}
}
}
+174
View File
@@ -0,0 +1,174 @@
// AutoFill/Views/ExtensionUnlockView.swift
import SwiftUI
import Combine
import KeeVaultCore
/// Root view for the extension -- shows unlock screen or credential list depending on state.
struct ExtensionRootView: View {
@StateObject private var vm: ExtensionViewModel
init(
session: VaultSession,
serviceIdentifiers: [String],
bookmarkService: FileBookmarkService,
keychainStore: KeychainStore,
biometricService: BiometricAuthService,
onSelect: @escaping (Entry) -> Void,
onCancel: @escaping () -> Void
) {
_vm = StateObject(wrappedValue: ExtensionViewModel(
session: session,
serviceIdentifiers: serviceIdentifiers,
bookmarkService: bookmarkService,
keychainStore: keychainStore,
biometricService: biometricService,
onSelect: onSelect,
onCancel: onCancel
))
}
var body: some View {
NavigationStack {
SwiftUI.Group {
if vm.isLocked {
extensionUnlockView
} else {
CredentialListView(vm: vm)
}
}
.background(Color("VaultBackground"))
.navigationTitle("KeeVault")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: vm.cancel)
}
}
}
.tint(Color("AccentColor"))
.task { await vm.tryBiometricUnlock() }
}
private var extensionUnlockView: some View {
VStack(spacing: 24) {
Spacer()
Image(systemName: "lock.shield.fill").font(.system(size: 48)).foregroundStyle(.tint)
Text("Vault Locked").font(.headline)
if vm.canUseBiometrics {
Button(action: { Task { await vm.tryBiometricUnlock() } }) {
Label("Use Face ID / Touch ID", systemImage: "faceid")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
}
SecureField("Master Password", text: $vm.password)
.textFieldStyle(.roundedBorder)
.onSubmit { Task { await vm.unlockWithPassword() } }
Button("Unlock") { Task { await vm.unlockWithPassword() } }
.buttonStyle(.bordered)
.disabled(vm.password.isEmpty || vm.isUnlocking)
if let msg = vm.errorMessage {
Text(msg).foregroundStyle(.red).font(.caption).multilineTextAlignment(.center)
}
if !vm.hasVault || vm.errorMessage != nil {
Link(destination: URL(string: "keevault://unlock")!) {
Label("Open KeeVault to set up vault", systemImage: "arrow.up.right")
.font(.caption)
}
}
Spacer()
}
.padding(24)
}
}
@MainActor
final class ExtensionViewModel: ObservableObject {
@Published var password: String = ""
@Published var errorMessage: String?
@Published var isUnlocking: Bool = false
let session: VaultSession
let serviceIdentifiers: [String]
private let bookmarkService: FileBookmarkService
private let keychainStore: KeychainStore
private let biometricService: BiometricAuthService
private let onSelect: (Entry) -> Void
private let onCancel: () -> Void
private var sessionCancellable: AnyCancellable?
init(
session: VaultSession,
serviceIdentifiers: [String],
bookmarkService: FileBookmarkService,
keychainStore: KeychainStore,
biometricService: BiometricAuthService,
onSelect: @escaping (Entry) -> Void,
onCancel: @escaping () -> Void
) {
self.session = session
self.serviceIdentifiers = serviceIdentifiers
self.bookmarkService = bookmarkService
self.keychainStore = keychainStore
self.biometricService = biometricService
self.onSelect = onSelect
self.onCancel = onCancel
sessionCancellable = session.objectWillChange.sink { [weak self] in
self?.objectWillChange.send()
}
}
var isLocked: Bool { session.isLocked }
var hasVault: Bool { bookmarkService.hasBookmark }
var canUseBiometrics: Bool { biometricService.isAvailable && keychainStore.exists(for: "masterPassword") }
var suggestedEntries: [Entry] {
CredentialMatcher.filter(entries: session.allEntries(), for: serviceIdentifiers).suggested
}
var allEntries: [Entry] {
CredentialMatcher.filter(entries: session.allEntries(), for: serviceIdentifiers).all
}
func select(_ entry: Entry) { onSelect(entry) }
func cancel() { onCancel() }
func tryBiometricUnlock() async {
guard canUseBiometrics, session.isLocked else { return }
isUnlocking = true
do {
try await biometricService.authenticate(reason: "Unlock KeeVault")
try performUnlockFromKeychain()
} catch {
errorMessage = error.localizedDescription
}
isUnlocking = false
}
func unlockWithPassword() async {
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: "masterPassword")
password = ""
} catch {
errorMessage = error.localizedDescription
}
isUnlocking = false
}
private func performUnlockFromKeychain() throws {
let pw = try keychainStore.load(for: "masterPassword")
let url = try bookmarkService.resolveURL()
defer { bookmarkService.stopAccess(url: url) }
try session.unlock(url: url, password: pw)
}
}
@@ -7,10 +7,12 @@
objects = {
/* Begin PBXBuildFile section */
205678DB2FC0EB5200251C6B /* MyPassCore in Frameworks */ = {isa = PBXBuildFile; productRef = 205678DA2FC0EB5200251C6B /* MyPassCore */; };
205678DB2FC0EB5200251C6B /* KeeVaultCore in Frameworks */ = {isa = PBXBuildFile; productRef = 205678DA2FC0EB5200251C6B /* KeeVaultCore */; };
2096B432305EC2B200C2F253 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 205678E32FC0F52A00251C6B /* AuthenticationServices.framework */; };
2096B43D305EC2B200C2F253 /* AutoFill.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2096B431305EC2B200C2F253 /* AutoFill.appex */; platformFilters = (ios, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
67DE2FCFC21FD6DF5E761C87 /* MyPassCore in Frameworks */ = {isa = PBXBuildFile; productRef = 205678DA2FC0EB5200251C6B /* MyPassCore */; };
2096B43D305EC2B200C2F253 /* AutoFill.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2096B431305EC2B200C2F253 /* AutoFill.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
67DE2FCFC21FD6DF5E761C87 /* KeeVaultCore in Frameworks */ = {isa = PBXBuildFile; productRef = 205678DA2FC0EB5200251C6B /* KeeVaultCore */; };
982CE38F924E4FD387BC2C20 /* BiometricAuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7DFA4A59640D7BF6B4960 /* BiometricAuthService.swift */; };
EABAC2B148F845C8A20EC2CC /* FileBookmarkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBBB9F1BA62473294D2B5B5 /* FileBookmarkService.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -19,14 +21,14 @@
containerPortal = 2056788B2FBF9CBB00251C6B /* Project object */;
proxyType = 1;
remoteGlobalIDString = 205678922FBF9CBB00251C6B;
remoteInfo = MyPass;
remoteInfo = KeeVault;
};
205678AD2FBF9CBC00251C6B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 2056788B2FBF9CBB00251C6B /* Project object */;
proxyType = 1;
remoteGlobalIDString = 205678922FBF9CBB00251C6B;
remoteInfo = MyPass;
remoteInfo = KeeVault;
};
2096B43B305EC2B200C2F253 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
@@ -52,11 +54,13 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
205678932FBF9CBB00251C6B /* MyPass.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyPass.app; sourceTree = BUILT_PRODUCTS_DIR; };
205678A22FBF9CBC00251C6B /* MyPassTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyPassTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
205678AC2FBF9CBC00251C6B /* MyPassUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyPassUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
205678932FBF9CBB00251C6B /* KeeVault.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeeVault.app; sourceTree = BUILT_PRODUCTS_DIR; };
205678A22FBF9CBC00251C6B /* KeeVaultTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KeeVaultTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
205678AC2FBF9CBC00251C6B /* KeeVaultUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KeeVaultUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
205678E32FC0F52A00251C6B /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; };
2096B431305EC2B200C2F253 /* AutoFill.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = AutoFill.appex; sourceTree = BUILT_PRODUCTS_DIR; };
ADBBB9F1BA62473294D2B5B5 /* FileBookmarkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FileBookmarkService.swift; path = KeeVault/Services/FileBookmarkService.swift; sourceTree = SOURCE_ROOT; };
D6C7DFA4A59640D7BF6B4960 /* BiometricAuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BiometricAuthService.swift; path = KeeVault/Services/BiometricAuthService.swift; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -70,19 +74,19 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
205678952FBF9CBB00251C6B /* MyPass */ = {
205678952FBF9CBB00251C6B /* KeeVault */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = MyPass;
path = KeeVault;
sourceTree = "<group>";
};
205678A52FBF9CBC00251C6B /* MyPassTests */ = {
205678A52FBF9CBC00251C6B /* KeeVaultTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = MyPassTests;
path = KeeVaultTests;
sourceTree = "<group>";
};
205678AF2FBF9CBC00251C6B /* MyPassUITests */ = {
205678AF2FBF9CBC00251C6B /* KeeVaultUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = MyPassUITests;
path = KeeVaultUITests;
sourceTree = "<group>";
};
2096B433305EC2B200C2F253 /* AutoFill */ = {
@@ -100,7 +104,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
205678DB2FC0EB5200251C6B /* MyPassCore in Frameworks */,
205678DB2FC0EB5200251C6B /* KeeVaultCore in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -123,20 +127,30 @@
buildActionMask = 2147483647;
files = (
2096B432305EC2B200C2F253 /* AuthenticationServices.framework in Frameworks */,
67DE2FCFC21FD6DF5E761C87 /* MyPassCore in Frameworks */,
67DE2FCFC21FD6DF5E761C87 /* KeeVaultCore in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
1A2B3C4D5E6F7A8B9C0D1E2F /* AutoFill Shared Services */ = {
isa = PBXGroup;
children = (
ADBBB9F1BA62473294D2B5B5 /* FileBookmarkService.swift */,
D6C7DFA4A59640D7BF6B4960 /* BiometricAuthService.swift */,
);
name = "AutoFill Shared Services";
sourceTree = "<group>";
};
2056788A2FBF9CBB00251C6B = {
isa = PBXGroup;
children = (
205678952FBF9CBB00251C6B /* MyPass */,
205678A52FBF9CBC00251C6B /* MyPassTests */,
205678AF2FBF9CBC00251C6B /* MyPassUITests */,
205678952FBF9CBB00251C6B /* KeeVault */,
205678A52FBF9CBC00251C6B /* KeeVaultTests */,
205678AF2FBF9CBC00251C6B /* KeeVaultUITests */,
2096B433305EC2B200C2F253 /* AutoFill */,
1A2B3C4D5E6F7A8B9C0D1E2F /* AutoFill Shared Services */,
205678E22FC0F52A00251C6B /* Frameworks */,
205678942FBF9CBB00251C6B /* Products */,
);
@@ -145,9 +159,9 @@
205678942FBF9CBB00251C6B /* Products */ = {
isa = PBXGroup;
children = (
205678932FBF9CBB00251C6B /* MyPass.app */,
205678A22FBF9CBC00251C6B /* MyPassTests.xctest */,
205678AC2FBF9CBC00251C6B /* MyPassUITests.xctest */,
205678932FBF9CBB00251C6B /* KeeVault.app */,
205678A22FBF9CBC00251C6B /* KeeVaultTests.xctest */,
205678AC2FBF9CBC00251C6B /* KeeVaultUITests.xctest */,
2096B431305EC2B200C2F253 /* AutoFill.appex */,
);
name = Products;
@@ -164,9 +178,9 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
205678922FBF9CBB00251C6B /* MyPass */ = {
205678922FBF9CBB00251C6B /* KeeVault */ = {
isa = PBXNativeTarget;
buildConfigurationList = 205678B62FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "MyPass" */;
buildConfigurationList = 205678B62FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "KeeVault" */;
buildPhases = (
2056788F2FBF9CBB00251C6B /* Sources */,
205678902FBF9CBB00251C6B /* Frameworks */,
@@ -179,19 +193,19 @@
2096B43C305EC2B200C2F253 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
205678952FBF9CBB00251C6B /* MyPass */,
205678952FBF9CBB00251C6B /* KeeVault */,
);
name = MyPass;
name = KeeVault;
packageProductDependencies = (
205678DA2FC0EB5200251C6B /* MyPassCore */,
205678DA2FC0EB5200251C6B /* KeeVaultCore */,
);
productName = MyPass;
productReference = 205678932FBF9CBB00251C6B /* MyPass.app */;
productName = KeeVault;
productReference = 205678932FBF9CBB00251C6B /* KeeVault.app */;
productType = "com.apple.product-type.application";
};
205678A12FBF9CBC00251C6B /* MyPassTests */ = {
205678A12FBF9CBC00251C6B /* KeeVaultTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 205678B92FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "MyPassTests" */;
buildConfigurationList = 205678B92FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "KeeVaultTests" */;
buildPhases = (
2056789E2FBF9CBC00251C6B /* Sources */,
2056789F2FBF9CBC00251C6B /* Frameworks */,
@@ -203,18 +217,18 @@
205678A42FBF9CBC00251C6B /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
205678A52FBF9CBC00251C6B /* MyPassTests */,
205678A52FBF9CBC00251C6B /* KeeVaultTests */,
);
name = MyPassTests;
name = KeeVaultTests;
packageProductDependencies = (
);
productName = MyPassTests;
productReference = 205678A22FBF9CBC00251C6B /* MyPassTests.xctest */;
productName = KeeVaultTests;
productReference = 205678A22FBF9CBC00251C6B /* KeeVaultTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
205678AB2FBF9CBC00251C6B /* MyPassUITests */ = {
205678AB2FBF9CBC00251C6B /* KeeVaultUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 205678BC2FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "MyPassUITests" */;
buildConfigurationList = 205678BC2FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "KeeVaultUITests" */;
buildPhases = (
205678A82FBF9CBC00251C6B /* Sources */,
205678A92FBF9CBC00251C6B /* Frameworks */,
@@ -226,13 +240,13 @@
205678AE2FBF9CBC00251C6B /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
205678AF2FBF9CBC00251C6B /* MyPassUITests */,
205678AF2FBF9CBC00251C6B /* KeeVaultUITests */,
);
name = MyPassUITests;
name = KeeVaultUITests;
packageProductDependencies = (
);
productName = MyPassUITests;
productReference = 205678AC2FBF9CBC00251C6B /* MyPassUITests.xctest */;
productName = KeeVaultUITests;
productReference = 205678AC2FBF9CBC00251C6B /* KeeVaultUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
2096B430305EC2B200C2F253 /* AutoFill */ = {
@@ -252,7 +266,7 @@
);
name = AutoFill;
packageProductDependencies = (
205678DA2FC0EB5200251C6B /* MyPassCore */,
205678DA2FC0EB5200251C6B /* KeeVaultCore */,
);
productName = AutoFill;
productReference = 2096B431305EC2B200C2F253 /* AutoFill.appex */;
@@ -266,7 +280,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2660;
LastUpgradeCheck = 2650;
LastUpgradeCheck = 2660;
TargetAttributes = {
205678922FBF9CBB00251C6B = {
CreatedOnToolsVersion = 26.5;
@@ -284,7 +298,7 @@
};
};
};
buildConfigurationList = 2056788E2FBF9CBB00251C6B /* Build configuration list for PBXProject "MyPass" */;
buildConfigurationList = 2056788E2FBF9CBB00251C6B /* Build configuration list for PBXProject "KeeVault" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
@@ -294,16 +308,16 @@
mainGroup = 2056788A2FBF9CBB00251C6B;
minimizedProjectReferenceProxies = 1;
packageReferences = (
205678D92FC0EB5200251C6B /* XCLocalSwiftPackageReference "MyPassCore" */,
205678D92FC0EB5200251C6B /* XCLocalSwiftPackageReference "KeeVaultCore" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 205678942FBF9CBB00251C6B /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
205678922FBF9CBB00251C6B /* MyPass */,
205678A12FBF9CBC00251C6B /* MyPassTests */,
205678AB2FBF9CBC00251C6B /* MyPassUITests */,
205678922FBF9CBB00251C6B /* KeeVault */,
205678A12FBF9CBC00251C6B /* KeeVaultTests */,
205678AB2FBF9CBC00251C6B /* KeeVaultUITests */,
2096B430305EC2B200C2F253 /* AutoFill */,
);
};
@@ -366,6 +380,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
EABAC2B148F845C8A20EC2CC /* FileBookmarkService.swift in Sources */,
982CE38F924E4FD387BC2C20 /* BiometricAuthService.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -374,17 +390,17 @@
/* Begin PBXTargetDependency section */
205678A42FBF9CBC00251C6B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 205678922FBF9CBB00251C6B /* MyPass */;
target = 205678922FBF9CBB00251C6B /* KeeVault */;
targetProxy = 205678A32FBF9CBC00251C6B /* PBXContainerItemProxy */;
};
205678AE2FBF9CBC00251C6B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 205678922FBF9CBB00251C6B /* MyPass */;
target = 205678922FBF9CBB00251C6B /* KeeVault */;
targetProxy = 205678AD2FBF9CBC00251C6B /* PBXContainerItemProxy */;
};
2096B43C305EC2B200C2F253 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilters = (ios, );
platformFilter = ios;
target = 2096B430305EC2B200C2F253 /* AutoFill */;
targetProxy = 2096B43B305EC2B200C2F253 /* PBXContainerItemProxy */;
};
@@ -425,6 +441,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -447,6 +464,7 @@
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
@@ -486,6 +504,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -501,6 +520,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
@@ -510,14 +530,18 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = MyPass/MyPass.entitlements;
CODE_SIGN_ENTITLEMENTS = KeeVault/KeeVault.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = F2KB6W8N4R;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
ENTITLEMENTS_REQUIRED = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_NSFaceIDUsageDescription = "Unlock your vault with Face ID.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -533,10 +557,8 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.MyPass;
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = org.antiloop222.MyPass;
"PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = org.antiloop222.MyPass;
"PRODUCT_BUNDLE_IDENTIFIER[sdk=xros*]" = org.antiloop222.MyPass;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.keevault;
"PRODUCT_BUNDLE_IDENTIFIER[sdk=xros*]" = org.antiloop222.KeeVault;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SDKROOT = auto;
@@ -558,14 +580,18 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = MyPass/MyPass.entitlements;
CODE_SIGN_ENTITLEMENTS = KeeVault/KeeVault.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = F2KB6W8N4R;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
ENTITLEMENTS_REQUIRED = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_NSFaceIDUsageDescription = "Unlock your vault with Face ID.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -581,10 +607,8 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.MyPass;
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = org.antiloop222.MyPass;
"PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = org.antiloop222.MyPass;
"PRODUCT_BUNDLE_IDENTIFIER[sdk=xros*]" = org.antiloop222.MyPass;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.keevault;
"PRODUCT_BUNDLE_IDENTIFIER[sdk=xros*]" = org.antiloop222.KeeVault;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SDKROOT = auto;
@@ -607,11 +631,12 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.MyPassTests;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.keevaulttests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -622,7 +647,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MyPass.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MyPass";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KeeVault.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/KeeVault";
XROS_DEPLOYMENT_TARGET = 26.5;
};
name = Debug;
@@ -633,11 +658,12 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.MyPassTests;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.keevaulttests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -648,7 +674,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MyPass.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MyPass";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KeeVault.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/KeeVault";
XROS_DEPLOYMENT_TARGET = 26.5;
};
name = Release;
@@ -658,11 +684,12 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.MyPassUITests;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.keevaultuitests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -673,7 +700,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = MyPass;
TEST_TARGET_NAME = KeeVault;
XROS_DEPLOYMENT_TARGET = 26.5;
};
name = Debug;
@@ -683,11 +710,12 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.MyPassUITests;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.keevaultuitests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -698,7 +726,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = MyPass;
TEST_TARGET_NAME = KeeVault;
XROS_DEPLOYMENT_TARGET = 26.5;
};
name = Release;
@@ -706,13 +734,16 @@
2096B43E305EC2B200C2F253 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = AutoFill/AutoFill.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = F2KB6W8N4R;
ENTITLEMENTS_REQUIRED = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AutoFill/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AutoFill;
INFOPLIST_KEY_NSFaceIDUsageDescription = "Unlock your vault with Face ID.";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
LD_RUNPATH_SEARCH_PATHS = (
@@ -721,7 +752,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.MyPass.AutoFill;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.keevault.autofill;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SDKROOT = iphoneos;
@@ -741,13 +772,16 @@
2096B43F305EC2B200C2F253 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = AutoFill/AutoFill.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = F2KB6W8N4R;
ENTITLEMENTS_REQUIRED = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AutoFill/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AutoFill;
INFOPLIST_KEY_NSFaceIDUsageDescription = "Unlock your vault with Face ID.";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
LD_RUNPATH_SEARCH_PATHS = (
@@ -756,7 +790,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.MyPass.AutoFill;
PRODUCT_BUNDLE_IDENTIFIER = org.antiloop222.keevault.autofill;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SDKROOT = iphoneos;
@@ -777,7 +811,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
2056788E2FBF9CBB00251C6B /* Build configuration list for PBXProject "MyPass" */ = {
2056788E2FBF9CBB00251C6B /* Build configuration list for PBXProject "KeeVault" */ = {
isa = XCConfigurationList;
buildConfigurations = (
205678B42FBF9CBC00251C6B /* Debug */,
@@ -786,7 +820,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
205678B62FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "MyPass" */ = {
205678B62FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "KeeVault" */ = {
isa = XCConfigurationList;
buildConfigurations = (
205678B72FBF9CBC00251C6B /* Debug */,
@@ -795,7 +829,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
205678B92FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "MyPassTests" */ = {
205678B92FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "KeeVaultTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
205678BA2FBF9CBC00251C6B /* Debug */,
@@ -804,7 +838,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
205678BC2FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "MyPassUITests" */ = {
205678BC2FBF9CBC00251C6B /* Build configuration list for PBXNativeTarget "KeeVaultUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
205678BD2FBF9CBC00251C6B /* Debug */,
@@ -825,16 +859,16 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
205678D92FC0EB5200251C6B /* XCLocalSwiftPackageReference "MyPassCore" */ = {
205678D92FC0EB5200251C6B /* XCLocalSwiftPackageReference "KeeVaultCore" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = MyPassCore;
relativePath = KeeVaultCore;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
205678DA2FC0EB5200251C6B /* MyPassCore */ = {
205678DA2FC0EB5200251C6B /* KeeVaultCore */ = {
isa = XCSwiftPackageProductDependency;
productName = MyPassCore;
productName = KeeVaultCore;
};
/* End XCSwiftPackageProductDependency section */
};
@@ -7,12 +7,12 @@
<key>AutoFill.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>4</integer>
<integer>2</integer>
</dict>
<key>MyPass.xcscheme_^#shared#^_</key>
<key>KeeVault.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
<integer>1</integer>
</dict>
</dict>
</dict>
@@ -0,0 +1,38 @@
{
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.420",
"green" : "0.271",
"blue" : "0.467",
"alpha" : "1.000"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.651",
"green" : "0.498",
"blue" : "0.690",
"alpha" : "1.000"
}
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -3,7 +3,8 @@
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
"size" : "1024x1024",
"filename" : "icon-1024-light.png"
},
{
"appearances" : [
@@ -14,7 +15,8 @@
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
"size" : "1024x1024",
"filename" : "icon-1024-dark.png"
},
{
"appearances" : [
@@ -25,57 +27,68 @@
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
"size" : "1024x1024",
"filename" : "icon-1024-tinted.png"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
"size" : "16x16",
"filename" : "icon-mac-16.png"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
"size" : "16x16",
"filename" : "icon-mac-16@2x.png"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
"size" : "32x32",
"filename" : "icon-mac-32.png"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
"size" : "32x32",
"filename" : "icon-mac-32@2x.png"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
"size" : "128x128",
"filename" : "icon-mac-128.png"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
"size" : "128x128",
"filename" : "icon-mac-128@2x.png"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
"size" : "256x256",
"filename" : "icon-mac-256.png"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
"size" : "256x256",
"filename" : "icon-mac-256@2x.png"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
"size" : "512x512",
"filename" : "icon-mac-512.png"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
"size" : "512x512",
"filename" : "icon-mac-512@2x.png"
}
],
"info" : {
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

@@ -0,0 +1,32 @@
{
"images" : [
{
"filename" : "icon-light.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "icon-dark.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

@@ -1,9 +1,4 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
@@ -0,0 +1,38 @@
{
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.980",
"green" : "0.957",
"blue" : "0.925",
"alpha" : "1.000"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.141",
"green" : "0.125",
"blue" : "0.094",
"alpha" : "1.000"
}
}
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,12 +1,12 @@
//
// ContentView.swift
// MyPass
// KeeVault
//
// Created by Christophe Vila on 21/05/2026.
//
import SwiftUI
import MyPassCore
import KeeVaultCore
#if os(macOS)
import AppKit
#endif
@@ -22,6 +22,7 @@ struct ContentView: View {
VaultRootView(session: session)
}
}
.background(Color("VaultBackground"))
.onReceive(
NotificationCenter.default.publisher(for: sceneBackgroundNotification)
) { _ in
@@ -2,13 +2,15 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.antiloop222.mypass</string>
<string>group.org.antiloop222.keevault</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)org.antiloop222.mypass</string>
<string>$(AppIdentifierPrefix)org.antiloop222.keevault</string>
</array>
</dict>
</plist>
@@ -1,6 +1,6 @@
//
// MyPassApp.swift
// MyPass
// KeeVaultApp.swift
// KeeVault
//
// Created by Christophe Vila on 21/05/2026.
//
@@ -8,7 +8,7 @@
import SwiftUI
@main
struct MyPassApp: App {
struct KeeVaultApp: App {
var body: some Scene {
WindowGroup {
ContentView()
@@ -1,6 +1,6 @@
//
// BiometricAuthService.swift
// MyPass
// KeeVault
//
import LocalAuthentication
@@ -1,6 +1,6 @@
//
// ClipboardService.swift
// MyPass
// KeeVault
//
import Foundation
@@ -1,6 +1,6 @@
//
// FileBookmarkService.swift
// MyPass
// KeeVault
//
import Foundation
@@ -10,7 +10,7 @@ public final class FileBookmarkService {
private static let key = "kdbxBookmark"
private let defaults: UserDefaults
public init(appGroup: String = "group.org.antiloop222.mypass") {
public init(appGroup: String = "group.org.antiloop222.keevault") {
defaults = UserDefaults(suiteName: appGroup) ?? .standard
}
@@ -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,
@@ -1,8 +1,8 @@
// MyPass/ViewModels/EntryEditViewModel.swift
// KeeVault/ViewModels/EntryEditViewModel.swift
import Foundation
import Combine
import SwiftUI
import MyPassCore
import KeeVaultCore
@MainActor
final class EntryEditViewModel: ObservableObject {
@@ -20,7 +20,7 @@ final class EntryEditViewModel: ObservableObject {
private let existingEntry: Entry?
/// `groupId` is the target group for a new entry, or the entry's current group when
/// editing (shown read-only in that case -- MyPass doesn't support re-parenting an
/// editing (shown read-only in that case -- KeeVault doesn't support re-parenting an
/// existing entry to a different group yet).
init(session: VaultSession, groupId: UUID, existing: Entry? = nil) {
self.session = session
@@ -1,6 +1,6 @@
import Foundation
import Combine
import MyPassCore
import KeeVaultCore
@MainActor
final class GroupFilterViewModel: ObservableObject {
@@ -16,11 +16,11 @@ final class GroupFilterViewModel: ObservableObject {
self.session = session
}
func isSelected(_ group: MyPassCore.Group) -> Bool {
func isSelected(_ group: KeeVaultCore.Group) -> Bool {
selectedGroupIds.contains(group.id)
}
func toggle(_ group: MyPassCore.Group) {
func toggle(_ group: KeeVaultCore.Group) {
guard let root = session.database?.root else { return }
selectedGroupIds = GroupSelection.toggling(group.id, in: root, current: selectedGroupIds)
lastToggledGroupId = selectedGroupIds.isEmpty ? nil : group.id
@@ -1,11 +1,11 @@
//
// UnlockViewModel.swift
// MyPass
// KeeVault
//
import Foundation
import Combine
import MyPassCore
import KeeVaultCore
@MainActor
final class UnlockViewModel: ObservableObject {
@@ -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.keevault"),
biometricService: BiometricAuthService = .init()
) {
self.session = session
@@ -44,7 +44,7 @@ final class UnlockViewModel: ObservableObject {
isUnlocking = true
errorMessage = nil
do {
try await biometricService.authenticate(reason: "Unlock MyPass")
try await biometricService.authenticate(reason: "Unlock KeeVault")
let storedPassword = try keychainStore.load(for: keychainAccount)
let url = try bookmarkService.resolveURL()
defer { bookmarkService.stopAccess(url: url) }
@@ -1,7 +1,7 @@
// MyPass/ViewModels/VaultViewModel.swift
// KeeVault/ViewModels/VaultViewModel.swift
import Foundation
import Combine
import MyPassCore
import KeeVaultCore
@MainActor
final class VaultViewModel: ObservableObject {
@@ -1,11 +1,11 @@
//
// EntryDetailView.swift
// MyPass
// KeeVault
//
import SwiftUI
import Combine
import MyPassCore
import KeeVaultCore
struct EntryDetailView: View {
let entry: Entry
@@ -1,6 +1,6 @@
// MyPass/Views/EntryEditView.swift
// KeeVault/Views/EntryEditView.swift
import SwiftUI
import MyPassCore
import KeeVaultCore
struct EntryEditView: View {
@ObservedObject var vm: EntryEditViewModel
@@ -1,10 +1,9 @@
// MyPass/Views/EntryListView.swift
// KeeVault/Views/EntryListView.swift
import SwiftUI
import MyPassCore
import KeeVaultCore
struct EntryListView: View {
@ObservedObject var vm: VaultViewModel
@State private var isSearching = false
@State private var showAddEntry = false
var body: some View {
@@ -24,17 +23,11 @@ struct EntryListView: View {
offsets.map { vm.displayedEntries[$0] }.forEach(vm.deleteEntry)
}
}
.navigationTitle("MyPass")
.searchable(text: $vm.searchQuery, isPresented: $isSearching, prompt: "Search entries…")
.scrollContentBackground(.hidden)
.background(Color("VaultBackground"))
.navigationTitle("KeeVault")
.searchable(text: $vm.searchQuery, prompt: "Search entries…")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
if isSearching { vm.searchQuery = "" }
isSearching.toggle()
} label: {
Image(systemName: "magnifyingglass")
}
}
ToolbarItem(placement: .primaryAction) {
Button { showAddEntry = true } label: {
Image(systemName: "plus")
@@ -1,35 +1,39 @@
import SwiftUI
import MyPassCore
import KeeVaultCore
struct GroupFilterView: View {
@ObservedObject var vm: GroupFilterViewModel
let rootGroup: MyPassCore.Group
let rootGroup: KeeVaultCore.Group
var body: some View {
List {
if !vm.selectedGroupIds.isEmpty {
Button("Clear Filter", action: vm.clear)
}
GroupFilterNode(group: rootGroup, vm: vm)
}
.scrollContentBackground(.hidden)
.background(Color("VaultBackground"))
.navigationTitle("Groups")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Clear", action: vm.clear)
.disabled(vm.selectedGroupIds.isEmpty)
}
}
}
}
private struct GroupFilterNode: View {
let group: MyPassCore.Group
let group: KeeVaultCore.Group
@ObservedObject var vm: GroupFilterViewModel
var depth: Int = 0
var body: some View {
if group.subgroups.isEmpty {
// A plain SwiftUI.Group (not a container view) flattens its children into
// separate List rows, so the whole subtree renders always-expanded --
// no DisclosureGroup, no tap-to-reveal -- active filters are visible at a glance.
SwiftUI.Group {
row
} else {
DisclosureGroup {
ForEach(group.subgroups) { sub in
GroupFilterNode(group: sub, vm: vm)
}
} label: {
row
ForEach(group.subgroups) { sub in
GroupFilterNode(group: sub, vm: vm, depth: depth + 1)
}
}
}
@@ -45,5 +49,6 @@ private struct GroupFilterNode: View {
}
}
.buttonStyle(.plain)
.padding(.leading, CGFloat(depth) * 16)
}
}
@@ -1,11 +1,11 @@
//
// UnlockView.swift
// MyPass
// KeeVault
//
import SwiftUI
import UniformTypeIdentifiers
import MyPassCore
import KeeVaultCore
struct UnlockView: View {
@ObservedObject var vm: UnlockViewModel
@@ -13,10 +13,11 @@ struct UnlockView: View {
var body: some View {
VStack(spacing: 24) {
Spacer()
Image(systemName: "lock.shield.fill")
.font(.system(size: 64))
.foregroundStyle(.tint)
Text("MyPass")
Image("AppIconImage")
.resizable()
.frame(width: 88, height: 88)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
Text("KeeVault")
.font(.largeTitle.bold())
if vm.hasVault {
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>KeeVaultCore.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
</dict>
</dict>
</dict>
</plist>
@@ -2,24 +2,24 @@
import PackageDescription
let package = Package(
name: "MyPassCore",
name: "KeeVaultCore",
platforms: [.iOS(.v17), .macOS(.v14)],
products: [
.library(name: "MyPassCore", targets: ["MyPassCore"]),
.library(name: "KeeVaultCore", targets: ["KeeVaultCore"]),
],
dependencies: [
.package(path: "../Vendor/KeePassKit"),
],
targets: [
.target(
name: "MyPassCore",
name: "KeeVaultCore",
dependencies: [
.product(name: "KeePassKit", package: "KeePassKit"),
]
),
.testTarget(
name: "MyPassCoreTests",
dependencies: ["MyPassCore"],
name: "KeeVaultCoreTests",
dependencies: ["KeeVaultCore"],
resources: [.copy("Fixtures")]
),
]
@@ -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 {
@@ -1,7 +1,7 @@
import Foundation
/// Maps KeePass's standard icon palette (indices 0-68) to an approximate SF Symbol.
/// This is a best-effort visual match, not the actual KeePass icon artwork -- MyPass's
/// This is a best-effort visual match, not the actual KeePass icon artwork -- KeeVault's
/// data model only stores the numeric iconIndex, not custom icon images.
public enum KeePassIconMapper {
private static let symbolsByIndex: [Int: String] = [
@@ -3,7 +3,7 @@ import KeePassKit
enum KDBXMapper {
// MARK: - KeePassKit MyPassCore
// MARK: - KeePassKit KeeVaultCore
static func database(from tree: KPKTree) -> KDBXDatabase {
let meta = DatabaseMetadata(
@@ -67,7 +67,7 @@ enum KDBXMapper {
)
}
// MARK: - MyPassCore KeePassKit
// MARK: - KeeVaultCore KeePassKit
static func tree(from db: KDBXDatabase) -> KPKTree {
let tree = KPKTree()
@@ -129,7 +129,7 @@ enum KDBXMapper {
var components = URLComponents()
components.scheme = "otpauth"
components.host = "totp"
components.path = "/MyPass"
components.path = "/KeeVault"
components.queryItems = [
URLQueryItem(name: "secret", value: config.secret),
URLQueryItem(name: "period", value: String(config.period)),
@@ -5,7 +5,7 @@ public struct KeychainStore {
private let accessGroup: String
private let service: String
public init(accessGroup: String, service: String = "org.antiloop222.mypass") {
public init(accessGroup: String, service: String = "org.antiloop222.keevault") {
self.accessGroup = accessGroup
self.service = service
}
@@ -1,5 +1,5 @@
import XCTest
@testable import MyPassCore
@testable import KeeVaultCore
final class CredentialMatcherTests: XCTestCase {
func makeEntry(url: String) -> Entry {
@@ -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"),
@@ -1,5 +1,5 @@
import XCTest
@testable import MyPassCore
@testable import KeeVaultCore
final class GroupSelectionTests: XCTestCase {
private func makeSampleTree() -> (root: Group, work: Group, email: Group, personal: Group) {
@@ -1,5 +1,5 @@
import XCTest
@testable import MyPassCore
@testable import KeeVaultCore
final class GroupTreeTests: XCTestCase {
private func makeSampleTree() -> (root: Group, work: Group, email: Group) {
@@ -1,5 +1,5 @@
import XCTest
@testable import MyPassCore
@testable import KeeVaultCore
final class KDBXDocumentTests: XCTestCase {
var fixtureURL: URL!
@@ -1,5 +1,5 @@
import XCTest
@testable import MyPassCore
@testable import KeeVaultCore
final class KeePassIconMapperTests: XCTestCase {
func test_knownIndex_returnsMappedSymbol() {
@@ -1,5 +1,5 @@
import XCTest
@testable import MyPassCore
@testable import KeeVaultCore
final class ProtectedStringTests: XCTestCase {
func test_reveal_returnsOriginalValue() {
@@ -1,5 +1,5 @@
import XCTest
@testable import MyPassCore
@testable import KeeVaultCore
final class TOTPGeneratorTests: XCTestCase {
// RFC 6238 Section 8 test vectors for SHA-1
@@ -1,13 +1,13 @@
//
// MyPassTests.swift
// MyPassTests
// KeeVaultTests.swift
// KeeVaultTests
//
// Created by Christophe Vila on 21/05/2026.
//
import Testing
struct MyPassTests {
struct KeeVaultTests {
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
@@ -1,13 +1,13 @@
//
// MyPassUITests.swift
// MyPassUITests
// KeeVaultUITests.swift
// KeeVaultUITests
//
// Created by Christophe Vila on 21/05/2026.
//
import XCTest
final class MyPassUITests: XCTestCase {
final class KeeVaultUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
@@ -1,13 +1,13 @@
//
// MyPassUITestsLaunchTests.swift
// MyPassUITests
// KeeVaultUITestsLaunchTests.swift
// KeeVaultUITests
//
// Created by Christophe Vila on 21/05/2026.
//
import XCTest
final class MyPassUITestsLaunchTests: XCTestCase {
final class KeeVaultUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
@@ -152,7 +152,7 @@ NavigationSplitView(columnVisibility:)
`NavigationSplitView` is used on **both** platforms instead of maintaining separate iOS/macOS navigation code. On iPhone-width layouts it automatically collapses the sidebar into an overlay; on iPad/Mac it can sit persistently alongside the detail column. This behavior is built into the component — no platform-specific branching needed for it.
- **Hamburger button** (leading toolbar item, `"line.3.horizontal"`) toggles `columnVisibility` between `.all` and `.detailOnly`, showing/hiding the sidebar.
- **Search icon** (trailing toolbar item, `"magnifyingglass"`) toggles a `.searchable()` bar over `EntryListView`; tapping again hides it and clears the query.
- **Search** is always visible via `.searchable()` over `EntryListView` (no toggle icon -- changed 2026-09-19 after hands-on use showed the extra tap added no value over an always-present search field).
- Swipe-to-delete on entry rows; toolbar `+` button opens `EntryEditView` for a new entry (see "Add-entry flow" below).
- Tapping a password field in `EntryDetailView` copies to clipboard (clears after 30 s).
- TOTP code displays with a countdown ring and refreshes automatically.