Refactor core data classes (#671)

This commit is contained in:
Mingshen Sun
2025-01-25 15:40:12 -08:00
committed by GitHub
parent ab453580ad
commit d1de81d919
24 changed files with 605 additions and 433 deletions

View File

@@ -195,6 +195,9 @@
DC4914961E434301007FF592 /* LabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914941E434301007FF592 /* LabelTableViewCell.swift */; };
DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */; };
DC5F385B1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */; };
DC6474532D20DD0C004B4BBC /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */; };
DC64745C2D29BE9B004B4BBC /* PasswordEntityTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474592D29BD43004B4BBC /* PasswordEntityTest.swift */; };
DC64745D2D29BEA9004B4BBC /* CoreDataTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6474582D29BD43004B4BBC /* CoreDataTestCase.swift */; };
DC7CBBBD2D0FA3F2003BB4D2 /* YubiKit in Frameworks */ = {isa = PBXBuildFile; productRef = DC7CBBBC2D0FA3F2003BB4D2 /* YubiKit */; };
DC7CBBBF2D0FAC92003BB4D2 /* YKFSmartCardInterfaceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7CBBBE2D0FAC8E003BB4D2 /* YKFSmartCardInterfaceExtension.swift */; };
DC8963C01E38EEB900828B09 /* SSHKeyURLImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8963BF1E38EEB900828B09 /* SSHKeyURLImportTableViewController.swift */; };
@@ -493,6 +496,9 @@
DC4914941E434301007FF592 /* LabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelTableViewCell.swift; sourceTree = "<group>"; };
DC4914981E434600007FF592 /* PasswordDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordDetailTableViewController.swift; sourceTree = "<group>"; };
DC5F385A1E56AADB00C69ACA /* PGPKeyArmorImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PGPKeyArmorImportTableViewController.swift; sourceTree = "<group>"; };
DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = "<group>"; };
DC6474582D29BD43004B4BBC /* CoreDataTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataTestCase.swift; sourceTree = "<group>"; };
DC6474592D29BD43004B4BBC /* PasswordEntityTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordEntityTest.swift; sourceTree = "<group>"; };
DC7CBBBE2D0FAC8E003BB4D2 /* YKFSmartCardInterfaceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YKFSmartCardInterfaceExtension.swift; sourceTree = "<group>"; };
DC8963BF1E38EEB900828B09 /* SSHKeyURLImportTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSHKeyURLImportTableViewController.swift; sourceTree = "<group>"; };
DC917BD31E2E8231000FDF54 /* Pass.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pass.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -868,6 +874,7 @@
A26075861EEC6F34005DB03E /* passKitTests */ = {
isa = PBXGroup;
children = (
DC64745A2D29BD43004B4BBC /* CoreData */,
30A86F93230F235800F821A4 /* Crypto */,
30BAC8C322E3BA4300438475 /* Testbase */,
30697C5521F63F870064FCAC /* Extensions */,
@@ -901,6 +908,7 @@
children = (
30697C3121F63C8B0064FCAC /* PasscodeLockPresenter.swift */,
30697C3221F63C8B0064FCAC /* PasscodeLockViewController.swift */,
DC6474522D20DD0C004B4BBC /* CoreDataStack.swift */,
);
path = Controllers;
sourceTree = "<group>";
@@ -1021,6 +1029,15 @@
path = Views;
sourceTree = "<group>";
};
DC64745A2D29BD43004B4BBC /* CoreData */ = {
isa = PBXGroup;
children = (
DC6474582D29BD43004B4BBC /* CoreDataTestCase.swift */,
DC6474592D29BD43004B4BBC /* PasswordEntityTest.swift */,
);
path = CoreData;
sourceTree = "<group>";
};
DC917BCA1E2E8231000FDF54 = {
isa = PBXGroup;
children = (
@@ -1589,6 +1606,7 @@
3087574F2343E42A00B971A2 /* Colors.swift in Sources */,
30697C2C21F63C5A0064FCAC /* FileManagerExtension.swift in Sources */,
30697C3321F63C8B0064FCAC /* PasscodeLockPresenter.swift in Sources */,
DC6474532D20DD0C004B4BBC /* CoreDataStack.swift in Sources */,
30697C3D21F63C990064FCAC /* UIViewExtension.swift in Sources */,
30697C3A21F63C990064FCAC /* UIViewControllerExtension.swift in Sources */,
30697C2E21F63C5A0064FCAC /* Utils.swift in Sources */,
@@ -1619,9 +1637,11 @@
30697C5F21F674800064FCAC /* String+UtilitiesTest.swift in Sources */,
3032328A22C9FBA2009EBD9C /* KeyFileManagerTest.swift in Sources */,
306623332406F1A8000E2AD6 /* PasswordGeneratorTest.swift in Sources */,
DC64745C2D29BE9B004B4BBC /* PasswordEntityTest.swift in Sources */,
30BAC8C722E3BAAF00438475 /* TestPGPKeys.swift in Sources */,
30A1D2AA21B32A0100E2D1F7 /* OTPTypeTest.swift in Sources */,
301F6468216165290071A4CE /* ConstantsTest.swift in Sources */,
DC64745D2D29BEA9004B4BBC /* CoreDataTestCase.swift in Sources */,
30A1D29C21AF451E00E2D1F7 /* PasswordGeneratorFlavorTest.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@@ -23,6 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var passcodeLockPresenter = PasscodeLockPresenter(mainWindow: self.window)
func application(_: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
PersistenceController.shared.setup()
// Override point for customization after application launch.
SVProgressHUD.setMinimumSize(CGSize(width: 150, height: 100))
passcodeLockPresenter.present(windowLevel: UIApplication.shared.windows.last?.windowLevel.rawValue)
@@ -80,6 +81,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
appIconView.center = (window?.center)!
appIconView.tag = ViewTag.appicon.rawValue
window?.addSubview(appIconView)
PersistenceController.shared.save()
}
func applicationDidEnterBackground(_: UIApplication) {
@@ -102,55 +105,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationWillTerminate(_: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
saveContext()
}
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
// The persistent container for the application. This implementation
// creates and returns a container, having loaded the store for the
// application to it. This property is optional since there are legitimate
// error conditions that could cause the creation of the store to fail.
let modelURL = Bundle(identifier: Globals.passKitBundleIdentifier)!.url(forResource: "pass", withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)
let container = NSPersistentContainer(name: "pass", managedObjectModel: managedObjectModel!)
if FileManager.default.fileExists(atPath: Globals.documentPath) {
try! FileManager.default.createDirectory(atPath: Globals.documentPath, withIntermediateDirectories: true, attributes: nil)
}
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: URL(fileURLWithPath: Globals.dbPath))]
container.loadPersistentStores { _, error in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
// Typical reasons for an error here include:
//
// * The parent directory does not exist, cannot be created, or disallows writing.
// * The persistent store is not accessible, due to permissions or data protection when the device is locked.
// * The device is out of space.
// * The store could not be migrated to the current model version.
//
// Check the error message to determine what the actual problem was.
fatalError("UnresolvedError".localize("\(error), \(error.userInfo)"))
}
}
return container
}()
// MARK: - Core Data Saving support
func saveContext() {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("UnresolvedError".localize("\(nserror), \(nserror.userInfo)"))
}
}
PersistenceController.shared.save()
}
}

View File

@@ -128,18 +128,18 @@
<rect key="frame" x="0.0" y="105.33333587646484" width="390" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="1ze-MS-Xbj" id="W7U-oL-hOh">
<rect key="frame" x="0.0" y="0.0" width="359.66666666666669" height="43.666667938232422"/>
<rect key="frame" x="0.0" y="0.0" width="331.66666666666669" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="PGP Key" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="RR9-xr-9ko">
<rect key="frame" x="20" y="11.999999999999998" width="65.666666666666671" height="20.333333333333332"/>
<rect key="frame" x="8" y="11.999999999999998" width="65.666666666666671" height="20.333333333333332"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Not Set" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="7lc-Vh-G9W">
<rect key="frame" x="294.33333333333337" y="11.999999999999998" width="57.333333333333336" height="20.333333333333332"/>
<rect key="frame" x="266.33333333333337" y="11.999999999999998" width="57.333333333333336" height="20.333333333333332"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
@@ -156,18 +156,18 @@
<rect key="frame" x="0.0" y="205.00000381469729" width="390" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="6Y0-mj-qhA" id="qlv-tQ-Xmc">
<rect key="frame" x="0.0" y="0.0" width="331.66666666666669" height="43.666667938232422"/>
<rect key="frame" x="0.0" y="0.0" width="359.66666666666669" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Passcode Lock" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="RaZ-6t-0CU">
<rect key="frame" x="8.0000000000000071" y="11.999999999999998" width="114.66666666666667" height="20.333333333333332"/>
<rect key="frame" x="20.000000000000007" y="11.999999999999998" width="114.66666666666667" height="20.333333333333332"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Off" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="HXb-ZX-HUv">
<rect key="frame" x="299.33333333333337" y="11.999999999999998" width="24.333333333333332" height="20.333333333333332"/>
<rect key="frame" x="327.33333333333337" y="11.999999999999998" width="24.333333333333332" height="20.333333333333332"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
@@ -184,11 +184,11 @@
<rect key="frame" x="0.0" y="284.66667175292969" width="390" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="tQN-gu-iRe" id="Xs0-LN-r43">
<rect key="frame" x="0.0" y="0.0" width="331.66666666666669" height="43.666667938232422"/>
<rect key="frame" x="0.0" y="0.0" width="359.66666666666669" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Advanced" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="MKj-d0-8q3">
<rect key="frame" x="8" y="0.0" width="315.66666666666669" height="43.666667938232422"/>
<rect key="frame" x="20" y="0.0" width="331.66666666666669" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@@ -320,7 +320,7 @@
<rect key="frame" x="0.0" y="55.333332061767578" width="390" height="44.333332061767578"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="FRr-pf-aPO" id="60A-PS-qGe">
<rect key="frame" x="0.0" y="0.0" width="358" height="44.333332061767578"/>
<rect key="frame" x="0.0" y="0.0" width="318" height="44.333332061767578"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Git Repository URL" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="EVT-VU-sCi">
@@ -349,7 +349,7 @@
<rect key="frame" x="0.0" y="155.66666221618652" width="390" height="44.333332061767578"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5XL-Uj-JWL" id="BfY-mD-gfv">
<rect key="frame" x="0.0" y="0.0" width="390" height="44.333332061767578"/>
<rect key="frame" x="0.0" y="0.0" width="350" height="44.333332061767578"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Branch Name" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="VVI-gJ-e37" userLabel="Text">
@@ -377,7 +377,7 @@
<rect key="frame" x="0.0" y="255.9999942779541" width="390" height="44.333332061767578"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="tnj-5U-kMB" id="f0c-pI-MSJ">
<rect key="frame" x="0.0" y="0.0" width="390" height="44.333332061767578"/>
<rect key="frame" x="0.0" y="0.0" width="350" height="44.333332061767578"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Username" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="TMg-Gk-7nG">
@@ -402,20 +402,20 @@
<tableViewSection headerTitle="Authentication Method" id="h0N-tI-shZ">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="2" indentationWidth="0.0" shouldIndentWhileEditing="NO" id="KrP-nb-haa">
<rect key="frame" x="0.0" y="356.33332633972168" width="390" height="43.333332061767578"/>
<rect key="frame" x="0.0" y="356.33332633972168" width="390" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="KrP-nb-haa" id="1uB-oE-kfI">
<rect key="frame" x="0.0" y="0.0" width="390" height="43.333332061767578"/>
<rect key="frame" x="0.0" y="0.0" width="350" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Password" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LfQ-Af-j2O">
<rect key="frame" x="74" y="10.999999999999998" width="256" height="21.333333333333329"/>
<rect key="frame" x="74" y="11.000000000000002" width="256" height="21.666666666666671"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" tag="1001" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="✓" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Scc-5J-bu1">
<rect key="frame" x="35" y="10.666666666666664" width="16" height="22"/>
<rect key="frame" x="35" y="11" width="16" height="22"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
<color key="textColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
@@ -434,20 +434,20 @@
<inset key="separatorInset" minX="62" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="blue" accessoryType="detailButton" hidesAccessoryWhenEditing="NO" indentationLevel="2" indentationWidth="0.0" shouldIndentWhileEditing="NO" id="Qmt-bo-CuJ">
<rect key="frame" x="0.0" y="399.66665840148926" width="390" height="43.333332061767578"/>
<rect key="frame" x="0.0" y="399.9999942779541" width="390" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="Qmt-bo-CuJ" id="p3u-8b-h3U">
<rect key="frame" x="0.0" y="0.0" width="358" height="43.333332061767578"/>
<rect key="frame" x="0.0" y="0.0" width="318" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SSH Key" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ezz-76-a53">
<rect key="frame" x="74" y="10.999999999999998" width="223" height="21.333333333333329"/>
<rect key="frame" x="74" y="11.000000000000002" width="223" height="21.666666666666671"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" tag="1001" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="✓" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wbx-rk-i8H">
<rect key="frame" x="35" y="10.666666666666664" width="16" height="22"/>
<rect key="frame" x="35" y="11" width="16" height="22"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
<color key="textColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
@@ -513,18 +513,18 @@
<rect key="frame" x="0.0" y="18" width="390" height="49.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="9bH-ha-yOA" id="EeF-hE-hJM">
<rect key="frame" x="0.0" y="0.0" width="390" height="49.666667938232422"/>
<rect key="frame" x="0.0" y="0.0" width="350" height="49.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="ASCII-Armor Keys" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="cd7-0l-AZW">
<rect key="frame" x="20.000000000000007" y="6.6666666666666661" width="121.66666666666667" height="17"/>
<rect key="frame" x="8.0000000000000071" y="6.6666666666666661" width="121.66666666666667" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" name="HelveticaNeue-Bold" family="Helvetica Neue" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="GpgAsciiArmorServerExplanation." lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="s90-cp-Qxp" customClass="UICodeHighlightingLabel" customModule="Pass" customModuleProvider="target">
<rect key="frame" x="20" y="25.999999999999996" width="197.66666666666666" height="15.333333333333334"/>
<rect key="frame" x="8" y="25.999999999999996" width="197.66666666666666" height="15.333333333333334"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" name="HelveticaNeue" family="Helvetica Neue" pointSize="13"/>
<color key="textColor" red="0.29804000000000003" green="0.29804000000000003" blue="0.29804000000000003" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
@@ -541,7 +541,7 @@
<rect key="frame" x="0.0" y="103.66666793823242" width="390" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="BYZ-9g-xZy" id="Zfn-rK-sN1">
<rect key="frame" x="0.0" y="0.0" width="390" height="61"/>
<rect key="frame" x="0.0" y="0.0" width="350" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Public Key URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dWi-eh-7Eq">
@@ -574,7 +574,7 @@
<rect key="frame" x="0.0" y="164.66666793823242" width="390" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="vpk-J8-j7t" id="1td-qT-6ts">
<rect key="frame" x="0.0" y="0.0" width="390" height="61"/>
<rect key="frame" x="0.0" y="0.0" width="350" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Private Key URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Qht-RC-Yeg">
@@ -1106,7 +1106,7 @@ Secret Question 1: What is your childhood best friend's most bizarre superhero f
<rect key="frame" x="0.0" y="154.99999809265137" width="390" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="SVj-jD-qPT" id="HaO-5w-qZt">
<rect key="frame" x="0.0" y="0.0" width="371.66666666666669" height="43.666667938232422"/>
<rect key="frame" x="0.0" y="0.0" width="331.66666666666669" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Git Signature" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="87a-xY-AbR">
@@ -1117,7 +1117,7 @@ Secret Question 1: What is your childhood best friend's most bizarre superhero f
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Not Set" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="2qr-d7-0SK">
<rect key="frame" x="306.33333333333337" y="11.999999999999998" width="57.333333333333336" height="20.333333333333332"/>
<rect key="frame" x="266.33333333333337" y="11.999999999999998" width="57.333333333333336" height="20.333333333333332"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
@@ -1158,11 +1158,11 @@ Secret Question 1: What is your childhood best friend's most bizarre superhero f
<rect key="frame" x="0.0" y="354.33333396911621" width="390" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Jm8-B5-wKx" id="tjS-Q6-y2M">
<rect key="frame" x="0.0" y="0.0" width="390" height="43.666667938232422"/>
<rect key="frame" x="0.0" y="0.0" width="350" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Discard All Local Changes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="zrl-v3-fxg">
<rect key="frame" x="20" y="0.0" width="362" height="43.666667938232422"/>
<rect key="frame" x="8" y="0.0" width="334" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" systemColor="systemRedColor"/>
@@ -1321,18 +1321,18 @@ Secret Question 1: What is your childhood best friend's most bizarre superhero f
<rect key="frame" x="0.0" y="18" width="390" height="49.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Pv0-ev-stj" id="ywz-II-W1g">
<rect key="frame" x="0.0" y="0.0" width="390" height="49.666667938232422"/>
<rect key="frame" x="0.0" y="0.0" width="350" height="49.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="ASCII-Armor Keys" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="M32-yr-IfE">
<rect key="frame" x="20.000000000000007" y="6.6666666666666661" width="121.66666666666667" height="17"/>
<rect key="frame" x="8.0000000000000071" y="6.6666666666666661" width="121.66666666666667" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" name="HelveticaNeue-Bold" family="Helvetica Neue" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="GpgAsciiArmorCopyExplanation." lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="sMx-qd-MTJ" customClass="UICodeHighlightingLabel" customModule="Pass" customModuleProvider="target">
<rect key="frame" x="20" y="25.999999999999996" width="191.33333333333334" height="15.333333333333334"/>
<rect key="frame" x="8" y="25.999999999999996" width="191.33333333333334" height="15.333333333333334"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" name="HelveticaNeue" family="Helvetica Neue" pointSize="13"/>
<color key="textColor" red="0.29804000000000003" green="0.29804000000000003" blue="0.29804000000000003" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
@@ -1386,7 +1386,7 @@ Secret Question 1: What is your childhood best friend's most bizarre superhero f
<rect key="frame" x="0.0" y="383.33333587646484" width="390" height="160"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="J8U-ev-FRQ" id="eb0-vb-Fcc">
<rect key="frame" x="0.0" y="0.0" width="390" height="160"/>
<rect key="frame" x="0.0" y="0.0" width="350" height="160"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lrQ-Ln-ZOv">
@@ -1809,11 +1809,11 @@ Secret Question 1: What is your childhood best friend's most bizarre superhero f
<rect key="frame" x="0.0" y="123.66666793823242" width="390" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="2TD-Vi-g4T" id="RLf-Tg-LcS">
<rect key="frame" x="0.0" y="0.0" width="359.66666666666669" height="61"/>
<rect key="frame" x="0.0" y="0.0" width="331.66666666666669" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Select file ..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="XVY-Dj-6Mx">
<rect key="frame" x="20" y="0.0" width="331.66666666666669" height="61"/>
<rect key="frame" x="8" y="0.0" width="315.66666666666669" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -1830,11 +1830,11 @@ Secret Question 1: What is your childhood best friend's most bizarre superhero f
<rect key="frame" x="0.0" y="240.66666793823242" width="390" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="tT1-V9-E5r" id="cpU-gf-Vk0">
<rect key="frame" x="0.0" y="0.0" width="359.66666666666669" height="61"/>
<rect key="frame" x="0.0" y="0.0" width="331.66666666666669" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Select file ..." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Ka2-8Z-fwx">
<rect key="frame" x="20" y="0.0" width="331.66666666666669" height="61"/>
<rect key="frame" x="8" y="0.0" width="315.66666666666669" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
@@ -1973,16 +1973,16 @@ Secret Question 1: What is your childhood best friend's most bizarre superhero f
<image name="Lock" width="25" height="25"/>
<image name="Settings" width="25" height="25"/>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemGray3Color">
<color red="0.7803921568627451" green="0.7803921568627451" blue="0.80000000000000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.78039215689999997" green="0.78039215689999997" blue="0.80000000000000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemRedColor">
<color red="1" green="0.23137254901960785" blue="0.18823529411764706" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -43,8 +43,8 @@ class AddPasswordTableViewController: PasswordEditorTableViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if segue.identifier == "saveAddPasswordSegue" {
let (name, url) = getNameURL()
password = Password(name: name, url: url, plainText: plainText)
let (name, path) = getNamePath()
password = Password(name: name, path: path, plainText: plainText)
}
}
}

View File

@@ -24,9 +24,9 @@ class EditPasswordTableViewController: PasswordEditorTableViewController {
super.prepare(for: segue, sender: sender)
if segue.identifier == "saveEditPasswordSegue" {
let editedPlainText = plainText
let (name, url) = getNameURL()
if password!.plainText != editedPlainText || password!.url.path != url.path {
password!.updatePassword(name: name, url: url, plainText: editedPlainText)
let (name, path) = getNamePath()
if password!.plainText != editedPlainText || password!.path != path {
password!.updatePassword(name: name, path: path, plainText: editedPlainText)
}
}
}

View File

@@ -71,7 +71,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
navigationItem.rightBarButtonItem = editUIBarButtonItem
navigationItem.largeTitleDisplayMode = .never
if let imageData = passwordEntity?.getImage() {
if let imageData = passwordEntity?.image {
let image = UIImage(data: imageData as Data)
passwordImage = image
}
@@ -158,7 +158,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
self?.editUIBarButtonItem.isEnabled = true
if !Defaults.isHidePasswordImagesOn {
if let urlString = self?.password?.urlString {
if self?.passwordEntity?.getImage() == nil {
if self?.passwordEntity?.image == nil {
self?.updatePasswordImage(urlString: urlString)
}
}
@@ -337,7 +337,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
self?.tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.automatic)
let imageData = image.jpegData(compressionQuality: 1)
if let entity = self?.passwordEntity {
self?.passwordStore.updateImage(passwordEntity: entity, image: imageData)
entity.image = imageData
}
}
}
@@ -443,13 +443,13 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
cell.labelImageConstraint.isActive = false
cell.labelCellConstraint.isActive = true
}
let passwordName = passwordEntity!.getName()
if passwordEntity!.synced == false {
let passwordName = passwordEntity!.name
if passwordEntity!.isSynced == false {
cell.nameLabel.text = "\(passwordName)"
} else {
cell.nameLabel.text = passwordName
}
cell.categoryLabel.text = passwordEntity!.getCategoryText()
cell.categoryLabel.text = passwordEntity!.dirText
cell.selectionStyle = .none
return cell
case .addition, .main:
@@ -504,7 +504,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
footerLabel.numberOfLines = 0
footerLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
footerLabel.textColor = UIColor.gray
let dateString = passwordStore.getLatestUpdateInfo(filename: password!.url.path)
let dateString = passwordStore.getLatestUpdateInfo(path: password!.path)
footerLabel.text = "LastUpdated".localize(dateString)
view.addSubview(footerLabel)
return view
@@ -593,7 +593,7 @@ extension PasswordDetailTableViewController {
handleError(error: AppError.other(message: "PasswordDoesNotExist"))
return
}
let encryptedDataPath = PasswordStore.shared.storeURL.appendingPathComponent(passwordEntity.getPath())
let encryptedDataPath = PasswordStore.shared.storeURL.appendingPathComponent(passwordEntity.path)
guard let encryptedData = try? Data(contentsOf: encryptedDataPath) else {
handleError(error: AppError.other(message: "PasswordDoesNotExist"))
@@ -606,9 +606,7 @@ extension PasswordDetailTableViewController {
guard let decryptedDataString = String(data: decryptedData, encoding: .utf8) else {
throw AppError.yubiKey(.decipher(message: "Failed to convert plaintext to string."))
}
guard let password = try? Password(name: passwordEntity.getName(), url: passwordEntity.getURL(), plainText: decryptedDataString) else {
throw AppError.yubiKey(.decipher(message: "Failed to construct password."))
}
password = Password(name: passwordEntity.name, path: passwordEntity.path, plainText: decryptedDataString)
self.password = password
self.showPassword()
} catch let error as AppError {

View File

@@ -351,11 +351,13 @@ class PasswordEditorTableViewController: UITableViewController {
}
}
func getNameURL() -> (String, URL) {
let encodedName = (nameCell?.getContent()?.stringByAddingPercentEncodingForRFC3986())!
let name = URL(string: encodedName)!.lastPathComponent
let url = URL(string: encodedName)!.appendingPathExtension("gpg")
return (name, url)
func getNamePath() -> (String, String) {
guard let encodedName = nameCell?.getContent()?.stringByAddingPercentEncodingForRFC3986(), let url = URL(string: encodedName) else {
return ("", "")
}
let name = url.lastPathComponent
let path = url.appendingPathExtension("gpg").path
return (name, path)
}
func checkName() -> Bool {

View File

@@ -169,7 +169,7 @@ class PasswordNavigationViewController: UIViewController {
navigationItem.title = "PasswordStore".localize()
} else {
navigationItem.largeTitleDisplayMode = .never
navigationItem.title = parentPasswordEntity?.getName()
navigationItem.title = parentPasswordEntity?.name
}
if viewingUnsyncedPasswords {
navigationItem.title = "Unsynced"
@@ -261,7 +261,7 @@ class PasswordNavigationViewController: UIViewController {
if passwordTableEntry.isDir {
return
}
passwordManager.providePasswordPasteboard(with: passwordTableEntry.passwordEntity.getPath())
passwordManager.providePasswordPasteboard(with: passwordTableEntry.passwordEntity.path)
}
}
}
@@ -335,7 +335,7 @@ extension PasswordNavigationViewController {
} else if segue.identifier == "addPasswordSegue" {
if let navController = segue.destination as? UINavigationController,
let viewController = navController.topViewController as? AddPasswordTableViewController,
let path = parentPasswordEntity?.getPath() {
let path = parentPasswordEntity?.path {
viewController.defaultDirPrefix = "\(path)/"
}
}

View File

@@ -76,7 +76,7 @@ class PasswordNavigationDataSource: NSObject, UITableViewDataSource {
func showUnsyncedTableEntries() {
filteredSections = sections.map { section in
let entries = section.entries.filter { !$0.synced }
let entries = section.entries.filter { !$0.isSynced }
return Section(title: section.title, entries: entries)
}
.filter { !$0.entries.isEmpty }

View File

@@ -11,7 +11,7 @@ import passKit
class PasswordTableViewCell: UITableViewCell {
func configure(with entry: PasswordTableEntry) {
textLabel?.font = UIFont.preferredFont(forTextStyle: .body)
textLabel?.text = entry.passwordEntity.synced ? entry.title : "\(entry.title)"
textLabel?.text = entry.passwordEntity.isSynced ? entry.title : "\(entry.title)"
textLabel?.adjustsFontForContentSizeCategory = true
accessoryType = .none
@@ -24,7 +24,7 @@ class PasswordTableViewCell: UITableViewCell {
accessoryType = .disclosureIndicator
textLabel?.font = UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: .body).pointSize, weight: .medium)
detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .body)
detailTextLabel?.text = "\(entry.passwordEntity.children?.count ?? 0)"
detailTextLabel?.text = "\(entry.passwordEntity.children.count)"
}
}
}

View File

@@ -73,6 +73,6 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
extension CredentialProviderViewController: PasswordSelectionDelegate {
func selected(password: PasswordTableEntry) {
credentialProvider.persistAndProvideCredentials(with: password.passwordEntity.getPath())
credentialProvider.persistAndProvideCredentials(with: password.passwordEntity.path)
}
}

View File

@@ -89,9 +89,9 @@ extension ExtensionViewController: PasswordSelectionDelegate {
func selected(password: PasswordTableEntry) {
switch action {
case .findLogin:
credentialProvider.provideCredentialsFindLogin(with: password.passwordEntity.getPath())
credentialProvider.provideCredentialsFindLogin(with: password.passwordEntity.path)
case .fillBrowser:
credentialProvider.provideCredentialsBrowser(with: password.passwordEntity.getPath())
credentialProvider.provideCredentialsBrowser(with: password.passwordEntity.path)
default:
extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}

View File

@@ -0,0 +1,90 @@
//
// CoreDataStack.swift
// passKit
//
// Created by Mingshen Sun on 12/28/24.
// Copyright © 2024 Bob Sun. All rights reserved.
//
import CoreData
public class PersistenceController {
public static let shared = PersistenceController()
static let modelName = "pass"
public func viewContext() -> NSManagedObjectContext {
container.viewContext
}
let container: NSPersistentContainer
init(isUnitTest: Bool = false) {
self.container = NSPersistentContainer(name: Self.modelName, managedObjectModel: .sharedModel)
if isUnitTest {
let description = NSPersistentStoreDescription()
description.url = URL(fileURLWithPath: "/dev/null")
container.persistentStoreDescriptions = [description]
}
}
public func setup() {
container.loadPersistentStores { _, error in
if error != nil {
self.reinitializePersistentStore()
}
}
}
func reinitializePersistentStore() {
deletePersistentStore()
container.loadPersistentStores { _, finalError in
if let finalError {
fatalError("Failed to load persistent stores: \(finalError.localizedDescription)")
}
}
PasswordEntity.initPasswordEntityCoreData(url: Globals.repositoryURL, in: container.viewContext)
try? container.viewContext.save()
}
func deletePersistentStore(inMemoryStore: Bool = false) {
let coordinator = container.persistentStoreCoordinator
guard let storeURL = container.persistentStoreDescriptions.first?.url else {
return
}
do {
if #available(iOS 15.0, *) {
let storeType: NSPersistentStore.StoreType = inMemoryStore ? .inMemory : .sqlite
try coordinator.destroyPersistentStore(at: storeURL, type: storeType)
} else {
let storeType: String = inMemoryStore ? NSInMemoryStoreType : NSSQLiteStoreType
try coordinator.destroyPersistentStore(at: storeURL, ofType: storeType)
}
} catch {
fatalError("Failed to destroy persistent store: \(error)")
}
}
public func save() {
let context = viewContext()
if context.hasChanges {
do {
try context.save()
} catch {
fatalError("Failed to save changes: \(error)")
}
}
}
}
extension NSManagedObjectModel {
static let sharedModel: NSManagedObjectModel = {
let url = Bundle(identifier: Globals.passKitBundleIdentifier)!.url(forResource: "pass", withExtension: "momd")!
guard let managedObjectModel = NSManagedObjectModel(contentsOf: url) else {
fatalError("Failed to create managed object model: \(url)")
}
return managedObjectModel
}()
}

View File

@@ -27,7 +27,8 @@ public final class Globals {
public static let pgpPublicKeyPath = documentPath + "/gpg_key.pub"
public static let pgpPrivateKeyPath = documentPath + "/gpg_key"
public static let gitSSHPrivateKeyPath = documentPath + "/ssh_key"
public static let repositoryPath = libraryPath + "/password-store"
public static let repositoryURL = sharedContainerURL.appendingPathComponent("Library/password-store/")
public static let dbPath = documentPath + "/pass.sqlite"
public static let iTunesFileSharingPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]

View File

@@ -11,7 +11,7 @@ import OneTimePassword
public class Password {
public var name: String
public var url: URL
public var path: String
public var plainText: String
public var changed: Int = 0
@@ -28,11 +28,11 @@ public class Password {
}
public var namePath: String {
url.deletingPathExtension().path
(path as NSString).deletingPathExtension
}
public var nameFromPath: String? {
url.deletingPathExtension().path.split(separator: "/").last.map { String($0) }
URL(string: path)?.deletingPathExtension().pathComponents.last
}
public var password: String {
@@ -75,15 +75,15 @@ public class Password {
additions.map(\.title).filter(Constants.isOtpKeyword).count - (firstLineIsOTPField ? 1 : 0)
}
public init(name: String, url: URL, plainText: String) {
public init(name: String, path: String, plainText: String) {
self.name = name
self.url = url
self.path = path
self.plainText = plainText
initEverything()
}
public func updatePassword(name: String, url: URL, plainText: String) {
guard self.plainText != plainText || self.url != url else {
public func updatePassword(name: String, path: String, plainText: String) {
guard self.plainText != plainText || self.path != path else {
return
}
@@ -91,8 +91,8 @@ public class Password {
self.plainText = plainText
changed |= PasswordChange.content.rawValue
}
if self.url != url {
self.url = url
if self.path != path {
self.path = path
changed |= PasswordChange.path.rawValue
}
@@ -213,7 +213,7 @@ public class Password {
if newOtpauth != nil {
lines.append(newOtpauth!)
}
updatePassword(name: name, url: url, plainText: lines.joined(separator: "\n"))
updatePassword(name: name, path: path, plainText: lines.joined(separator: "\n"))
// get and return the password
return otpToken?.currentPassword

View File

@@ -6,56 +6,208 @@
// Copyright © 2017 Bob Sun. All rights reserved.
//
import CoreData
import Foundation
import ObjectiveGit
import SwiftyUserDefaults
public extension PasswordEntity {
var nameWithCategory: String {
if let path {
if path.hasSuffix(".gpg") {
return String(path.prefix(upTo: path.index(path.endIndex, offsetBy: -4)))
}
return path
}
return ""
public final class PasswordEntity: NSManagedObject, Identifiable {
/// Name of the password, i.e., filename without extension.
@NSManaged public var name: String
/// A Boolean value indicating whether the entity is a directory.
@NSManaged public var isDir: Bool
/// A Boolean value indicating whether the entity is synced with remote repository.
@NSManaged public var isSynced: Bool
/// The relative file path of the password or directory.
@NSManaged public var path: String
/// The thumbnail image of the password if there is a url entry in the password.
@NSManaged public var image: Data?
/// The parent password entity.
@NSManaged public var parent: PasswordEntity?
/// A set of child password entities.
@NSManaged public var children: Set<PasswordEntity>
@nonobjc
public static func fetchRequest() -> NSFetchRequest<PasswordEntity> {
NSFetchRequest<PasswordEntity>(entityName: "PasswordEntity")
}
func getCategoryText() -> String {
getCategoryArray().joined(separator: " > ")
/// A String value with password directory and name, i.e., path without extension.
public var nameWithDir: String {
(path as NSString).deletingPathExtension
}
func getCategoryArray() -> [String] {
public var dirText: String {
getDirArray().joined(separator: " > ")
}
public func getDirArray() -> [String] {
var parentEntity = parent
var passwordCategoryArray: [String] = []
while parentEntity != nil {
passwordCategoryArray.append(parentEntity!.name!)
parentEntity = parentEntity!.parent
while let current = parentEntity {
passwordCategoryArray.append(current.name)
parentEntity = current.parent
}
passwordCategoryArray.reverse()
return passwordCategoryArray
}
func getURL() throws -> URL {
if let path = getPath().stringByAddingPercentEncodingForRFC3986(), let url = URL(string: path) {
return url
public static func fetchAll(in context: NSManagedObjectContext) -> [PasswordEntity] {
let request = Self.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
return (try? context.fetch(request) as? [Self]) ?? []
}
public static func fetchAllPassword(in context: NSManagedObjectContext) -> [PasswordEntity] {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "isDir = false")
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
return (try? context.fetch(request) as? [Self]) ?? []
}
public static func totalNumber(in context: NSManagedObjectContext) -> Int {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "isDir = false")
return (try? context.count(for: request)) ?? 0
}
public static func fetchUnsynced(in context: NSManagedObjectContext) -> [PasswordEntity] {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "isSynced = false")
return (try? context.fetch(request) as? [Self]) ?? []
}
public static func fetch(by path: String, in context: NSManagedObjectContext) -> PasswordEntity? {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "path = %@", path)
return try? context.fetch(request).first as? Self
}
public static func fetch(by path: String, isDir: Bool, in context: NSManagedObjectContext) -> PasswordEntity? {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "path = %@ and isDir = %@", path, isDir as NSNumber)
return try? context.fetch(request).first as? Self
}
public static func fetch(by parent: PasswordEntity?, in context: NSManagedObjectContext) -> [PasswordEntity] {
let request = Self.fetchRequest()
request.predicate = NSPredicate(format: "parent = %@", parent ?? 0)
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
return (try? context.fetch(request) as? [Self]) ?? []
}
public static func updateAllToSynced(in context: NSManagedObjectContext) -> Int {
let request = NSBatchUpdateRequest(entity: Self.entity())
request.resultType = .updatedObjectsCountResultType
request.predicate = NSPredicate(format: "isSynced = false")
request.propertiesToUpdate = ["isSynced": true]
let result = try? context.execute(request) as? NSBatchUpdateResult
return result?.result as? Int ?? 0
}
public static func deleteRecursively(entity: PasswordEntity, in context: NSManagedObjectContext) {
var currentEntity: PasswordEntity? = entity
while let node = currentEntity, node.children.isEmpty {
let parent = node.parent
context.delete(node)
try? context.save()
currentEntity = parent
}
throw AppError.other(message: "cannot decode URL")
}
// XXX: define some getters to get core data, we need to consider
// manually write models instead auto generation.
func getImage() -> Data? {
image
public static func deleteAll(in context: NSManagedObjectContext) {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: Self.fetchRequest())
_ = try? context.execute(deleteRequest)
}
func getName() -> String {
// unwrap non-optional core data
name ?? ""
public static func exists(password: Password, in context: NSManagedObjectContext) -> Bool {
let request = fetchRequest()
request.predicate = NSPredicate(format: "name = %@ and path = %@ and isDir = false", password.name, password.path)
if let count = try? context.count(for: request) {
return count > 0
}
return false
}
func getPath() -> String {
// unwrap non-optional core data
path ?? ""
@discardableResult
public static func insert(name: String, path: String, isDir: Bool, into context: NSManagedObjectContext) -> PasswordEntity {
let entity = PasswordEntity(context: context)
entity.name = name
entity.path = path
entity.isDir = isDir
entity.isSynced = false
return entity
}
public static func initPasswordEntityCoreData(url: URL, in context: NSManagedObjectContext) {
let localFileManager = FileManager.default
let root = {
let entity = PasswordEntity(context: context)
entity.name = "root"
entity.isDir = true
entity.path = ""
return entity
}()
var queue = [root]
while !queue.isEmpty {
let current = queue.removeFirst()
let resourceKeys = Set<URLResourceKey>([.nameKey, .isDirectoryKey])
let options = FileManager.DirectoryEnumerationOptions([.skipsHiddenFiles, .skipsSubdirectoryDescendants])
let currentURL = url.appendingPathComponent(current.path)
guard let directoryEnumerator = localFileManager.enumerator(at: currentURL, includingPropertiesForKeys: Array(resourceKeys), options: options) else {
continue
}
for case let fileURL as URL in directoryEnumerator {
guard let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys),
let isDirectory = resourceValues.isDirectory,
let name = resourceValues.name
else {
continue
}
let passwordEntity = PasswordEntity(context: context)
passwordEntity.isDir = isDirectory
if isDirectory {
passwordEntity.name = name
queue.append(passwordEntity)
} else {
if (name as NSString).pathExtension == "gpg" {
passwordEntity.name = (name as NSString).deletingPathExtension
} else {
passwordEntity.name = name
}
}
passwordEntity.parent = current
let path = String(fileURL.path.replacingOccurrences(of: url.path, with: "").drop(while: { $0 == "/" }))
passwordEntity.path = path
}
}
context.delete(root)
}
}
public extension PasswordEntity {
@objc(addChildrenObject:)
@NSManaged
func addToChildren(_ value: PasswordEntity)
@objc(removeChildrenObject:)
@NSManaged
func removeFromChildren(_ value: PasswordEntity)
@objc(addChildren:)
@NSManaged
func addToChildren(_ values: NSSet)
@objc(removeChildren:)
@NSManaged
func removeFromChildren(_ values: NSSet)
}

View File

@@ -54,35 +54,10 @@ public class PasswordStore {
}
private let fileManager = FileManager.default
private lazy var context: NSManagedObjectContext = {
let modelURL = Bundle(identifier: Globals.passKitBundleIdentifier)!.url(forResource: "pass", withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)
let container = NSPersistentContainer(name: "pass", managedObjectModel: managedObjectModel!)
if FileManager.default.fileExists(atPath: Globals.documentPath) {
try! FileManager.default.createDirectory(atPath: Globals.documentPath, withIntermediateDirectories: true, attributes: nil)
}
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: URL(fileURLWithPath: Globals.dbPath))]
container.loadPersistentStores { _, error in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
// Typical reasons for an error here include:
//
// * The parent directory does not exist, cannot be created, or disallows writing.
// * The persistent store is not accessible, due to permissions or data protection when the device is locked.
// * The device is out of space.
// * The store could not be migrated to the current model version.
//
// Check the error message to determine what the actual problem was.
fatalError("UnresolvedError".localize("\(error.localizedDescription), \(error.userInfo)"))
}
}
return container.viewContext
}()
private lazy var context: NSManagedObjectContext = PersistenceController.shared.viewContext()
public var numberOfPasswords: Int {
fetchPasswordEntityCoreData(withDir: false).count
PasswordEntity.totalNumber(in: context)
}
public var sizeOfRepositoryByteCount: UInt64 {
@@ -111,7 +86,7 @@ public class PasswordStore {
storeRepository?.numberOfCommits(inCurrentBranch: nil)
}
init(url: URL = URL(fileURLWithPath: "\(Globals.repositoryPath)")) {
init(url: URL = Globals.repositoryURL) {
self.storeURL = url
// Migration
@@ -137,27 +112,15 @@ public class PasswordStore {
}
public func repositoryExists() -> Bool {
fileManager.fileExists(atPath: Globals.repositoryPath)
fileManager.fileExists(atPath: Globals.repositoryURL.path)
}
public func passwordExisted(password: Password) -> Bool {
let passwordEntityFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "PasswordEntity")
do {
passwordEntityFetchRequest.predicate = NSPredicate(format: "name = %@ and path = %@", password.name, password.url.path)
return try context.count(for: passwordEntityFetchRequest) > 0
} catch {
fatalError("FailedToFetchPasswordEntities".localize(error))
}
PasswordEntity.exists(password: password, in: context)
}
public func getPasswordEntity(by path: String, isDir: Bool) -> PasswordEntity? {
let passwordEntityFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "PasswordEntity")
do {
passwordEntityFetchRequest.predicate = NSPredicate(format: "path = %@ and isDir = %@", path, isDir as NSNumber)
return try context.fetch(passwordEntityFetchRequest).first as? PasswordEntity
} catch {
fatalError("FailedToFetchPasswordEntities".localize(error))
}
PasswordEntity.fetch(by: path, isDir: isDir, in: context)
}
public func cloneRepository(
@@ -186,14 +149,15 @@ public class PasswordStore {
} catch {
Defaults.lastSyncedTime = nil
DispatchQueue.main.async {
self.deleteCoreData(entityName: "PasswordEntity")
self.deleteCoreData()
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
}
throw (error)
}
Defaults.lastSyncedTime = Date()
DispatchQueue.main.async {
self.updatePasswordEntityCoreData()
self.deleteCoreData()
self.initPasswordEntityCoreData()
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
}
}
@@ -226,60 +190,14 @@ public class PasswordStore {
Defaults.lastSyncedTime = Date()
setAllSynced()
DispatchQueue.main.async {
self.updatePasswordEntityCoreData()
self.deleteCoreData()
self.initPasswordEntityCoreData()
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
}
}
private func updatePasswordEntityCoreData() {
deleteCoreData(entityName: "PasswordEntity")
do {
var entities = try fileManager.contentsOfDirectory(atPath: storeURL.path)
.filter { !$0.hasPrefix(".") }
.map { filename -> PasswordEntity in
let passwordEntity = NSEntityDescription.insertNewObject(forEntityName: "PasswordEntity", into: context) as! PasswordEntity
if filename.hasSuffix(".gpg") {
passwordEntity.name = String(filename.prefix(upTo: filename.index(filename.endIndex, offsetBy: -4)))
} else {
passwordEntity.name = filename
}
passwordEntity.path = filename
passwordEntity.parent = nil
return passwordEntity
}
while !entities.isEmpty {
let entity = entities.first!
entities.remove(at: 0)
guard !entity.name!.hasPrefix(".") else {
continue
}
var isDirectory: ObjCBool = false
let filePath = storeURL.appendingPathComponent(entity.path!).path
if fileManager.fileExists(atPath: filePath, isDirectory: &isDirectory) {
if isDirectory.boolValue {
entity.isDir = true
let files = try fileManager.contentsOfDirectory(atPath: filePath)
.filter { !$0.hasPrefix(".") }
.map { filename -> PasswordEntity in
let passwordEntity = NSEntityDescription.insertNewObject(forEntityName: "PasswordEntity", into: context) as! PasswordEntity
if filename.hasSuffix(".gpg") {
passwordEntity.name = String(filename.prefix(upTo: filename.index(filename.endIndex, offsetBy: -4)))
} else {
passwordEntity.name = filename
}
passwordEntity.path = "\(entity.path!)/\(filename)"
passwordEntity.parent = entity
return passwordEntity
}
entities += files
} else {
entity.isDir = false
}
}
}
} catch {
print(error)
}
private func initPasswordEntityCoreData() {
PasswordEntity.initPasswordEntityCoreData(url: storeURL, in: context)
saveUpdatedContext()
}
@@ -301,65 +219,31 @@ public class PasswordStore {
}
public func fetchPasswordEntityCoreData(parent: PasswordEntity?) -> [PasswordEntity] {
let passwordEntityFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "PasswordEntity")
do {
passwordEntityFetch.predicate = NSPredicate(format: "parent = %@", parent ?? 0)
let fetchedPasswordEntities = try context.fetch(passwordEntityFetch) as! [PasswordEntity]
return fetchedPasswordEntities.sorted { $0.name!.caseInsensitiveCompare($1.name!) == .orderedAscending }
} catch {
fatalError("FailedToFetchPasswords".localize(error))
}
PasswordEntity.fetch(by: parent, in: context)
}
public func fetchPasswordEntityCoreData(withDir: Bool) -> [PasswordEntity] {
let passwordEntityFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "PasswordEntity")
do {
if !withDir {
passwordEntityFetch.predicate = NSPredicate(format: "isDir = false")
}
let fetchedPasswordEntities = try context.fetch(passwordEntityFetch) as! [PasswordEntity]
return fetchedPasswordEntities.sorted { $0.name!.caseInsensitiveCompare($1.name!) == .orderedAscending }
} catch {
fatalError("FailedToFetchPasswords".localize(error))
}
public func fetchPasswordEntityCoreData(withDir _: Bool) -> [PasswordEntity] {
PasswordEntity.fetchAllPassword(in: context)
}
public func fetchUnsyncedPasswords() -> [PasswordEntity] {
let passwordEntityFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "PasswordEntity")
passwordEntityFetchRequest.predicate = NSPredicate(format: "synced = %i", 0)
do {
return try context.fetch(passwordEntityFetchRequest) as! [PasswordEntity]
} catch {
fatalError("FailedToFetchPasswords".localize(error))
}
PasswordEntity.fetchUnsynced(in: context)
}
public func fetchPasswordEntity(with path: String) -> PasswordEntity? {
let passwordEntityFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "PasswordEntity")
passwordEntityFetchRequest.predicate = NSPredicate(format: "path = %@", path)
do {
let passwordEntities = try context.fetch(passwordEntityFetchRequest) as! [PasswordEntity]
return passwordEntities.first
} catch {
fatalError("FailedToFetchPasswords".localize(error))
}
PasswordEntity.fetch(by: path, in: context)
}
public func setAllSynced() {
let passwordEntities = fetchUnsyncedPasswords()
if !passwordEntities.isEmpty {
for passwordEntity in passwordEntities {
passwordEntity.synced = true
}
saveUpdatedContext()
}
_ = PasswordEntity.updateAllToSynced(in: context)
saveUpdatedContext()
}
public func getLatestUpdateInfo(filename: String) -> String {
public func getLatestUpdateInfo(path: String) -> String {
guard let storeRepository else {
return "Unknown".localize()
}
guard let blameHunks = try? storeRepository.blame(withFile: filename, options: nil).hunks else {
guard let blameHunks = try? storeRepository.blame(withFile: path, options: nil).hunks else {
return "Unknown".localize()
}
guard let latestCommitTime = blameHunks.map({ $0.finalSignature?.time?.timeIntervalSince1970 ?? 0 }).max() else {
@@ -393,18 +277,16 @@ public class PasswordStore {
}
private func deleteDirectoryTree(at url: URL) throws {
var tempURL = storeURL.appendingPathComponent(url.deletingLastPathComponent().path)
var count = try fileManager.contentsOfDirectory(atPath: tempURL.path).count
while count == 0 {
var tempURL = url.deletingLastPathComponent()
while try fileManager.contentsOfDirectory(atPath: tempURL.path).isEmpty {
try fileManager.removeItem(at: tempURL)
tempURL.deleteLastPathComponent()
count = try fileManager.contentsOfDirectory(atPath: tempURL.path).count
}
}
private func createDirectoryTree(at url: URL) throws {
let tempURL = storeURL.appendingPathComponent(url.deletingLastPathComponent().path)
try fileManager.createDirectory(at: tempURL, withIntermediateDirectories: true, attributes: nil)
let tempURL = url.deletingLastPathComponent()
try fileManager.createDirectory(at: tempURL, withIntermediateDirectories: true)
}
private func gitMv(from: String, to: String) throws {
@@ -460,148 +342,99 @@ public class PasswordStore {
throw AppError.passwordDuplicated
}
var passwordURL = password.url
var previousPathLength = Int.max
var paths: [String] = []
while passwordURL.path != "." {
paths.append(passwordURL.path)
passwordURL = passwordURL.deletingLastPathComponent()
// better identify errors before saving a new password
if passwordURL.path != ".", passwordURL.path.count >= previousPathLength {
throw AppError.wrongPasswordFilename
}
previousPathLength = passwordURL.path.count
}
paths.reverse()
var parentPasswordEntity: PasswordEntity?
for path in paths {
let isDir = !path.hasSuffix(".gpg")
if let passwordEntity = getPasswordEntity(by: path, isDir: isDir) {
passwordEntity.synced = false
parentPasswordEntity = passwordEntity
} else {
let passwordEntity = NSEntityDescription.insertNewObject(forEntityName: "PasswordEntity", into: context) as! PasswordEntity
let pathURL = URL(string: path.stringByAddingPercentEncodingForRFC3986()!)!
if isDir {
passwordEntity.name = pathURL.lastPathComponent
} else {
passwordEntity.name = pathURL.deletingPathExtension().lastPathComponent
}
passwordEntity.path = path
passwordEntity.parent = parentPasswordEntity
passwordEntity.synced = false
passwordEntity.isDir = isDir
parentPasswordEntity = passwordEntity
}
var path = password.path
while !path.isEmpty {
paths.append(path)
path = (path as NSString).deletingLastPathComponent
}
var parentPasswordEntity: PasswordEntity?
for (index, path) in paths.reversed().enumerated() {
if index == paths.count - 1 {
let passwordEntity = PasswordEntity.insert(name: password.name, path: path, isDir: false, into: context)
passwordEntity.parent = parentPasswordEntity
parentPasswordEntity = passwordEntity
} else {
if let passwordEntity = PasswordEntity.fetch(by: path, isDir: true, in: context) {
passwordEntity.isSynced = false
parentPasswordEntity = passwordEntity
} else {
let name = (path as NSString).lastPathComponent
let passwordEntity = PasswordEntity.insert(name: name, path: path, isDir: true, into: context)
passwordEntity.parent = parentPasswordEntity
parentPasswordEntity = passwordEntity
}
}
}
saveUpdatedContext()
return parentPasswordEntity
}
public func add(password: Password, keyID: String? = nil) throws -> PasswordEntity? {
try createDirectoryTree(at: password.url)
let saveURL = storeURL.appendingPathComponent(password.url.path)
let saveURL = storeURL.appendingPathComponent(password.path)
try createDirectoryTree(at: saveURL)
try encrypt(password: password, keyID: keyID).write(to: saveURL)
try gitAdd(path: password.url.path)
_ = try gitCommit(message: "AddPassword.".localize(password.url.deletingPathExtension().path))
try gitAdd(path: password.path)
_ = try gitCommit(message: "AddPassword.".localize(password.path))
let newPasswordEntity = try addPasswordEntities(password: password)
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
return newPasswordEntity
}
public func delete(passwordEntity: PasswordEntity) throws {
let deletedFileURL = try passwordEntity.getURL()
try gitRm(path: deletedFileURL.path)
let deletedFileURL = storeURL.appendingPathComponent(passwordEntity.path)
try gitRm(path: passwordEntity.path)
try deletePasswordEntities(passwordEntity: passwordEntity)
try deleteDirectoryTree(at: deletedFileURL)
_ = try gitCommit(message: "RemovePassword.".localize(deletedFileURL.deletingPathExtension().path.removingPercentEncoding!))
_ = try gitCommit(message: "RemovePassword.".localize(passwordEntity.path))
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
}
public func edit(passwordEntity: PasswordEntity, password: Password, keyID: String? = nil) throws -> PasswordEntity? {
var newPasswordEntity: PasswordEntity? = passwordEntity
let url = try passwordEntity.getURL()
let url = storeURL.appendingPathComponent(passwordEntity.path)
if password.changed & PasswordChange.content.rawValue != 0 {
let saveURL = storeURL.appendingPathComponent(url.path)
try encrypt(password: password, keyID: keyID).write(to: saveURL)
try gitAdd(path: url.path)
_ = try gitCommit(message: "EditPassword.".localize(url.deletingPathExtension().path.removingPercentEncoding!))
try encrypt(password: password, keyID: keyID).write(to: url)
try gitAdd(path: password.path)
_ = try gitCommit(message: "EditPassword.".localize(passwordEntity.path))
newPasswordEntity = passwordEntity
newPasswordEntity?.synced = false
saveUpdatedContext()
newPasswordEntity?.isSynced = false
}
if password.changed & PasswordChange.path.rawValue != 0 {
let deletedFileURL = url
// add
try createDirectoryTree(at: password.url)
let newFileURL = storeURL.appendingPathComponent(password.path)
try createDirectoryTree(at: newFileURL)
newPasswordEntity = try addPasswordEntities(password: password)
// mv
try gitMv(from: deletedFileURL.path, to: password.url.path)
try gitMv(from: passwordEntity.path, to: password.path)
// delete
try deleteDirectoryTree(at: deletedFileURL)
try deletePasswordEntities(passwordEntity: passwordEntity)
_ = try gitCommit(message: "RenamePassword.".localize(deletedFileURL.deletingPathExtension().path.removingPercentEncoding!, password.url.deletingPathExtension().path.removingPercentEncoding!))
_ = try gitCommit(message: "RenamePassword.".localize(passwordEntity.path, password.path))
}
saveUpdatedContext()
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
return newPasswordEntity
}
private func deletePasswordEntities(passwordEntity: PasswordEntity) throws {
var current: PasswordEntity? = passwordEntity
// swiftformat:disable:next isEmpty
while current != nil, current!.children!.count == 0 || !current!.isDir {
let parent = current!.parent
context.delete(current!)
current = parent
}
PasswordEntity.deleteRecursively(entity: passwordEntity, in: context)
saveUpdatedContext()
}
public func saveUpdatedContext() {
do {
if context.hasChanges {
try context.save()
}
} catch {
fatalError("FailureToSaveContext".localize(error))
}
PersistenceController.shared.save()
}
public func deleteCoreData(entityName: String) {
let deleteFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: deleteFetchRequest)
do {
try context.execute(deleteRequest)
try context.save()
context.reset()
} catch let error as NSError {
print(error)
}
}
public func updateImage(passwordEntity: PasswordEntity, image: Data?) {
guard let image else {
return
}
let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateMOC.parent = context
privateMOC.perform {
passwordEntity.image = image
do {
try privateMOC.save()
self.context.performAndWait {
self.saveUpdatedContext()
}
} catch {
fatalError("FailureToSaveContext".localize(error))
}
}
public func deleteCoreData() {
PasswordEntity.deleteAll(in: context)
PersistenceController.shared.save()
}
public func eraseStoreData() {
@@ -610,7 +443,7 @@ public class PasswordStore {
try? fileManager.removeItem(at: tempStoreURL)
// Delete core data.
deleteCoreData(entityName: "PasswordEntity")
deleteCoreData()
// Clean up variables inside PasswordStore.
storeRepository = nil
@@ -652,7 +485,8 @@ public class PasswordStore {
}
try storeRepository.reset(to: newHead, resetType: .hard)
setAllSynced()
updatePasswordEntityCoreData()
deleteCoreData()
initPasswordEntityCoreData()
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
NotificationCenter.default.post(name: .passwordStoreChangeDiscarded, object: nil)
@@ -678,11 +512,11 @@ public class PasswordStore {
}
public func decrypt(passwordEntity: PasswordEntity, keyID: String? = nil, requestPGPKeyPassphrase: @escaping (String) -> String) throws -> Password {
let encryptedDataPath = storeURL.appendingPathComponent(passwordEntity.getPath())
let encryptedData = try Data(contentsOf: encryptedDataPath)
let url = storeURL.appendingPathComponent(passwordEntity.path)
let encryptedData = try Data(contentsOf: url)
let data: Data? = try {
if Defaults.isEnableGPGIDOn {
let keyID = keyID ?? findGPGID(from: encryptedDataPath)
let keyID = keyID ?? findGPGID(from: url)
return try PGPAgent.shared.decrypt(encryptedData: encryptedData, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
}
return try PGPAgent.shared.decrypt(encryptedData: encryptedData, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
@@ -691,8 +525,7 @@ public class PasswordStore {
throw AppError.decryption
}
let plainText = String(data: decryptedData, encoding: .utf8) ?? ""
let url = try passwordEntity.getURL()
return Password(name: passwordEntity.getName(), url: url, plainText: plainText)
return Password(name: passwordEntity.name, path: passwordEntity.path, plainText: plainText)
}
public func decrypt(path: String, keyID: String? = nil, requestPGPKeyPassphrase: @escaping (String) -> String) throws -> Password {
@@ -706,7 +539,7 @@ public class PasswordStore {
}
public func encrypt(password: Password, keyID: String? = nil) throws -> Data {
let encryptedDataPath = storeURL.appendingPathComponent(password.url.path)
let encryptedDataPath = storeURL.appendingPathComponent(password.path)
let keyID = keyID ?? findGPGID(from: encryptedDataPath)
if Defaults.isEnableGPGIDOn {
return try PGPAgent.shared.encrypt(plainData: password.plainData, keyID: keyID)

View File

@@ -12,19 +12,19 @@ public class PasswordTableEntry: NSObject {
public let passwordEntity: PasswordEntity
@objc public let title: String
public let isDir: Bool
public let synced: Bool
public let isSynced: Bool
public let categoryText: String
public init(_ entity: PasswordEntity) {
self.passwordEntity = entity
self.title = entity.name!
self.title = entity.name
self.isDir = entity.isDir
self.synced = entity.synced
self.categoryText = entity.getCategoryText()
self.isSynced = entity.isSynced
self.categoryText = entity.dirText
}
public func matches(_ searchText: String) -> Bool {
Self.match(nameWithCategory: passwordEntity.nameWithCategory, searchText: searchText)
Self.match(nameWithCategory: passwordEntity.nameWithDir, searchText: searchText)
}
public static func match(nameWithCategory: String, searchText: String) -> Bool {

View File

@@ -1,15 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="12141" systemVersion="16F73" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="PasswordEntity" representedClassName="PasswordEntity" syncable="YES" codeGenerationType="class">
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
<attribute name="isDir" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="path" attributeType="String" syncable="YES"/>
<attribute name="synced" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<relationship name="children" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PasswordEntity" inverseName="parent" inverseEntity="PasswordEntity" syncable="YES"/>
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PasswordEntity" inverseName="children" inverseEntity="PasswordEntity" syncable="YES"/>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24B91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="PasswordEntity" representedClassName=".PasswordEntity" syncable="YES">
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
<attribute name="isDir" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isSynced" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String"/>
<attribute name="path" attributeType="String"/>
<relationship name="children" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PasswordEntity" inverseName="parent" inverseEntity="PasswordEntity"/>
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PasswordEntity" inverseName="children" inverseEntity="PasswordEntity"/>
</entity>
<elements>
<element name="PasswordEntity" positionX="36" positionY="81" width="128" height="150"/>
</elements>
</model>

View File

@@ -0,0 +1,31 @@
//
// CoreDataTestCase.swift
// pass
//
// Created by Mingshen Sun on 1/4/25.
// Copyright © 2025 Bob Sun. All rights reserved.
//
import CoreData
import Foundation
import XCTest
@testable import passKit
// swiftlint:disable:next final_test_case
class CoreDataTestCase: XCTestCase {
// swiftlint:disable:next test_case_accessibility
private(set) var controller: PersistenceController!
override func setUpWithError() throws {
try super.setUpWithError()
controller = PersistenceController(isUnitTest: true)
controller.setup()
}
override func tearDown() {
super.tearDown()
controller = nil
}
}

View File

@@ -0,0 +1,88 @@
//
// PasswordEntityTest.swift
// pass
//
// Created by Mingshen Sun on 1/4/25.
// Copyright © 2025 Bob Sun. All rights reserved.
//
import CoreData
import XCTest
@testable import passKit
final class PasswordEntityTest: CoreDataTestCase {
func testFetchAll() throws {
let context = controller.viewContext()
let expectedCount = 5
(0 ..< expectedCount).forEach { index in
let name = String(format: "Generated %05d", index)
let path = String(format: "/%05d", index)
PasswordEntity.insert(name: name, path: path, isDir: false, into: context)
}
let count = PasswordEntity.fetchAllPassword(in: context).count
XCTAssertEqual(expectedCount, count)
PasswordEntity.deleteAll(in: context)
}
func testTotalNumber() throws {
let context = controller.viewContext()
PasswordEntity.insert(name: "1", path: "path1", isDir: false, into: context)
PasswordEntity.insert(name: "2", path: "path2", isDir: false, into: context)
PasswordEntity.insert(name: "3", path: "path3", isDir: true, into: context)
XCTAssertEqual(2, PasswordEntity.totalNumber(in: context))
PasswordEntity.deleteAll(in: context)
}
func testFetchUnsynced() throws {
let context = controller.viewContext()
let syncedPasswordEntity = PasswordEntity.insert(name: "1", path: "path", isDir: false, into: context)
syncedPasswordEntity.isSynced = true
let expectedCount = 5
(0 ..< expectedCount).forEach { index in
let name = String(format: "Generated %05d", index)
let path = String(format: "/%05d", index)
PasswordEntity.insert(name: name, path: path, isDir: false, into: context)
}
let count = PasswordEntity.fetchUnsynced(in: context).count
XCTAssertEqual(expectedCount, count)
PasswordEntity.deleteAll(in: context)
}
func testFetchByPath() throws {
let context = controller.viewContext()
PasswordEntity.insert(name: "1", path: "path1", isDir: false, into: context)
PasswordEntity.insert(name: "2", path: "path2", isDir: true, into: context)
let passwordEntity = PasswordEntity.fetch(by: "path1", in: context)!
XCTAssertEqual(passwordEntity.path, "path1")
XCTAssertEqual(passwordEntity.name, "1")
}
func testFetchByParent() throws {
let context = controller.viewContext()
let parent = PasswordEntity.insert(name: "parent", path: "path1", isDir: true, into: context)
let child1 = PasswordEntity.insert(name: "child1", path: "path2", isDir: false, into: context)
let child2 = PasswordEntity.insert(name: "child2", path: "path3", isDir: true, into: context)
let child3 = PasswordEntity.insert(name: "child3", path: "path4", isDir: false, into: context)
parent.children = [child1, child2]
child2.children = [child3]
let childern = PasswordEntity.fetch(by: parent, in: context)
XCTAssertEqual(childern.count, 2)
}
func testDeleteRecursively() throws {
let context = controller.viewContext()
let parent = PasswordEntity.insert(name: "parent", path: "path1", isDir: true, into: context)
let child1 = PasswordEntity.insert(name: "child1", path: "path2", isDir: true, into: context)
let child2 = PasswordEntity.insert(name: "child2", path: "path3", isDir: false, into: context)
let child3 = PasswordEntity.insert(name: "child3", path: "path4", isDir: false, into: context)
parent.children = [child1, child2]
child1.children = [child3]
PasswordEntity.deleteRecursively(entity: child2, in: context)
PasswordEntity.deleteRecursively(entity: child3, in: context)
XCTAssertEqual(PasswordEntity.fetchAll(in: context).count, 0)
}
}

View File

@@ -16,7 +16,8 @@ final class PasswordStoreTest: XCTestCase {
private let remoteRepoURL = URL(string: "https://github.com/mssun/passforios-password-store.git")!
func testCloneAndDecryptMultiKeys() throws {
let url = URL(fileURLWithPath: "\(Globals.repositoryPath)-test")
let url = Globals.sharedContainerURL.appendingPathComponent("Library/password-store-test/")
Defaults.isEnableGPGIDOn = true
let passwordStore = PasswordStore(url: url)
try passwordStore.cloneRepository(remoteRepoURL: remoteRepoURL, branchName: "master")
@@ -42,7 +43,7 @@ final class PasswordStoreTest: XCTestCase {
let work = try decrypt(passwordStore: passwordStore, path: "work/github.com.gpg", passphrase: "passforios")
XCTAssertEqual(work.plainText, "passwordforwork\n")
let testPassword = Password(name: "test", url: URL(string: "test.gpg")!, plainText: "testpassword")
let testPassword = Password(name: "test", path: "test.gpg", plainText: "testpassword")
let testPasswordEntity = try passwordStore.add(password: testPassword)!
let testPasswordPlain = try passwordStore.decrypt(passwordEntity: testPasswordEntity, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
XCTAssertEqual(testPasswordPlain.plainText, "testpassword")

View File

@@ -14,7 +14,7 @@ final class PasswordTest: XCTestCase {
func testURL() {
let password = getPasswordObjectWith(content: "")
XCTAssertEqual(password.url, PASSWORD_URL)
XCTAssertEqual(password.path, PASSWORD_PATH)
XCTAssertEqual(password.namePath, PASSWORD_PATH)
}
@@ -242,11 +242,17 @@ final class PasswordTest: XCTestCase {
}
func testUsernameInPath() {
let password = getPasswordObjectWith(content: "", url: URL(fileURLWithPath: "exampleservice/exampleusername.pgp"))
let password = getPasswordObjectWith(content: "", path: "exampleservice/exampleusername.pgp")
XCTAssertEqual(password.nameFromPath, "exampleusername")
}
func testDotInFilename() {
let password = getPasswordObjectWith(content: "", path: "exampleservice/..pgp")
XCTAssertEqual(password.nameFromPath, ".")
}
func testMultilineValues() {
let lineBreakField = "with line breaks" => "|\n This is \n text spread over \n multiple lines! "
let noLineBreakField = "without line breaks" => " > \n This is \n text spread over\n multiple lines!"
@@ -283,16 +289,16 @@ final class PasswordTest: XCTestCase {
let password = getPasswordObjectWith(content: "")
XCTAssertEqual(password.changed, 0)
password.updatePassword(name: "password", url: PASSWORD_URL, plainText: "")
password.updatePassword(name: "password", path: PASSWORD_PATH, plainText: "")
XCTAssertEqual(password.changed, 0)
password.updatePassword(name: "", url: PASSWORD_URL, plainText: "a")
password.updatePassword(name: "", path: PASSWORD_PATH, plainText: "a")
XCTAssertEqual(password.changed, 2)
password.updatePassword(name: "", url: URL(fileURLWithPath: "/some/path/"), plainText: "a")
password.updatePassword(name: "", path: "/some/path/", plainText: "a")
XCTAssertEqual(password.changed, 3)
password.updatePassword(name: "", url: PASSWORD_URL, plainText: "")
password.updatePassword(name: "", path: PASSWORD_PATH, plainText: "")
XCTAssertEqual(password.changed, 3)
}

View File

@@ -11,7 +11,6 @@ import XCTest
@testable import passKit
let PASSWORD_PATH = "/path/to/password"
let PASSWORD_URL = URL(fileURLWithPath: "/path/to/password")
let PASSWORD_STRING = "abcd1234"
let TOTP_URL = "otpauth://totp/email@email.com?secret=abcd1234"
let STEAM_TOTP_URL = "otpauth://totp/username?secret=12345678901234567890&issuer=Steam&algorithm=SHA1&digits=5&period=30&representation=steamguard"
@@ -30,8 +29,8 @@ let TOTP_URL_FIELD = "otpauth" => "//totp/email@email.com?secret=abcd1234"
let MULTILINE_BLOCK_START = "multiline block" => "|"
let MULTILINE_LINE_START = "multiline line" => ">"
func getPasswordObjectWith(content: String, url: URL? = nil) -> Password {
Password(name: "password", url: url ?? PASSWORD_URL, plainText: content)
func getPasswordObjectWith(content: String, path: String? = nil) -> Password {
Password(name: "password", path: path ?? PASSWORD_PATH, plainText: content)
}
func assertDefaults(