2017-01-19 21:15:47 +08:00
//
// P a s s w o r d S t o r e . s w i f t
// p a s s
//
// C r e a t e d b y M i n g s h e n S u n o n 1 9 / 1 / 2 0 1 7 .
// C o p y r i g h t © 2 0 1 7 B o b S u n . A l l r i g h t s r e s e r v e d .
//
import Foundation
import CoreData
import UIKit
2017-01-22 01:42:36 +08:00
import SwiftyUserDefaults
2017-01-23 16:29:36 +08:00
import ObjectiveGit
2017-06-13 11:42:49 +08:00
import ObjectivePGP
2017-01-19 21:15:47 +08:00
2017-06-13 11:42:49 +08:00
public class PasswordStore {
public static let shared = PasswordStore ( )
public let storeURL = URL ( fileURLWithPath : " \( Globals . repositoryPath ) " )
public let tempStoreURL = URL ( fileURLWithPath : " \( Globals . repositoryPath ) -temp " )
public var storeRepository : GTRepository ?
public var pgpKeyID : String ?
public var publicKey : PGPKey ? {
2017-03-16 22:06:39 -07:00
didSet {
if publicKey != nil {
pgpKeyID = publicKey ! . keyID ! . shortKeyString
} else {
pgpKeyID = nil
}
}
}
2017-06-13 11:42:49 +08:00
public var privateKey : PGPKey ?
2017-03-16 22:06:39 -07:00
2017-06-13 11:42:49 +08:00
public var gitSignatureForNow : GTSignature {
2017-03-16 00:38:38 +08:00
get {
2017-06-13 11:42:49 +08:00
let gitSignatureName = SharedDefaults [ . gitSignatureName ] ? ? Globals . gitSignatureDefaultName
let gitSignatureEmail = SharedDefaults [ . gitSignatureEmail ] ? ? Globals . gitSignatureDefaultEmail
2017-04-27 22:48:11 -07:00
return GTSignature ( name : gitSignatureName , email : gitSignatureEmail , time : Date ( ) ) !
2017-03-16 00:38:38 +08:00
}
}
2017-01-23 16:29:36 +08:00
2017-06-13 11:42:49 +08:00
public var pgp : ObjectivePGP = ObjectivePGP ( )
2017-01-19 21:15:47 +08:00
2017-06-13 11:42:49 +08:00
public var pgpKeyPassphrase : String ? {
2017-02-20 11:48:39 +08:00
set {
2017-03-06 12:49:00 -08:00
Utils . addPasswordToKeychain ( name : " pgpKeyPassphrase " , password : newValue )
2017-02-20 11:48:39 +08:00
}
get {
return Utils . getPasswordFromKeychain ( name : " pgpKeyPassphrase " )
2017-02-19 22:10:36 +08:00
}
}
2017-04-28 20:33:41 -07:00
2017-06-13 11:42:49 +08:00
public var gitPassword : String ? {
2017-02-20 11:48:39 +08:00
set {
2017-04-02 11:21:24 -07:00
Utils . addPasswordToKeychain ( name : " gitPassword " , password : newValue )
2017-02-20 11:48:39 +08:00
}
get {
2017-04-02 11:21:24 -07:00
return Utils . getPasswordFromKeychain ( name : " gitPassword " )
}
}
2017-06-13 11:42:49 +08:00
public var gitSSHPrivateKeyPassphrase : String ? {
2017-04-02 11:21:24 -07:00
set {
Utils . addPasswordToKeychain ( name : " gitSSHPrivateKeyPassphrase " , password : newValue )
}
get {
2017-04-28 20:33:41 -07:00
return Utils . getPasswordFromKeychain ( name : " gitSSHPrivateKeyPassphrase " )
2017-02-19 22:10:36 +08:00
}
}
2017-06-13 11:42:49 +08:00
private let fm = FileManager . default
lazy private 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 ! )
container . persistentStoreDescriptions = [ NSPersistentStoreDescription ( url : Globals . sharedContainerURL . appendingPathComponent ( " Documents/pass.sqlite " ) ) ]
container . loadPersistentStores ( completionHandler : { ( storeDescription , error ) in
2017-06-11 02:09:45 +08:00
if let error = error as NSError ? {
// R e p l a c e t h i s i m p l e m e n t a t i o n w i t h c o d e t o h a n d l e t h e e r r o r a p p r o p r i a t e l y .
// f a t a l E r r o r ( ) c a u s e s t h e a p p l i c a t i o n t o g e n e r a t e a c r a s h l o g a n d t e r m i n a t e . Y o u s h o u l d n o t u s e t h i s f u n c t i o n i n a s h i p p i n g a p p l i c a t i o n , a l t h o u g h i t m a y b e u s e f u l d u r i n g d e v e l o p m e n t .
/*
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 ( " Unresolved error \( error ) , \( error . userInfo ) " )
}
} )
return container . viewContext
} ( )
2017-03-24 22:05:09 +08:00
2017-06-13 11:42:49 +08:00
public var numberOfPasswords : Int {
2017-03-24 22:05:09 +08:00
return self . fetchPasswordEntityCoreData ( withDir : false ) . count
}
2017-06-13 11:42:49 +08:00
public var sizeOfRepositoryByteCount : UInt64 {
2017-03-24 22:05:09 +08:00
var size = UInt64 ( 0 )
do {
if fm . fileExists ( atPath : self . storeURL . path ) {
size = try fm . allocatedSizeOfDirectoryAtURL ( directoryURL : self . storeURL )
}
} catch {
print ( error )
}
return size
}
2017-01-19 21:15:47 +08:00
private init ( ) {
2017-06-11 02:09:45 +08:00
// F i l e m i g r a t i o n t o g r o u p
2017-06-13 11:42:49 +08:00
print ( Globals . documentPath )
print ( Globals . libraryPath )
print ( Globals . documentPathLegacy )
print ( Globals . libraryPathLegacy )
2017-06-11 02:09:45 +08:00
migration ( )
2017-01-23 16:29:36 +08:00
do {
2017-06-11 02:09:45 +08:00
if fm . fileExists ( atPath : storeURL . path ) {
2017-02-06 11:17:56 +08:00
try storeRepository = GTRepository . init ( url : storeURL )
}
2017-06-07 21:11:01 +08:00
try initPGPKeys ( )
2017-01-23 16:29:36 +08:00
} catch {
print ( error )
2017-01-19 21:15:47 +08:00
}
2017-04-02 11:21:24 -07:00
}
2017-06-11 02:09:45 +08:00
private func migration ( ) {
let needMigration = fm . fileExists ( atPath : Globals . documentPathLegacy ) && ! fm . fileExists ( atPath : Globals . documentPath ) && fm . fileExists ( atPath : Globals . libraryPathLegacy ) && ! fm . fileExists ( atPath : Globals . libraryPath )
guard needMigration = = true else {
return
}
do {
2017-06-13 11:42:49 +08:00
try fm . moveItem ( atPath : Globals . documentPathLegacy , toPath : Globals . documentPath )
try fm . moveItem ( atPath : Globals . libraryPathLegacy , toPath : Globals . libraryPath )
SharedDefaults = Defaults
2017-06-11 02:09:45 +08:00
} catch {
print ( " Cannot migrate: \( error ) " )
}
updatePasswordEntityCoreData ( )
}
2017-04-02 11:21:24 -07:00
enum SSHKeyType {
case ` public ` , secret
}
2017-06-13 11:42:49 +08:00
public func initGitSSHKey ( with armorKey : String ) throws {
2017-06-07 02:12:54 +08:00
let keyPath = Globals . gitSSHPrivateKeyPath
2017-04-02 11:21:24 -07:00
try armorKey . write ( toFile : keyPath , atomically : true , encoding : . ascii )
2017-01-19 21:15:47 +08:00
}
2017-02-19 22:10:36 +08:00
2017-06-07 21:11:01 +08:00
public func initPGPKeys ( ) throws {
try initPGPKey ( . public )
try initPGPKey ( . secret )
2017-03-16 22:06:39 -07:00
}
public func initPGPKey ( _ keyType : PGPKeyType ) throws {
switch keyType {
case . public :
2017-04-09 00:25:04 +08:00
let keyPath = Globals . pgpPublicKeyPath
self . publicKey = importKey ( from : keyPath )
if self . publicKey = = nil {
2017-04-30 16:16:52 -05:00
throw AppError . KeyImportError
2017-04-09 00:25:04 +08:00
}
2017-03-16 22:06:39 -07:00
case . secret :
2017-04-09 00:25:04 +08:00
let keyPath = Globals . pgpPrivateKeyPath
self . privateKey = importKey ( from : keyPath )
if self . privateKey = = nil {
2017-04-30 16:16:52 -05:00
throw AppError . KeyImportError
2017-03-16 22:06:39 -07:00
}
2017-04-09 00:25:04 +08:00
default :
2017-04-30 16:16:52 -05:00
throw AppError . UnknownError
2017-02-23 16:53:50 +08:00
}
2017-03-16 22:06:39 -07:00
}
2017-06-07 21:11:01 +08:00
public func initPGPKey ( from url : URL , keyType : PGPKeyType ) throws {
2017-03-16 22:06:39 -07:00
var pgpKeyLocalPath = " "
if keyType = = . public {
pgpKeyLocalPath = Globals . pgpPublicKeyPath
} else {
pgpKeyLocalPath = Globals . pgpPrivateKeyPath
2017-02-13 15:01:04 +08:00
}
2017-03-16 22:06:39 -07:00
let pgpKeyData = try Data ( contentsOf : url )
try pgpKeyData . write ( to : URL ( fileURLWithPath : pgpKeyLocalPath ) , options : . atomic )
try initPGPKey ( keyType )
}
public func initPGPKey ( with armorKey : String , keyType : PGPKeyType ) throws {
var pgpKeyLocalPath = " "
if keyType = = . public {
pgpKeyLocalPath = Globals . pgpPublicKeyPath
} else {
pgpKeyLocalPath = Globals . pgpPrivateKeyPath
2017-01-22 01:42:36 +08:00
}
2017-03-16 22:06:39 -07:00
try armorKey . write ( toFile : pgpKeyLocalPath , atomically : true , encoding : . ascii )
try initPGPKey ( keyType )
}
private func importKey ( from keyPath : String ) -> PGPKey ? {
if fm . fileExists ( atPath : keyPath ) {
if let keys = pgp . importKeys ( fromFile : keyPath , allowDuplicates : false ) as ? [ PGPKey ] {
2017-04-09 00:25:04 +08:00
return keys . first
2017-03-16 22:06:39 -07:00
}
}
return nil
2017-01-22 01:42:36 +08:00
}
2017-02-24 17:59:04 +03:00
2017-06-13 11:42:49 +08:00
public func getPgpPrivateKey ( ) -> PGPKey {
2017-02-24 17:59:04 +03:00
return pgp . getKeysOf ( . secret ) [ 0 ]
}
2017-01-22 01:42:36 +08:00
2017-06-13 11:42:49 +08:00
public func repositoryExisted ( ) -> Bool {
2017-02-27 17:30:33 +08:00
let fm = FileManager ( )
return fm . fileExists ( atPath : Globals . repositoryPath )
}
2017-06-13 11:42:49 +08:00
public func passwordExisted ( password : Password ) -> Bool {
2017-03-02 17:26:12 +08:00
let passwordEntityFetchRequest = NSFetchRequest < NSFetchRequestResult > ( entityName : " PasswordEntity " )
do {
2017-04-23 10:03:09 -07:00
passwordEntityFetchRequest . predicate = NSPredicate ( format : " name = %@ and path = %@ " , password . name , password . url ! . path )
2017-03-02 17:26:12 +08:00
let count = try context . count ( for : passwordEntityFetchRequest )
if count > 0 {
return true
} else {
return false
}
} catch {
fatalError ( " Failed to fetch password entities: \( error ) " )
}
return true
}
2017-06-13 11:42:49 +08:00
public func passwordEntityExisted ( path : String ) -> Bool {
2017-04-23 10:03:09 -07:00
let passwordEntityFetchRequest = NSFetchRequest < NSFetchRequestResult > ( entityName : " PasswordEntity " )
do {
passwordEntityFetchRequest . predicate = NSPredicate ( format : " path = %@ " , path )
let count = try context . count ( for : passwordEntityFetchRequest )
if count > 0 {
return true
} else {
return false
}
} catch {
fatalError ( " Failed to fetch password entities: \( error ) " )
}
return true
}
2017-06-13 11:42:49 +08:00
public func getPasswordEntity ( by path : String , isDir : Bool ) -> PasswordEntity ? {
2017-04-23 10:03:09 -07:00
let passwordEntityFetchRequest = NSFetchRequest < NSFetchRequestResult > ( entityName : " PasswordEntity " )
do {
2017-04-26 23:02:05 -07:00
passwordEntityFetchRequest . predicate = NSPredicate ( format : " path = %@ and isDir = %@ " , path , isDir . description )
2017-04-23 10:03:09 -07:00
return try context . fetch ( passwordEntityFetchRequest ) . first as ? PasswordEntity
} catch {
fatalError ( " Failed to fetch password entities: \( error ) " )
}
}
2017-06-13 11:42:49 +08:00
public func cloneRepository ( remoteRepoURL : URL ,
2017-01-24 01:49:55 +08:00
credential : GitCredential ,
2017-06-13 13:19:18 +08:00
requestGitPassword : @ escaping ( GitCredential . Credential , String ? ) -> String ? ,
2017-01-23 17:36:10 +08:00
transferProgressBlock : @ escaping ( UnsafePointer < git_transfer_progress > , UnsafeMutablePointer < ObjCBool > ) -> Void ,
2017-02-04 14:24:59 +08:00
checkoutProgressBlock : @ escaping ( String ? , UInt , UInt ) -> Void ) throws {
2017-02-15 22:51:26 +08:00
Utils . removeFileIfExists ( at : storeURL )
Utils . removeFileIfExists ( at : tempStoreURL )
2017-02-04 14:59:55 +08:00
do {
2017-06-13 13:19:18 +08:00
let credentialProvider = try credential . credentialProvider ( requestGitPassword : requestGitPassword )
2017-04-30 16:16:52 -05:00
let options = [ GTRepositoryCloneOptionsCredentialProvider : credentialProvider ]
2017-04-28 20:33:41 -07:00
storeRepository = try GTRepository . clone ( from : remoteRepoURL , toWorkingDirectory : tempStoreURL , options : options , transferProgressBlock : transferProgressBlock )
2017-02-04 14:59:55 +08:00
if fm . fileExists ( atPath : storeURL . path ) {
try fm . removeItem ( at : storeURL )
}
try fm . copyItem ( at : tempStoreURL , to : storeURL )
try fm . removeItem ( at : tempStoreURL )
2017-04-28 20:33:41 -07:00
storeRepository = try GTRepository ( url : storeURL )
2017-02-04 14:59:55 +08:00
} catch {
2017-04-28 20:33:41 -07:00
credential . delete ( )
throw ( error )
2017-02-04 14:59:55 +08:00
}
2017-03-29 22:59:30 -07:00
DispatchQueue . main . async {
2017-06-13 11:42:49 +08:00
SharedDefaults [ . lastSyncedTime ] = Date ( )
2017-03-29 22:59:30 -07:00
self . updatePasswordEntityCoreData ( )
2017-03-30 23:19:04 +08:00
NotificationCenter . default . post ( name : . passwordStoreUpdated , object : nil )
2017-03-29 22:59:30 -07:00
}
2017-01-23 16:29:36 +08:00
}
2017-01-24 16:57:16 +08:00
2017-06-13 13:19:18 +08:00
public func pullRepository ( credential : GitCredential , requestGitPassword : @ escaping ( GitCredential . Credential , String ? ) -> String ? , transferProgressBlock : @ escaping ( UnsafePointer < git_transfer_progress > , UnsafeMutablePointer < ObjCBool > ) -> Void ) throws {
2017-04-30 18:29:47 -05:00
guard let storeRepository = storeRepository else {
2017-04-30 16:16:52 -05:00
throw AppError . RepositoryNotSetError
2017-02-09 19:44:12 +08:00
}
2017-04-28 20:33:41 -07:00
do {
2017-06-13 13:19:18 +08:00
let credentialProvider = try credential . credentialProvider ( requestGitPassword : requestGitPassword )
2017-04-30 16:16:52 -05:00
let options = [ GTRepositoryRemoteOptionsCredentialProvider : credentialProvider ]
2017-04-30 18:29:47 -05:00
let remote = try GTRemote ( name : " origin " , in : storeRepository )
try storeRepository . pull ( storeRepository . currentBranch ( ) , from : remote , withOptions : options , progress : transferProgressBlock )
2017-04-28 20:33:41 -07:00
} catch {
credential . delete ( )
throw ( error )
}
2017-03-29 22:59:30 -07:00
DispatchQueue . main . async {
2017-06-13 11:42:49 +08:00
SharedDefaults [ . lastSyncedTime ] = Date ( )
2017-03-29 22:59:30 -07:00
self . setAllSynced ( )
self . updatePasswordEntityCoreData ( )
NotificationCenter . default . post ( name : . passwordStoreUpdated , object : nil )
}
2017-01-19 21:15:47 +08:00
}
2017-03-24 21:53:07 +08:00
private func updatePasswordEntityCoreData ( ) {
2017-02-07 16:45:14 +08:00
deleteCoreData ( entityName : " PasswordEntity " )
2017-03-02 14:51:40 +08:00
do {
var q = try fm . contentsOfDirectory ( atPath : self . 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 = filename . substring ( to : filename . index ( filename . endIndex , offsetBy : - 4 ) )
} else {
passwordEntity . name = filename
}
passwordEntity . path = filename
passwordEntity . parent = nil
return passwordEntity
}
while q . count > 0 {
let e = q . first !
q . remove ( at : 0 )
guard ! e . name ! . hasPrefix ( " . " ) else {
continue
}
var isDirectory : ObjCBool = false
let filePath = storeURL . appendingPathComponent ( e . path ! ) . path
if fm . fileExists ( atPath : filePath , isDirectory : & isDirectory ) {
if isDirectory . boolValue {
e . isDir = true
let files = try fm . contentsOfDirectory ( atPath : filePath ) . map { ( filename ) -> PasswordEntity in
let passwordEntity = NSEntityDescription . insertNewObject ( forEntityName : " PasswordEntity " , into : context ) as ! PasswordEntity
if filename . hasSuffix ( " .gpg " ) {
passwordEntity . name = filename . substring ( to : filename . index ( filename . endIndex , offsetBy : - 4 ) )
} else {
passwordEntity . name = filename
}
passwordEntity . path = " \( e . path ! ) / \( filename ) "
passwordEntity . parent = e
return passwordEntity
}
q += files
} else {
e . isDir = false
2017-02-06 21:53:54 +08:00
}
2017-01-19 21:15:47 +08:00
}
}
2017-03-02 14:51:40 +08:00
} catch {
print ( error )
}
2017-01-19 21:15:47 +08:00
do {
try context . save ( )
} catch {
print ( " Error with save: \( error ) " )
}
}
2017-06-13 11:42:49 +08:00
public func getRecentCommits ( count : Int ) throws -> [ GTCommit ] {
2017-04-30 18:29:47 -05:00
guard let storeRepository = storeRepository else {
2017-03-19 10:36:59 -07:00
return [ ]
}
2017-02-22 18:43:19 +08:00
var commits = [ GTCommit ] ( )
2017-04-30 18:29:47 -05:00
let enumerator = try GTEnumerator ( repository : storeRepository )
if let sha = try storeRepository . headReference ( ) . targetOID . sha {
2017-04-30 16:16:52 -05:00
try enumerator . pushSHA ( sha )
}
for _ in 0 . . < count {
let commit = try enumerator . nextObject ( withSuccess : nil )
commits . append ( commit )
2017-02-22 18:43:19 +08:00
}
return commits
}
2017-06-13 11:42:49 +08:00
public func fetchPasswordEntityCoreData ( parent : PasswordEntity ? ) -> [ PasswordEntity ] {
2017-01-19 21:15:47 +08:00
let passwordEntityFetch = NSFetchRequest < NSFetchRequestResult > ( entityName : " PasswordEntity " )
do {
2017-03-02 14:51:40 +08:00
passwordEntityFetch . predicate = NSPredicate ( format : " parent = %@ " , parent ? ? 0 )
2017-01-19 21:15:47 +08:00
let fetchedPasswordEntities = try context . fetch ( passwordEntityFetch ) as ! [ PasswordEntity ]
2017-02-09 13:38:42 +08:00
return fetchedPasswordEntities . sorted { $0 . name ! . caseInsensitiveCompare ( $1 . name ! ) = = . orderedAscending }
2017-01-19 21:15:47 +08:00
} catch {
2017-02-12 01:59:40 +08:00
fatalError ( " Failed to fetch passwords: \( error ) " )
2017-01-19 21:15:47 +08:00
}
}
2017-06-13 11:42:49 +08:00
public func fetchPasswordEntityCoreData ( withDir : Bool ) -> [ PasswordEntity ] {
2017-03-02 14:51:40 +08:00
let passwordEntityFetch = NSFetchRequest < NSFetchRequestResult > ( entityName : " PasswordEntity " )
2017-02-06 21:53:54 +08:00
do {
2017-03-02 14:51:40 +08:00
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 }
2017-02-12 01:59:40 +08:00
} catch {
2017-03-02 14:51:40 +08:00
fatalError ( " Failed to fetch passwords: \( error ) " )
2017-02-12 01:59:40 +08:00
}
}
2017-03-02 14:51:40 +08:00
2017-06-13 11:42:49 +08:00
public func fetchUnsyncedPasswords ( ) -> [ PasswordEntity ] {
2017-02-12 01:59:40 +08:00
let passwordEntityFetchRequest = NSFetchRequest < NSFetchRequestResult > ( entityName : " PasswordEntity " )
passwordEntityFetchRequest . predicate = NSPredicate ( format : " synced = %i " , 0 )
do {
let passwordEntities = try context . fetch ( passwordEntityFetchRequest ) as ! [ PasswordEntity ]
return passwordEntities
} catch {
fatalError ( " Failed to fetch passwords: \( error ) " )
}
}
2017-06-13 11:42:49 +08:00
public func setAllSynced ( ) {
2017-02-12 01:59:40 +08:00
let passwordEntities = fetchUnsyncedPasswords ( )
for passwordEntity in passwordEntities {
passwordEntity . synced = true
}
do {
2017-02-15 21:48:06 +08:00
if context . hasChanges {
try context . save ( )
}
2017-02-12 01:59:40 +08:00
} catch {
fatalError ( " Failed to save: \( error ) " )
}
}
2017-06-13 11:42:49 +08:00
public func getNumberOfUnsyncedPasswords ( ) -> Int {
2017-02-12 01:59:40 +08:00
let passwordEntityFetchRequest = NSFetchRequest < NSFetchRequestResult > ( entityName : " PasswordEntity " )
do {
passwordEntityFetchRequest . predicate = NSPredicate ( format : " synced = %i " , 0 )
return try context . count ( for : passwordEntityFetchRequest )
2017-02-06 21:53:54 +08:00
} catch {
2017-03-02 17:26:12 +08:00
fatalError ( " Failed to fetch unsynced passwords: \( error ) " )
2017-02-06 21:53:54 +08:00
}
}
2017-03-02 17:26:12 +08:00
2017-06-13 11:42:49 +08:00
public func getLatestUpdateInfo ( filename : String ) -> String {
2017-04-30 18:29:47 -05:00
guard let storeRepository = storeRepository else {
2017-04-30 16:16:52 -05:00
return " Unknown "
}
2017-04-30 18:29:47 -05:00
guard let blameHunks = try ? storeRepository . blame ( withFile : filename , options : nil ) . hunks ,
2017-04-30 16:16:52 -05:00
let latestCommitTime = blameHunks . map ( {
2017-02-27 15:21:25 +08:00
$0 . finalSignature ? . time ? . timeIntervalSince1970 ? ? 0
} ) . max ( ) else {
2017-04-30 16:16:52 -05:00
return " Unknown "
2017-02-27 15:21:25 +08:00
}
2017-03-02 00:06:27 +08:00
let lastCommitDate = Date ( timeIntervalSince1970 : latestCommitTime )
let currentDate = Date ( )
var autoFormattedDifference : String
if currentDate . timeIntervalSince ( lastCommitDate ) <= 60 {
2017-03-03 20:16:06 +08:00
autoFormattedDifference = " Just now "
2017-03-02 00:06:27 +08:00
} else {
let diffDate = Calendar . current . dateComponents ( [ . year , . month , . day , . hour , . minute ] , from : lastCommitDate , to : currentDate )
let dateComponentsFormatter = DateComponentsFormatter ( )
dateComponentsFormatter . unitsStyle = . full
dateComponentsFormatter . maximumUnitCount = 2
dateComponentsFormatter . includesApproximationPhrase = true
2017-04-30 16:16:52 -05:00
autoFormattedDifference = dateComponentsFormatter . string ( from : diffDate ) ! . appending ( " ago " )
2017-03-02 00:06:27 +08:00
}
return autoFormattedDifference
2017-02-27 15:21:25 +08:00
}
2017-06-13 11:42:49 +08:00
public func updateRemoteRepo ( ) {
2017-01-19 21:15:47 +08:00
}
2017-02-07 16:45:14 +08:00
2017-04-25 13:01:17 -07:00
private func gitAdd ( path : String ) throws {
2017-04-30 18:29:47 -05:00
guard let storeRepository = storeRepository else {
2017-04-30 16:16:52 -05:00
throw AppError . RepositoryNotSetError
2017-04-25 13:01:17 -07:00
}
2017-04-30 18:29:47 -05:00
try storeRepository . index ( ) . addFile ( path )
try storeRepository . index ( ) . write ( )
2017-04-25 13:01:17 -07:00
}
private func gitRm ( path : String ) throws {
2017-04-30 18:29:47 -05:00
guard let storeRepository = storeRepository else {
2017-04-30 16:16:52 -05:00
throw AppError . RepositoryNotSetError
2017-02-13 01:15:42 +08:00
}
2017-04-30 18:29:47 -05:00
let url = storeURL . appendingPathComponent ( path )
2017-06-11 02:09:45 +08:00
if fm . fileExists ( atPath : url . path ) {
try fm . removeItem ( at : url )
2017-04-30 16:16:52 -05:00
}
2017-04-30 18:29:47 -05:00
try storeRepository . index ( ) . removeFile ( path )
try storeRepository . index ( ) . write ( )
2017-04-26 20:28:15 -07:00
}
private func deleteDirectoryTree ( at url : URL ) throws {
var tempURL = storeURL . appendingPathComponent ( url . deletingLastPathComponent ( ) . path )
var count = try fm . contentsOfDirectory ( atPath : tempURL . path ) . count
while count = = 0 {
try fm . removeItem ( at : tempURL )
tempURL . deleteLastPathComponent ( )
count = try fm . contentsOfDirectory ( atPath : tempURL . path ) . count
}
}
private func createDirectoryTree ( at url : URL ) throws {
let tempURL = storeURL . appendingPathComponent ( url . deletingLastPathComponent ( ) . path )
2017-06-11 02:09:45 +08:00
try fm . createDirectory ( at : tempURL , withIntermediateDirectories : true , attributes : nil )
2017-02-13 01:15:42 +08:00
}
2017-04-25 13:01:17 -07:00
private func gitMv ( from : String , to : String ) throws {
2017-04-30 18:29:47 -05:00
let fromURL = storeURL . appendingPathComponent ( from )
let toURL = storeURL . appendingPathComponent ( to )
guard fm . fileExists ( atPath : fromURL . path ) else {
print ( " \( from ) not exist " )
return
2017-04-25 22:33:37 -07:00
}
2017-04-30 18:29:47 -05:00
try fm . moveItem ( at : fromURL , to : toURL )
2017-04-25 13:01:17 -07:00
try gitAdd ( path : to )
try gitRm ( path : from )
}
private func gitCommit ( message : String ) throws -> GTCommit ? {
2017-04-30 18:29:47 -05:00
guard let storeRepository = storeRepository else {
2017-04-30 16:16:52 -05:00
throw AppError . RepositoryNotSetError
2017-02-11 01:50:39 +08:00
}
2017-04-30 18:29:47 -05:00
let newTree = try storeRepository . index ( ) . writeTree ( )
let headReference = try storeRepository . headReference ( )
let commitEnum = try GTEnumerator ( repository : storeRepository )
2017-04-30 16:16:52 -05:00
try commitEnum . pushSHA ( headReference . targetOID . sha ! )
let parent = commitEnum . nextObject ( ) as ! GTCommit
let signature = gitSignatureForNow
2017-04-30 18:29:47 -05:00
let commit = try storeRepository . createCommit ( with : newTree , message : message , author : signature , committer : signature , parents : [ parent ] , updatingReferenceNamed : headReference . name )
2017-04-30 16:16:52 -05:00
return commit
2017-02-11 01:50:39 +08:00
}
2017-04-30 16:16:52 -05:00
private func getLocalBranch ( withName branchName : String ) throws -> GTBranch ? {
2017-04-30 18:29:47 -05:00
guard let storeRepository = storeRepository else {
2017-04-30 16:16:52 -05:00
throw AppError . RepositoryNotSetError
2017-02-11 01:50:39 +08:00
}
2017-04-30 16:16:52 -05:00
let reference = GTBranch . localNamePrefix ( ) . appending ( branchName )
2017-04-30 18:29:47 -05:00
let branches = try storeRepository . branches ( withPrefix : reference )
2017-04-30 16:16:52 -05:00
return branches . first
2017-02-11 01:50:39 +08:00
}
2017-06-13 13:19:18 +08:00
public func pushRepository ( credential : GitCredential , requestGitPassword : @ escaping ( GitCredential . Credential , String ? ) -> String ? , transferProgressBlock : @ escaping ( UInt32 , UInt32 , Int , UnsafeMutablePointer < ObjCBool > ) -> Void ) throws {
2017-04-30 18:29:47 -05:00
guard let storeRepository = storeRepository else {
2017-04-30 16:16:52 -05:00
throw AppError . RepositoryNotSetError
}
2017-04-28 20:33:41 -07:00
do {
2017-06-13 13:19:18 +08:00
let credentialProvider = try credential . credentialProvider ( requestGitPassword : requestGitPassword )
2017-04-30 16:16:52 -05:00
let options = [ GTRepositoryRemoteOptionsCredentialProvider : credentialProvider ]
if let masterBranch = try getLocalBranch ( withName : " master " ) {
2017-04-30 18:29:47 -05:00
let remote = try GTRemote ( name : " origin " , in : storeRepository )
try storeRepository . push ( masterBranch , to : remote , withOptions : options , progress : transferProgressBlock )
2017-04-30 16:16:52 -05:00
}
2017-04-28 20:33:41 -07:00
} catch {
credential . delete ( )
throw ( error )
}
2017-02-10 22:15:01 +08:00
}
2017-04-25 13:01:17 -07:00
private func addPasswordEntities ( password : Password ) throws -> PasswordEntity ? {
guard ! passwordExisted ( password : password ) else {
2017-04-30 16:16:52 -05:00
throw AppError . PasswordDuplicatedError
2017-04-25 13:01:17 -07:00
}
2017-04-23 10:03:09 -07:00
var passwordURL = password . url !
var paths : [ String ] = [ ]
while passwordURL . path != " . " {
paths . append ( passwordURL . path )
passwordURL = passwordURL . deletingLastPathComponent ( )
2017-03-02 17:26:12 +08:00
}
2017-04-23 10:03:09 -07:00
paths . reverse ( )
var parentPasswordEntity : PasswordEntity ? = nil
for path in paths {
2017-04-26 23:02:05 -07:00
let isDir = ! path . hasSuffix ( " .gpg " )
if let passwordEntity = getPasswordEntity ( by : path , isDir : isDir ) {
print ( passwordEntity . path ! )
2017-04-23 10:03:09 -07:00
parentPasswordEntity = passwordEntity
} else {
2017-04-26 23:02:05 -07:00
if ! isDir {
2017-04-26 20:28:15 -07:00
return insertPasswordEntity ( name : URL ( string : path . stringByAddingPercentEncodingForRFC3986 ( ) ! ) ! . deletingPathExtension ( ) . lastPathComponent , path : path , parent : parentPasswordEntity , synced : false , isDir : false )
2017-04-23 10:03:09 -07:00
} else {
2017-04-26 20:28:15 -07:00
parentPasswordEntity = insertPasswordEntity ( name : URL ( string : path . stringByAddingPercentEncodingForRFC3986 ( ) ! ) ! . lastPathComponent , path : path , parent : parentPasswordEntity , synced : false , isDir : true )
2017-04-23 10:03:09 -07:00
}
}
2017-02-10 22:15:01 +08:00
}
2017-04-23 10:03:09 -07:00
return nil
2017-02-10 22:15:01 +08:00
}
2017-04-23 10:03:09 -07:00
private func insertPasswordEntity ( name : String , path : String , parent : PasswordEntity ? , synced : Bool = false , isDir : Bool = false ) -> PasswordEntity ? {
var ret : PasswordEntity ? = nil
2017-04-25 13:01:17 -07:00
if let passwordEntity = NSEntityDescription . insertNewObject ( forEntityName : " PasswordEntity " , into : self . context ) as ? PasswordEntity {
passwordEntity . name = name
passwordEntity . path = path
passwordEntity . parent = parent
passwordEntity . synced = synced
passwordEntity . isDir = isDir
do {
try self . context . save ( )
ret = passwordEntity
} catch {
fatalError ( " Failed to insert a PasswordEntity: \( error ) " )
2017-04-23 10:03:09 -07:00
}
2017-02-15 20:01:17 +08:00
}
2017-04-23 10:03:09 -07:00
return ret
2017-02-15 20:01:17 +08:00
}
2017-06-13 11:42:49 +08:00
public func add ( password : Password ) throws -> PasswordEntity ? {
2017-04-26 20:28:15 -07:00
try createDirectoryTree ( at : password . url ! )
2017-04-25 13:01:17 -07:00
let newPasswordEntity = try addPasswordEntities ( password : password )
2017-04-23 10:03:09 -07:00
let saveURL = storeURL . appendingPathComponent ( password . url ! . path )
try self . encrypt ( password : password ) . write ( to : saveURL )
2017-04-25 13:01:17 -07:00
try gitAdd ( path : password . url ! . path )
let _ = try gitCommit ( message : " Add password for \( password . url ! . deletingPathExtension ( ) . path ) to store using Pass for iOS. " )
2017-03-21 13:16:25 -07:00
NotificationCenter . default . post ( name : . passwordStoreUpdated , object : nil )
2017-04-23 10:03:09 -07:00
return newPasswordEntity
}
2017-04-26 20:28:15 -07:00
public func delete ( passwordEntity : PasswordEntity ) throws {
let deletedFileURL = passwordEntity . getURL ( ) !
try deleteDirectoryTree ( at : passwordEntity . getURL ( ) ! )
try deletePasswordEntities ( passwordEntity : passwordEntity )
try gitRm ( path : deletedFileURL . path )
2017-04-26 23:01:42 -07:00
let _ = try gitCommit ( message : " Remove \( deletedFileURL . deletingPathExtension ( ) . path . removingPercentEncoding ! ) from store using Pass for iOS. " )
2017-04-26 20:28:15 -07:00
NotificationCenter . default . post ( name : . passwordStoreUpdated , object : nil )
}
2017-06-13 11:42:49 +08:00
public func edit ( passwordEntity : PasswordEntity , password : Password ) throws -> PasswordEntity ? {
2017-04-25 13:01:17 -07:00
var newPasswordEntity : PasswordEntity ? = passwordEntity
if password . changed & PasswordChange . content . rawValue != 0 {
2017-04-26 20:28:15 -07:00
print ( " chagne content " )
2017-04-25 22:33:37 -07:00
let saveURL = storeURL . appendingPathComponent ( passwordEntity . getURL ( ) ! . path )
2017-04-25 13:01:17 -07:00
try self . encrypt ( password : password ) . write ( to : saveURL )
2017-04-25 22:33:37 -07:00
try gitAdd ( path : passwordEntity . getURL ( ) ! . path )
2017-04-26 23:01:42 -07:00
let _ = try gitCommit ( message : " Edit password for \( passwordEntity . getURL ( ) ! . deletingPathExtension ( ) . path . removingPercentEncoding ! ) to store using Pass for iOS. " )
2017-04-26 20:28:15 -07:00
newPasswordEntity = passwordEntity
2017-04-25 13:01:17 -07:00
}
2017-04-26 20:28:15 -07:00
2017-04-25 13:01:17 -07:00
if password . changed & PasswordChange . path . rawValue != 0 {
2017-04-26 20:28:15 -07:00
print ( " change path " )
let deletedFileURL = passwordEntity . getURL ( ) !
// a d d
try createDirectoryTree ( at : password . url ! )
newPasswordEntity = try addPasswordEntities ( password : password )
// m v
try gitMv ( from : deletedFileURL . path , to : password . url ! . path )
// d e l e t e
try deleteDirectoryTree ( at : deletedFileURL )
try deletePasswordEntities ( passwordEntity : passwordEntity )
2017-04-26 23:01:42 -07:00
let _ = try gitCommit ( message : " Rename \( deletedFileURL . deletingPathExtension ( ) . path . removingPercentEncoding ! ) to \( password . url ! . deletingPathExtension ( ) . path . removingPercentEncoding ! ) using Pass for iOS. " )
2017-04-26 20:28:15 -07:00
2017-04-25 13:01:17 -07:00
}
2017-04-25 20:58:48 -07:00
NotificationCenter . default . post ( name : . passwordStoreUpdated , object : nil )
2017-04-25 13:01:17 -07:00
return newPasswordEntity
2017-04-23 10:03:09 -07:00
}
2017-04-25 13:01:17 -07:00
private func deletePasswordEntities ( passwordEntity : PasswordEntity ) throws {
var current : PasswordEntity ? = passwordEntity
while current != nil && ( current ! . children ! . count = = 0 || ! current ! . isDir ) {
let parent = current ! . parent
self . context . delete ( current ! )
current = parent
do {
try self . context . save ( )
} catch {
fatalError ( " Failed to delete a PasswordEntity: \( error ) " )
2017-04-23 10:03:09 -07:00
}
}
2017-03-21 13:16:25 -07:00
}
2017-06-13 11:42:49 +08:00
public func saveUpdated ( passwordEntity : PasswordEntity ) {
2017-02-15 20:01:17 +08:00
do {
2017-02-13 01:15:42 +08:00
try context . save ( )
} catch {
2017-02-15 20:01:17 +08:00
fatalError ( " Failed to save a PasswordEntity: \( error ) " )
2017-02-13 01:15:42 +08:00
}
}
2017-06-13 11:42:49 +08:00
public func deleteCoreData ( entityName : String ) {
2017-02-07 16:45:14 +08:00
let deleteFetchRequest = NSFetchRequest < NSFetchRequestResult > ( entityName : entityName )
let deleteRequest = NSBatchDeleteRequest ( fetchRequest : deleteFetchRequest )
do {
try context . execute ( deleteRequest )
2017-02-15 16:51:12 +08:00
try context . save ( )
2017-02-15 21:37:02 +08:00
context . reset ( )
2017-02-07 16:45:14 +08:00
} catch let error as NSError {
print ( error )
}
}
2017-06-13 11:42:49 +08:00
public func updateImage ( passwordEntity : PasswordEntity , image : Data ? ) {
2017-04-30 18:29:47 -05:00
guard let image = image else {
2017-02-16 13:24:41 +08:00
return
}
let privateMOC = NSManagedObjectContext ( concurrencyType : . privateQueueConcurrencyType )
privateMOC . parent = context
privateMOC . perform {
2017-04-30 18:29:47 -05:00
passwordEntity . image = NSData ( data : image )
2017-02-16 13:24:41 +08:00
do {
try privateMOC . save ( )
self . context . performAndWait {
do {
try self . context . save ( )
} catch {
fatalError ( " Failure to save context: \( error ) " )
}
}
} catch {
fatalError ( " Failure to save context: \( error ) " )
}
}
}
2017-06-13 11:42:49 +08:00
public func erase ( ) {
2017-03-16 22:06:39 -07:00
publicKey = nil
privateKey = nil
2017-02-08 19:29:44 +08:00
Utils . removeFileIfExists ( at : storeURL )
2017-02-15 11:15:11 +08:00
Utils . removeFileIfExists ( at : tempStoreURL )
2017-02-10 22:15:01 +08:00
Utils . removeFileIfExists ( atPath : Globals . pgpPublicKeyPath )
Utils . removeFileIfExists ( atPath : Globals . pgpPrivateKeyPath )
2017-04-02 11:21:24 -07:00
Utils . removeFileIfExists ( atPath : Globals . gitSSHPrivateKeyPath )
2017-02-07 16:45:14 +08:00
2017-02-19 22:10:36 +08:00
Utils . removeAllKeychain ( )
2017-02-20 11:48:39 +08:00
2017-02-07 16:45:14 +08:00
deleteCoreData ( entityName : " PasswordEntity " )
2017-06-13 13:04:39 +08:00
SharedDefaults . removeAll ( )
2017-02-13 14:30:38 +08:00
storeRepository = nil
2017-03-18 00:18:55 +08:00
NotificationCenter . default . post ( name : . passwordStoreUpdated , object : nil )
NotificationCenter . default . post ( name : . passwordStoreErased , object : nil )
2017-02-07 16:45:14 +08:00
}
2017-03-04 01:15:28 +08:00
2017-03-07 01:43:23 +08:00
// r e t u r n t h e n u m b e r o f d i s c a r d e d c o m m i t s
2017-06-13 11:42:49 +08:00
public func reset ( ) throws -> Int {
2017-04-30 18:29:47 -05:00
guard let storeRepository = storeRepository else {
throw AppError . RepositoryNotSetError
}
2017-03-04 01:15:28 +08:00
// g e t a l i s t o f l o c a l c o m m i t s
2017-03-22 19:07:41 -07:00
if let localCommits = try getLocalCommits ( ) ,
2017-03-04 01:15:28 +08:00
localCommits . count > 0 {
2017-03-07 01:43:23 +08:00
// g e t t h e o l d e s t l o c a l c o m m i t
guard let firstLocalCommit = localCommits . last ,
2017-03-04 01:15:28 +08:00
firstLocalCommit . parents . count = = 1 ,
let newHead = firstLocalCommit . parents . first else {
2017-04-30 16:16:52 -05:00
throw AppError . GitResetError
2017-03-04 01:15:28 +08:00
}
2017-04-30 18:29:47 -05:00
try storeRepository . reset ( to : newHead , resetType : . hard )
2017-03-24 21:53:07 +08:00
self . setAllSynced ( )
2017-03-04 01:15:28 +08:00
self . updatePasswordEntityCoreData ( )
2017-03-29 00:56:07 +08:00
2017-03-18 00:18:55 +08:00
NotificationCenter . default . post ( name : . passwordStoreUpdated , object : nil )
NotificationCenter . default . post ( name : . passwordStoreChangeDiscarded , object : nil )
2017-03-07 01:43:23 +08:00
return localCommits . count
2017-03-04 01:15:28 +08:00
} else {
2017-03-07 01:43:23 +08:00
return 0 // n o n e w c o m m i t
2017-03-04 01:15:28 +08:00
}
}
2017-03-22 19:07:41 -07:00
2017-06-13 11:42:49 +08:00
public func numberOfLocalCommits ( ) -> Int {
2017-03-22 19:07:41 -07:00
do {
if let localCommits = try getLocalCommits ( ) {
return localCommits . count
} else {
return 0
}
} catch {
print ( error )
}
return 0
}
private func getLocalCommits ( ) throws -> [ GTCommit ] ? {
2017-04-30 18:29:47 -05:00
guard let storeRepository = storeRepository else {
2017-04-30 16:16:52 -05:00
throw AppError . RepositoryNotSetError
}
2017-03-22 19:07:41 -07:00
// g e t t h e r e m o t e o r i g i n / m a s t e r b r a n c h
2017-04-30 18:29:47 -05:00
guard let index = try storeRepository . remoteBranches ( ) . index ( where : { $0 . shortName = = " master " } ) else {
2017-04-30 16:16:52 -05:00
throw AppError . RepositoryRemoteMasterNotFoundError
2017-03-22 19:07:41 -07:00
}
2017-04-30 18:29:47 -05:00
let remoteMasterBranch = try storeRepository . remoteBranches ( ) [ index ]
2017-03-22 19:07:41 -07:00
2017-05-10 09:29:23 -07:00
// c h e c k o i d b e f o r e c a l l i n g l o c a l C o m m i t s R e l a t i v e
guard remoteMasterBranch . oid != nil else {
throw AppError . RepositoryRemoteMasterNotFoundError
}
2017-03-22 19:07:41 -07:00
// g e t a l i s t o f l o c a l c o m m i t s
2017-04-30 18:29:47 -05:00
return try storeRepository . localCommitsRelative ( toRemoteBranch : remoteMasterBranch )
2017-03-22 19:07:41 -07:00
}
2017-04-23 10:03:09 -07:00
2017-06-13 11:42:49 +08:00
public func decrypt ( passwordEntity : PasswordEntity , requestPGPKeyPassphrase : ( ) -> String ) throws -> Password ? {
2017-04-30 18:29:47 -05:00
let encryptedDataPath = storeURL . appendingPathComponent ( passwordEntity . path ! )
2017-04-23 10:03:09 -07:00
let encryptedData = try Data ( contentsOf : encryptedDataPath )
var passphrase = self . pgpKeyPassphrase
if passphrase = = nil {
passphrase = requestPGPKeyPassphrase ( )
}
let decryptedData = try PasswordStore . shared . pgp . decryptData ( encryptedData , passphrase : passphrase )
let plainText = String ( data : decryptedData , encoding : . utf8 ) ? ? " "
2017-04-26 20:28:15 -07:00
let escapedPath = passwordEntity . path ! . stringByAddingPercentEncodingForRFC3986 ( ) ? ? " "
2017-04-30 18:29:47 -05:00
return Password ( name : passwordEntity . name ! , url : URL ( string : escapedPath ) , plainText : plainText )
2017-04-23 10:03:09 -07:00
}
2017-06-13 11:42:49 +08:00
public func encrypt ( password : Password ) throws -> Data {
2017-04-30 18:29:47 -05:00
guard let publicKey = pgp . getKeysOf ( . public ) . first else {
throw AppError . PGPPublicKeyNotExistError
}
2017-04-23 10:03:09 -07:00
let plainData = password . getPlainData ( )
2017-06-13 11:42:49 +08:00
let encryptedData = try pgp . encryptData ( plainData , usingPublicKey : publicKey , armored : SharedDefaults [ . encryptInArmored ] )
2017-04-23 10:03:09 -07:00
return encryptedData
}
2017-06-03 18:12:33 -07:00
2017-06-13 11:42:49 +08:00
public func removePGPKeys ( ) {
2017-06-03 18:12:33 -07:00
Utils . removeFileIfExists ( atPath : Globals . pgpPublicKeyPath )
Utils . removeFileIfExists ( atPath : Globals . pgpPrivateKeyPath )
2017-06-13 13:04:39 +08:00
SharedDefaults . remove ( . pgpKeySource )
SharedDefaults . remove ( . pgpPublicKeyArmor )
SharedDefaults . remove ( . pgpPrivateKeyArmor )
SharedDefaults . remove ( . pgpPrivateKeyURL )
SharedDefaults . remove ( . pgpPublicKeyURL )
2017-06-03 18:12:33 -07:00
Utils . removeKeychain ( name : " .pgpKeyPassphrase " )
pgp = ObjectivePGP ( )
publicKey = nil
privateKey = nil
}
2017-06-13 11:42:49 +08:00
public func removeGitSSHKeys ( ) {
2017-06-03 18:12:33 -07:00
Utils . removeFileIfExists ( atPath : Globals . gitSSHPrivateKeyPath )
Defaults . remove ( . gitSSHPrivateKeyArmor )
Defaults . remove ( . gitSSHPrivateKeyURL )
Utils . removeKeychain ( name : " .gitSSHPrivateKeyPassphrase " )
}
2017-06-07 16:54:54 +08:00
2017-06-13 11:42:49 +08:00
public func gitSSHKeyExists ( ) -> Bool {
2017-06-07 16:54:54 +08:00
return fm . fileExists ( atPath : Globals . gitSSHPrivateKeyPath )
}
2017-06-13 11:42:49 +08:00
public func pgpKeyExists ( ) -> Bool {
2017-06-07 16:54:54 +08:00
return fm . fileExists ( atPath : Globals . pgpPublicKeyPath ) && fm . fileExists ( atPath : Globals . pgpPrivateKeyPath )
}
2017-01-19 21:15:47 +08:00
}