Support folder operations

Example:
  - add: a/b/c/d
  - delete: a/b/c/d
  - move: a/b/c/d -> a/b/c/d/e
This commit is contained in:
Bob Sun 2017-04-23 10:03:09 -07:00
parent b1ef8a343f
commit d8ecd1e889
No known key found for this signature in database
GPG key ID: 1F86BA2052FED3B4
8 changed files with 213 additions and 141 deletions

View file

@ -20,6 +20,15 @@ class Password {
static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"]
var name = ""
var url: URL?
var namePath: String {
get {
if url == nil {
return ""
}
return url!.deletingPathExtension().path
}
}
var password = ""
var additions = [String: String]()
var additionKeys = [String]()
@ -47,19 +56,20 @@ class Password {
}
}
init(name: String, plainText: String) {
self.initEverything(name: name, plainText: plainText)
init(name: String, url: URL?, plainText: String) {
self.initEverything(name: name, url: url, plainText: plainText)
}
func updatePassword(name: String, plainText: String) {
if self.plainText != plainText {
self.initEverything(name: name, plainText: plainText)
func updatePassword(name: String, url: URL?, plainText: String) {
if self.plainText != plainText || self.url != url {
self.initEverything(name: name, url: url, plainText: plainText)
changed = true
}
}
private func initEverything(name: String, plainText: String) {
private func initEverything(name: String, url: URL?, plainText: String) {
self.name = name
self.url = url
self.plainText = plainText
self.additions.removeAll()
self.additionKeys.removeAll()
@ -322,7 +332,7 @@ class Password {
if newOtpauth != nil {
lines.append(newOtpauth!)
}
self.updatePassword(name: self.name, plainText: lines.joined(separator: "\n"))
self.updatePassword(name: self.name, url: self.url, plainText: lines.joined(separator: "\n"))
// get and return the password
return self.otpToken?.currentPassword

View file

@ -21,24 +21,6 @@ extension PasswordEntity {
}
}
func decrypt(passphrase: String) throws -> Password? {
var password: Password?
let encryptedDataPath = URL(fileURLWithPath: "\(Globals.repositoryPath)/\(path!)")
let encryptedData = try Data(contentsOf: encryptedDataPath)
let decryptedData = try PasswordStore.shared.pgp.decryptData(encryptedData, passphrase: passphrase)
let plainText = String(data: decryptedData, encoding: .utf8) ?? ""
password = Password(name: name!, plainText: plainText)
return password
}
func encrypt(password: Password) throws -> Data {
name = password.name
let plainData = password.getPlainData()
let pgp = PasswordStore.shared.pgp
let encryptedData = try pgp.encryptData(plainData, usingPublicKey: pgp.getKeysOf(.public)[0], armored: Defaults[.encryptInArmored])
return encryptedData
}
func getCategoryText() -> String {
var parentEntity = parent
var passwordCategoryArray: [String] = []

View file

@ -284,10 +284,9 @@ class PasswordStore {
}
func passwordExisted(password: Password) -> Bool {
print(password.name)
let passwordEntityFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "PasswordEntity")
do {
passwordEntityFetchRequest.predicate = NSPredicate(format: "name = %@", password.name)
passwordEntityFetchRequest.predicate = NSPredicate(format: "name = %@ and path = %@", password.name, password.url!.path)
let count = try context.count(for: passwordEntityFetchRequest)
if count > 0 {
return true
@ -300,6 +299,32 @@ class PasswordStore {
return true
}
func passwordEntityExisted(path: String) -> Bool {
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
}
func getPasswordEntity(by path: String) -> PasswordEntity? {
let passwordEntityFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "PasswordEntity")
do {
passwordEntityFetchRequest.predicate = NSPredicate(format: "path = %@", path)
return try context.fetch(passwordEntityFetchRequest).first as? PasswordEntity
} catch {
fatalError("Failed to fetch password entities: \(error)")
}
}
func cloneRepository(remoteRepoURL: URL,
credential: GitCredential,
transferProgressBlock: @escaping (UnsafePointer<git_transfer_progress>, UnsafeMutablePointer<ObjCBool>) -> Void,
@ -511,19 +536,17 @@ class PasswordStore {
func updateRemoteRepo() {
}
func createAddCommitInRepository(message: String, fileData: Data, filename: String, progressBlock: (_ progress: Float) -> Void) -> GTCommit? {
func createAddCommitInRepository(message: String, path: String) -> GTCommit? {
do {
try storeRepository?.index().add(fileData, withPath: filename)
try storeRepository?.index().addFile(path)
try storeRepository?.index().write()
let newTree = try storeRepository!.index().writeTree()
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 signature = gitSignatureForNow
let commit = try storeRepository!.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name)
progressBlock(0.7)
return commit
} catch {
print(error)
@ -571,57 +594,93 @@ class PasswordStore {
try storeRepository?.push(masterBranch, to: remote, withOptions: options, progress: transferProgressBlock)
}
func add(password: Password, progressBlock: (_ progress: Float) -> Void) throws {
progressBlock(0.0)
private func addPasswordEntities(password: Password) -> PasswordEntity? {
var passwordURL = password.url!
var paths: [String] = []
while passwordURL.path != "." {
paths.append(passwordURL.path)
passwordURL = passwordURL.deletingLastPathComponent()
}
paths.reverse()
var parentPasswordEntity: PasswordEntity? = nil
for path in paths {
if let passwordEntity = getPasswordEntity(by: path) {
parentPasswordEntity = passwordEntity
} else {
if path.hasSuffix(".gpg") {
return insertPasswordEntity(name: URL(string: path)!.deletingPathExtension().lastPathComponent, path: path, parent: parentPasswordEntity, synced: false, isDir: false)
} else {
parentPasswordEntity = insertPasswordEntity(name: URL(string: path)!.lastPathComponent, path: path, parent: parentPasswordEntity, synced: false, isDir: true)
let fm = FileManager.default
let saveURL = storeURL.appendingPathComponent(path)
do {
try fm.createDirectory(at: saveURL, withIntermediateDirectories: false, attributes: nil)
} catch {
print(error)
}
}
}
}
return nil
}
private func insertPasswordEntity(name: String, path: String, parent: PasswordEntity?, synced: Bool = false, isDir: Bool = false) -> PasswordEntity? {
var ret: PasswordEntity? = nil
DispatchQueue.main.sync {
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)")
}
}
}
return ret
}
func add(password: Password) throws -> PasswordEntity? {
guard !passwordExisted(password: password) else {
throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot add password: password duplicated."])
}
let passwordEntity = NSEntityDescription.insertNewObject(forEntityName: "PasswordEntity", into: context) as! PasswordEntity
do {
let encryptedData = try passwordEntity.encrypt(password: password)
progressBlock(0.3)
let saveURL = storeURL.appendingPathComponent("\(password.name).gpg")
try encryptedData.write(to: saveURL)
passwordEntity.name = password.name
passwordEntity.path = "\(password.name).gpg"
passwordEntity.parent = nil
passwordEntity.synced = false
passwordEntity.isDir = false
try context.save()
print(saveURL.path)
let _ = createAddCommitInRepository(message: "Add password for \(passwordEntity.nameWithCategory) to store using Pass for iOS.", fileData: encryptedData, filename: saveURL.lastPathComponent, progressBlock: progressBlock)
progressBlock(1.0)
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
} catch {
print(error)
}
let newPasswordEntity = addPasswordEntities(password: password)
print("new: \(newPasswordEntity!.path!)")
let saveURL = storeURL.appendingPathComponent(password.url!.path)
try self.encrypt(password: password).write(to: saveURL)
let _ = createAddCommitInRepository(message: "Add password for \(password.url!.deletingPathExtension().path) to store using Pass for iOS.", path: password.url!.path)
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
return newPasswordEntity
}
func update(passwordEntity: PasswordEntity, password: Password, progressBlock: (_ progress: Float) -> Void) {
progressBlock(0.0)
do {
let encryptedData = try passwordEntity.encrypt(password: password)
let saveURL = storeURL.appendingPathComponent(passwordEntity.path!)
try encryptedData.write(to: saveURL)
progressBlock(0.3)
let _ = createAddCommitInRepository(message: "Edit password for \(passwordEntity.nameWithCategory) using Pass for iOS.", fileData: encryptedData, filename: saveURL.lastPathComponent, progressBlock: progressBlock)
progressBlock(1.0)
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
} catch {
print(error)
}
func update(passwordEntity: PasswordEntity, password: Password) throws -> PasswordEntity? {
delete(passwordEntity: passwordEntity)
return try add(password: password)
}
public func delete(passwordEntity: PasswordEntity) {
Utils.removeFileIfExists(at: storeURL.appendingPathComponent(passwordEntity.path!))
let _ = createRemoveCommitInRepository(message: "Remove \(passwordEntity.nameWithCategory) from store using Pass for iOS", path: passwordEntity.path!)
context.delete(passwordEntity)
do {
try context.save()
} catch {
fatalError("Failed to delete a PasswordEntity: \(error)")
DispatchQueue.main.async {
let _ = self.createRemoveCommitInRepository(message: "Remove \(passwordEntity.nameWithCategory) from store using Pass for iOS", path: passwordEntity.path!)
var current: PasswordEntity? = passwordEntity
while current != nil && (current!.children!.count == 0 || !current!.isDir) {
Utils.removeFileIfExists(at: self.storeURL.appendingPathComponent(current!.path!))
let parent = current!.parent
self.context.delete(current!)
current = parent
do {
try self.context.save()
} catch {
fatalError("Failed to delete a PasswordEntity: \(error)")
}
}
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
}
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
}
func saveUpdated(passwordEntity: PasswordEntity) {
@ -741,4 +800,27 @@ class PasswordStore {
// get a list of local commits
return try storeRepository?.localCommitsRelative(toRemoteBranch: remoteMasterBranch)
}
func decrypt(passwordEntity: PasswordEntity, requestPGPKeyPassphrase: () -> String) throws -> Password? {
var password: Password?
let encryptedDataPath = URL(fileURLWithPath: "\(Globals.repositoryPath)/\(passwordEntity.path!)")
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) ?? ""
password = Password(name: passwordEntity.name!, url: URL(string: passwordEntity.path!), plainText: plainText)
return password
}
func encrypt(password: Password) throws -> Data {
let plainData = password.getPlainData()
let pgp = PasswordStore.shared.pgp
let encryptedData = try pgp.encryptData(plainData, usingPublicKey: pgp.getKeysOf(.public)[0], armored: Defaults[.encryptInArmored])
return encryptedData
}
}