mirror of
https://github.com/mssun/passforios.git
synced 2025-12-14 20:35:41 +01:00
Refactor core data classes (#671)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
90
passKit/Controllers/CoreDataStack.swift
Normal file
90
passKit/Controllers/CoreDataStack.swift
Normal 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
|
||||
}()
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
31
passKitTests/CoreData/CoreDataTestCase.swift
Normal file
31
passKitTests/CoreData/CoreDataTestCase.swift
Normal 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
|
||||
}
|
||||
}
|
||||
88
passKitTests/CoreData/PasswordEntityTest.swift
Normal file
88
passKitTests/CoreData/PasswordEntityTest.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user