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 Result
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-01-19 21:15:47 +08:00
2017-01-24 01:49:55 +08:00
struct GitCredential {
enum Credential {
case http ( userName : String , password : String )
case ssh ( userName : String , password : String , publicKeyFile : URL , privateKeyFile : URL )
}
var credential : Credential
func credentialProvider ( ) throws -> GTCredentialProvider {
return GTCredentialProvider { ( _ , _ , _ ) -> ( GTCredential ) in
let credential : GTCredential ?
switch self . credential {
case let . http ( userName , password ) :
credential = try ? GTCredential ( userName : userName , password : password )
case let . ssh ( userName , password , publicKeyFile , privateKeyFile ) :
credential = try ? GTCredential ( userName : userName , publicKeyURL : publicKeyFile , privateKeyURL : privateKeyFile , passphrase : password )
}
return credential ? ? GTCredential ( )
}
}
}
2017-01-19 21:15:47 +08:00
class PasswordStore {
static let shared = PasswordStore ( )
2017-02-08 10:15:38 +08:00
let storeURL = URL ( fileURLWithPath : " \( Globals . documentPath ) /password-store " )
let tempStoreURL = URL ( fileURLWithPath : " \( Globals . documentPath ) /password-store-temp " )
2017-01-23 16:29:36 +08:00
var storeRepository : GTRepository ?
2017-02-02 15:02:57 +08:00
var gitCredential : GitCredential ?
2017-01-23 16:29:36 +08:00
2017-01-22 01:42:36 +08:00
let pgp : ObjectivePGP = ObjectivePGP ( )
2017-01-19 21:15:47 +08:00
let context = ( UIApplication . shared . delegate as ! AppDelegate ) . persistentContainer . viewContext
private init ( ) {
2017-01-23 16:29:36 +08:00
do {
2017-02-06 11:17:56 +08:00
if FileManager . default . fileExists ( atPath : storeURL . path ) {
try storeRepository = GTRepository . init ( url : storeURL )
}
2017-01-23 16:29:36 +08:00
} catch {
print ( error )
2017-01-19 21:15:47 +08:00
}
2017-01-22 01:42:36 +08:00
if Defaults [ . pgpKeyID ] != " " {
2017-02-10 22:15:01 +08:00
pgp . importKeys ( fromFile : Globals . pgpPublicKeyPath , allowDuplicates : false )
pgp . importKeys ( fromFile : Globals . pgpPrivateKeyPath , allowDuplicates : false )
2017-01-22 01:42:36 +08:00
}
2017-01-31 22:00:50 +08:00
if Defaults [ . gitRepositoryAuthenticationMethod ] = = " Password " {
2017-02-13 14:30:38 +08:00
gitCredential = GitCredential ( credential : GitCredential . Credential . http ( userName : Defaults [ . gitRepositoryUsername ] ! , password : Defaults [ . gitRepositoryPassword ] ! ) )
2017-02-02 15:02:57 +08:00
} else if Defaults [ . gitRepositoryAuthenticationMethod ] = = " SSH Key " {
2017-02-13 14:30:38 +08:00
gitCredential = GitCredential ( credential : GitCredential . Credential . ssh ( userName : Defaults [ . gitRepositoryUsername ] ! , password : Defaults [ . gitRepositorySSHPrivateKeyPassphrase ] ! , publicKeyFile : Globals . sshPublicKeyURL , privateKeyFile : Globals . sshPrivateKeyURL ) )
2017-02-02 15:02:57 +08:00
} else {
gitCredential = nil
2017-01-31 22:00:50 +08:00
}
2017-01-24 01:49:55 +08:00
2017-01-19 21:15:47 +08:00
}
2017-02-10 22:15:01 +08:00
func initPGP ( pgpPublicKeyURL : URL , pgpPublicKeyLocalPath : String , pgpPrivateKeyURL : URL , pgpPrivateKeyLocalPath : String ) throws {
let pgpPublicData = try Data ( contentsOf : pgpPublicKeyURL )
try pgpPublicData . write ( to : URL ( fileURLWithPath : pgpPublicKeyLocalPath ) , options : . atomic )
let pgpPrivateData = try Data ( contentsOf : pgpPrivateKeyURL )
try pgpPrivateData . write ( to : URL ( fileURLWithPath : pgpPrivateKeyLocalPath ) , options : . atomic )
pgp . importKeys ( fromFile : pgpPublicKeyLocalPath , allowDuplicates : false )
2017-02-13 15:01:04 +08:00
if pgp . getKeysOf ( . public ) . count = = 0 {
throw NSError ( domain : " me.mssun.pass.error " , code : 2 , userInfo : [ NSLocalizedDescriptionKey : " Cannot import public key. " ] )
}
2017-02-10 22:15:01 +08:00
pgp . importKeys ( fromFile : pgpPrivateKeyLocalPath , allowDuplicates : false )
2017-02-13 15:01:04 +08:00
if pgp . getKeysOf ( . secret ) . count = = 0 {
throw NSError ( domain : " me.mssun.pass.error " , code : 2 , userInfo : [ NSLocalizedDescriptionKey : " Cannot import seceret key. " ] )
}
2017-02-10 22:15:01 +08:00
let key = pgp . getKeysOf ( . public ) [ 0 ]
2017-02-06 00:59:27 +08:00
Defaults [ . pgpKeyID ] = key . keyID ! . shortKeyString
if let gpgUser = key . users [ 0 ] as ? PGPUser {
Defaults [ . pgpKeyUserID ] = gpgUser . userID
2017-01-22 01:42:36 +08:00
}
}
2017-01-23 17:36:10 +08:00
func cloneRepository ( remoteRepoURL : URL ,
2017-01-24 01:49:55 +08:00
credential : GitCredential ,
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:24:59 +08:00
let credentialProvider = try credential . credentialProvider ( )
let options : [ String : Any ] = [
GTRepositoryCloneOptionsCredentialProvider : credentialProvider ,
]
2017-02-04 14:59:55 +08:00
storeRepository = try GTRepository . clone ( from : remoteRepoURL , toWorkingDirectory : tempStoreURL , options : options , transferProgressBlock : transferProgressBlock , checkoutProgressBlock : checkoutProgressBlock )
let fm = FileManager . default
do {
if fm . fileExists ( atPath : storeURL . path ) {
try fm . removeItem ( at : storeURL )
}
try fm . copyItem ( at : tempStoreURL , to : storeURL )
try fm . removeItem ( at : tempStoreURL )
} catch {
print ( error )
}
storeRepository = try GTRepository ( url : storeURL )
2017-02-04 14:24:59 +08:00
gitCredential = credential
2017-01-23 16:29:36 +08:00
}
2017-01-24 16:57:16 +08:00
2017-02-04 14:24:59 +08:00
func pullRepository ( transferProgressBlock : @ escaping ( UnsafePointer < git_transfer_progress > , UnsafeMutablePointer < ObjCBool > ) -> Void ) throws {
2017-02-09 19:44:12 +08:00
if gitCredential = = nil {
throw NSError ( domain : " me.mssun.pass.error " , code : 1 , userInfo : [ NSLocalizedDescriptionKey : " Git Repository is not set. " ] )
}
2017-02-04 14:24:59 +08:00
let credentialProvider = try gitCredential ! . credentialProvider ( )
let options : [ String : Any ] = [
GTRepositoryRemoteOptionsCredentialProvider : credentialProvider
]
let remote = try GTRemote ( name : " origin " , in : storeRepository ! )
try storeRepository ? . pull ( ( storeRepository ? . currentBranch ( ) ) ! , from : remote , withOptions : options , progress : transferProgressBlock )
2017-01-19 21:15:47 +08:00
}
func updatePasswordEntityCoreData ( ) {
2017-02-07 16:45:14 +08:00
deleteCoreData ( entityName : " PasswordEntity " )
deleteCoreData ( entityName : " PasswordCategoryEntity " )
2017-01-19 21:15:47 +08:00
let fm = FileManager . default
2017-02-15 11:15:11 +08:00
fm . enumerator ( atPath : self . storeURL . path ) ? . forEach ( { ( e ) in
2017-01-19 21:15:47 +08:00
if let e = e as ? String , let url = URL ( string : e ) {
if url . pathExtension = = " gpg " {
2017-02-15 17:28:15 +08:00
let passwordEntity = NSEntityDescription . insertNewObject ( forEntityName : " PasswordEntity " , into : context ) as ! PasswordEntity
2017-01-23 17:53:49 +08:00
let endIndex = url . lastPathComponent . index ( url . lastPathComponent . endIndex , offsetBy : - 4 )
2017-02-06 21:53:54 +08:00
passwordEntity . name = url . lastPathComponent . substring ( to : endIndex )
2017-02-10 22:15:01 +08:00
passwordEntity . rawPath = " \( url . path ) "
2017-02-06 21:53:54 +08:00
let items = url . path . characters . split ( separator : " / " ) . map ( String . init )
for i in 0 . . < items . count - 1 {
let passwordCategoryEntity = PasswordCategoryEntity ( context : context )
passwordCategoryEntity . category = items [ i ]
2017-02-07 00:21:22 +08:00
passwordCategoryEntity . level = Int16 ( i )
2017-02-06 21:53:54 +08:00
passwordCategoryEntity . password = passwordEntity
}
2017-01-19 21:15:47 +08:00
}
}
} )
do {
try context . save ( )
} catch {
print ( " Error with save: \( error ) " )
}
}
func fetchPasswordEntityCoreData ( ) -> [ PasswordEntity ] {
let passwordEntityFetch = NSFetchRequest < NSFetchRequestResult > ( entityName : " PasswordEntity " )
do {
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-02-06 21:53:54 +08:00
func fetchPasswordCategoryEntityCoreData ( password : PasswordEntity ) -> [ PasswordCategoryEntity ] {
2017-02-07 00:21:22 +08:00
let passwordCategoryEntityFetchRequest = NSFetchRequest < NSFetchRequestResult > ( entityName : " PasswordCategoryEntity " )
passwordCategoryEntityFetchRequest . predicate = NSPredicate ( format : " password = %@ " , password )
passwordCategoryEntityFetchRequest . sortDescriptors = [ NSSortDescriptor ( key : " level " , ascending : true ) ]
2017-02-06 21:53:54 +08:00
do {
2017-02-07 00:21:22 +08:00
let passwordCategoryEntities = try context . fetch ( passwordCategoryEntityFetchRequest ) as ! [ PasswordCategoryEntity ]
2017-02-06 21:53:54 +08:00
return passwordCategoryEntities
2017-02-12 01:59:40 +08:00
} catch {
fatalError ( " Failed to fetch password categories: \( error ) " )
}
}
func fetchUnsyncedPasswords ( ) -> [ PasswordEntity ] {
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 ) " )
}
}
func setAllSynced ( ) {
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 ) " )
}
}
func getNumberOfUnsyncedPasswords ( ) -> Int {
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 {
fatalError ( " Failed to fetch employees: \( error ) " )
}
}
2017-01-19 21:15:47 +08:00
func updateRemoteRepo ( ) {
}
2017-02-07 16:45:14 +08:00
2017-02-13 01:15:42 +08:00
func addEntryToGTTree ( fileData : Data , filename : String ) -> GTTree {
2017-02-11 01:50:39 +08:00
do {
let head = try storeRepository ! . headReference ( )
let branch = GTBranch ( reference : head , repository : storeRepository ! )
let headCommit = try branch ? . targetCommit ( )
let treeBulider = try GTTreeBuilder ( tree : headCommit ? . tree , repository : storeRepository ! )
try treeBulider . addEntry ( with : fileData , fileName : filename , fileMode : GTFileMode . blob )
let newTree = try treeBulider . writeTree ( )
2017-02-13 01:15:42 +08:00
return newTree
} catch {
fatalError ( " Failed to add entries to GTTree: \( error ) " )
}
}
func removeEntryFromGTTree ( filename : String ) -> GTTree {
do {
let head = try storeRepository ! . headReference ( )
let branch = GTBranch ( reference : head , repository : storeRepository ! )
let headCommit = try branch ? . targetCommit ( )
let treeBulider = try GTTreeBuilder ( tree : headCommit ? . tree , repository : storeRepository ! )
try treeBulider . removeEntry ( withFileName : filename )
let newTree = try treeBulider . writeTree ( )
return newTree
} catch {
fatalError ( " Failed to remove entries to GTTree: \( error ) " )
}
}
func createAddCommitInRepository ( message : String , fileData : Data , filename : String , progressBlock : ( _ progress : Float ) -> Void ) -> GTCommit ? {
do {
let newTree = addEntryToGTTree ( fileData : fileData , filename : filename )
let headReference = try storeRepository ! . headReference ( )
let commitEnum = try GTEnumerator ( repository : storeRepository ! )
try commitEnum . pushSHA ( headReference . targetOID . sha )
let parent = commitEnum . nextObject ( ) as ! GTCommit
progressBlock ( 0.5 )
let commit = try storeRepository ! . createCommit ( with : newTree , message : message , parents : [ parent ] , updatingReferenceNamed : headReference . name )
progressBlock ( 0.7 )
return commit
} catch {
print ( error )
}
return nil
}
func createRemoveCommitInRepository ( message : String , filename : String , progressBlock : ( _ progress : Float ) -> Void ) -> GTCommit ? {
do {
let newTree = removeEntryFromGTTree ( filename : filename )
2017-02-11 01:50:39 +08:00
let headReference = try storeRepository ! . headReference ( )
let commitEnum = try GTEnumerator ( repository : storeRepository ! )
try commitEnum . pushSHA ( headReference . targetOID . sha )
let parent = commitEnum . nextObject ( ) as ! GTCommit
2017-02-11 19:48:47 +08:00
progressBlock ( 0.5 )
2017-02-11 01:50:39 +08:00
let commit = try storeRepository ! . createCommit ( with : newTree , message : message , parents : [ parent ] , updatingReferenceNamed : headReference . name )
2017-02-11 19:48:47 +08:00
progressBlock ( 0.7 )
2017-02-11 01:50:39 +08:00
return commit
} catch {
print ( error )
}
return nil
}
2017-02-12 01:59:40 +08:00
2017-02-11 01:50:39 +08:00
private func getLocalBranch ( withName branchName : String ) -> GTBranch ? {
do {
let reference = GTBranch . localNamePrefix ( ) . appending ( branchName )
let branches = try storeRepository ! . branches ( withPrefix : reference )
return branches [ 0 ]
} catch {
print ( error )
}
return nil
}
2017-02-11 19:48:47 +08:00
func pushRepository ( transferProgressBlock : @ escaping ( UInt32 , UInt32 , Int , UnsafeMutablePointer < ObjCBool > ) -> Void ) throws {
2017-02-11 01:50:39 +08:00
let credentialProvider = try gitCredential ! . credentialProvider ( )
let options : [ String : Any ] = [
GTRepositoryRemoteOptionsCredentialProvider : credentialProvider ,
]
let masterBranch = getLocalBranch ( withName : " master " ) !
let remote = try GTRemote ( name : " origin " , in : storeRepository ! )
2017-02-11 19:48:47 +08:00
try storeRepository ? . push ( masterBranch , to : remote , withOptions : options , progress : transferProgressBlock )
2017-02-10 22:15:01 +08:00
}
2017-02-11 19:48:47 +08:00
func add ( password : Password , progressBlock : ( _ progress : Float ) -> Void ) {
progressBlock ( 0.0 )
2017-02-10 22:15:01 +08:00
let passwordEntity = NSEntityDescription . insertNewObject ( forEntityName : " PasswordEntity " , into : context ) as ! PasswordEntity
do {
let encryptedData = try passwordEntity . encrypt ( password : password )
2017-02-11 19:48:47 +08:00
progressBlock ( 0.3 )
2017-02-10 22:15:01 +08:00
let saveURL = storeURL . appendingPathComponent ( " \( password . name ) .gpg " )
try encryptedData . write ( to : saveURL )
passwordEntity . rawPath = " \( password . name ) .gpg "
2017-02-12 01:59:40 +08:00
passwordEntity . synced = false
2017-02-10 22:15:01 +08:00
try context . save ( )
2017-02-11 01:50:39 +08:00
print ( saveURL . path )
2017-02-13 01:15:42 +08:00
let _ = createAddCommitInRepository ( message : " Add new password by pass for iOS " , fileData : encryptedData , filename : saveURL . lastPathComponent , progressBlock : progressBlock )
2017-02-11 19:48:47 +08:00
progressBlock ( 1.0 )
2017-02-10 22:15:01 +08:00
} catch {
print ( error )
}
}
2017-02-13 01:15:42 +08:00
func update ( passwordEntity : PasswordEntity , password : Password , progressBlock : ( _ progress : Float ) -> Void ) {
do {
let encryptedData = try passwordEntity . encrypt ( password : password )
let saveURL = storeURL . appendingPathComponent ( passwordEntity . rawPath ! )
try encryptedData . write ( to : saveURL )
2017-02-15 20:01:17 +08:00
progressBlock ( 0.3 )
2017-02-13 01:15:42 +08:00
let _ = createAddCommitInRepository ( message : " Update password by pass for iOS " , fileData : encryptedData , filename : saveURL . lastPathComponent , progressBlock : progressBlock )
2017-02-15 20:01:17 +08:00
} catch {
print ( error )
}
}
func saveUpdated ( passwordEntity : PasswordEntity ) {
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-02-07 16:45:14 +08:00
func deleteCoreData ( entityName : String ) {
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 )
}
}
func erase ( ) {
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-02-08 19:31:42 +08:00
Utils . removeFileIfExists ( at : Globals . sshPrivateKeyURL )
Utils . removeFileIfExists ( at : Globals . sshPublicKeyURL )
2017-02-07 16:45:14 +08:00
deleteCoreData ( entityName : " PasswordEntity " )
deleteCoreData ( entityName : " PasswordCategoryEntity " )
2017-02-13 14:30:38 +08:00
Defaults . removeAll ( )
storeRepository = nil
2017-02-07 16:45:14 +08:00
}
2017-01-19 21:15:47 +08:00
}