From ddddfda9317ab652a20b7e88057785d04f259b25 Mon Sep 17 00:00:00 2001 From: Danny Moesch Date: Sun, 1 Jul 2018 19:49:38 +0200 Subject: [PATCH] Use own parser for multiline values giving up Yams --- Cartfile | 1 - pass.xcodeproj/project.pbxproj | 5 - ...nSourceComponentsTableViewController.swift | 3 - .../PasswordEditorTableViewController.swift | 2 +- passKit/Models/Password.swift | 103 ++++++++--------- passKitTests/PasswordTests.swift | 105 +++++++++++++++--- 6 files changed, 137 insertions(+), 82 deletions(-) diff --git a/Cartfile b/Cartfile index 97ca48da..6fdfbbcc 100644 --- a/Cartfile +++ b/Cartfile @@ -4,4 +4,3 @@ github "libgit2/objective-git" github "leonbreedt/FavIcon" github "kishikawakatsumi/KeychainAccess" github "mattrubin/OneTimePassword" -github "jpsim/Yams" diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 23f9f075..438d1272 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 18F19A67B0C07F13C17169E0 /* Pods_pass.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5620D17DF5E86B61761D0E /* Pods_pass.framework */; }; 23B82F0228254275DBA609E7 /* Pods_passExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B975797E0F0B7476CADD6A7D /* Pods_passExtension.framework */; }; - 3012B06D2039D6E400BE1793 /* Yams.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3012B06C2039D6E400BE1793 /* Yams.framework */; }; 30B04860209A5141001013CA /* PasswordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B0485F209A5141001013CA /* PasswordTests.swift */; }; 61326CDA7A73757FB68DCB04 /* Pods_passKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAB3F5541E51ADC8C6B56642 /* Pods_passKit.framework */; }; A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; }; @@ -160,7 +159,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 3012B06C2039D6E400BE1793 /* Yams.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Yams.framework; path = Carthage/Build/iOS/Yams.framework; sourceTree = ""; }; 30B0485F209A5141001013CA /* PasswordTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTests.swift; sourceTree = ""; }; 31C3033E8868D05B2C55C8B1 /* Pods-passExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-passExtension/Pods-passExtension.debug.xcconfig"; sourceTree = ""; }; 3A5620D17DF5E86B61761D0E /* Pods_pass.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_pass.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -323,7 +321,6 @@ A260758D1EEC6F34005DB03E /* passKit.framework in Frameworks */, DCC408C71E307DBB00F29B0E /* SVProgressHUD.framework in Frameworks */, 18F19A67B0C07F13C17169E0 /* Pods_pass.framework in Frameworks */, - 3012B06D2039D6E400BE1793 /* Yams.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -560,7 +557,6 @@ DC917BED1E2F38C4000FDF54 /* Frameworks */ = { isa = PBXGroup; children = ( - 3012B06C2039D6E400BE1793 /* Yams.framework */, A2A61C101EEF8E3500CFE063 /* libPods-passKit.a */, A2A61C0C1EEF8DFE00CFE063 /* libPods-passExtension.a */, A2227D541EEE5E78002A69A9 /* libObjectivePGP.a */, @@ -958,7 +954,6 @@ "$(SRCROOT)/Carthage/Build/iOS/KeychainAccess.framework", "$(SRCROOT)/Carthage/Build/iOS/OneTimePassword.framework", "$(SRCROOT)/Carthage/Build/iOS/Base32.framework", - "$(SRCROOT)/Carthage/Build/iOS/Yams.framework", ); name = "Run Script"; outputPaths = ( diff --git a/pass/Controllers/OpenSourceComponentsTableViewController.swift b/pass/Controllers/OpenSourceComponentsTableViewController.swift index bdd809ad..d60af7d0 100644 --- a/pass/Controllers/OpenSourceComponentsTableViewController.swift +++ b/pass/Controllers/OpenSourceComponentsTableViewController.swift @@ -32,9 +32,6 @@ class OpenSourceComponentsTableViewController: BasicStaticTableViewController { ["SVProgressHUD", "https://github.com/SVProgressHUD/SVProgressHUD", "https://github.com/SVProgressHUD/SVProgressHUD/blob/master/LICENSE.txt"], - ["Yams", - "https://github.com/jpsim/Yams", - "https://github.com/jpsim/Yams/blob/master/LICENSE"], ] override func viewDidLoad() { diff --git a/pass/Controllers/PasswordEditorTableViewController.swift b/pass/Controllers/PasswordEditorTableViewController.swift index e03eb487..7ed2ed67 100644 --- a/pass/Controllers/PasswordEditorTableViewController.swift +++ b/pass/Controllers/PasswordEditorTableViewController.swift @@ -28,7 +28,7 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl private var navigationItemTitle: String? private var sectionHeaderTitles = ["name", "password", "additions",""].map {$0.uppercased()} - private var sectionFooterTitles = ["", "", "Use YAML format for additional fields.", ""] + private var sectionFooterTitles = ["", "", "Use \"key: value\" format for additional fields.", ""] private let nameSection = 0 private let passwordSection = 1 private let additionsSection = 2 diff --git a/passKit/Models/Password.swift b/passKit/Models/Password.swift index d795721f..e621b32c 100644 --- a/passKit/Models/Password.swift +++ b/passKit/Models/Password.swift @@ -10,7 +10,6 @@ import Foundation import SwiftyUserDefaults import OneTimePassword import Base32 -import Yams public struct AdditionField: Equatable { public var title: String = "" @@ -47,6 +46,13 @@ enum PasswordChange: Int { public class Password { public static let OTP_KEYWORDS = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"] + + public static let BLANK = " " + public static let MULTILINE_WITH_LINE_BREAK_INDICATOR = "|" + public static let MULTILINE_WITH_LINE_BREAK_SEPARATOR = "\n" + public static let MULTILINE_WITHOUT_LINE_BREAK_INDICATOR = ">" + public static let MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR = BLANK + private static let OTPAUTH = "otpauth" private static let OTPAUTH_URL_START = "\(OTPAUTH)://" private static let PASSWORD_KEYWORD = "password" @@ -104,73 +110,58 @@ public class Password { // get remaining lines (filter out empty lines) let additionalLines = plainTextSplit[1...].filter { !$0.isEmpty } - - // separate normal lines (no otp tokens) - let normalAdditionalLines = additionalLines.filter { - !$0.hasPrefix(Password.OTPAUTH_URL_START) - }.joined(separator: "\n") - - // try to interpret the text format as YAML first - do { - try getAdditionalFields(fromYaml: normalAdditionalLines) - } - catch { - getAdditionalFields(fromPlainText: normalAdditionalLines) - } - // get and append otp tokens - let otpAdditionalLines = additionalLines.filter { $0.hasPrefix(Password.OTPAUTH_URL_START) } - otpAdditionalLines.forEach { self.additions.append(AdditionField(title: Password.OTPAUTH, content: $0)) } - + // parse lines to get key-value pairs + parseDataFrom(lines: additionalLines) + // check whether the first line looks like an otp entry - let (key, value) = Password.getKeyValuePair(from: self.password) - if Password.OTP_KEYWORDS.contains(key ?? "") { - firstLineIsOTPField = true - self.additions.append(AdditionField(title: key!, content: value)) - } else { - firstLineIsOTPField = false - } - + checkPasswordForOtpToken() + // construct the otp token updateOtpToken() } - - // check whether the file has lines with duplicated field names - private func checkDuplicatedFields(lines: String) -> Bool { - var keys = Set() - var hasDuplicatedFields = false - lines.enumerateLines { (line, stop) -> () in - let (key, _) = Password.getKeyValuePair(from: line) - if let key = key { - hasDuplicatedFields = !keys.insert(key).0 - stop = hasDuplicatedFields + + private func parseDataFrom(lines: [String]) { + var unknownIndex = 0 + var i = lines.startIndex + while i < lines.count { + let line = lines[i] + i += 1 + var (key, value) = Password.getKeyValuePair(from: line) + if key == nil { + unknownIndex += 1 + key = "\(Password.UNKNOWN) \(unknownIndex)" + } else if value == Password.MULTILINE_WITH_LINE_BREAK_INDICATOR { + value = gatherMultilineValue(from: lines, startingAt: &i, removingLineBreaks: false) + } else if value == Password.MULTILINE_WITHOUT_LINE_BREAK_INDICATOR { + value = gatherMultilineValue(from: lines, startingAt: &i, removingLineBreaks: true) } + additions.append(AdditionField(title: key!, content: value)) } - return hasDuplicatedFields } - private func getAdditionalFields(fromYaml: String) throws { - guard !fromYaml.isEmpty else { return } - if checkDuplicatedFields(lines: fromYaml) { - throw AppError.YamlLoadError - } - guard let yamlFile = try Yams.load(yaml: fromYaml) as? [String: String] else { - throw AppError.YamlLoadError + private func gatherMultilineValue(from content: [String], startingAt i: inout Int, removingLineBreaks: Bool) -> String { + var result = "" + guard i < content.count else { return result } + let numberInitialBlanks = content[i].enumerated().first(where: { $1 != Character(Password.BLANK) })?.0 ?? content[i].count + guard numberInitialBlanks != 0 else { return result } + let initialBlanks = String(repeating: Password.BLANK, count: numberInitialBlanks) + + while i < content.count && content[i].starts(with: initialBlanks) { + result.append(String(content[i].dropFirst(numberInitialBlanks))) + result.append(removingLineBreaks ? Password.MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR : Password.MULTILINE_WITH_LINE_BREAK_SEPARATOR) + i += 1 } - additions.append(contentsOf: yamlFile.map { AdditionField(title: $0, content: String(describing: $1)) }) + return result.trimmingCharacters(in: .whitespacesAndNewlines) } - private func getAdditionalFields(fromPlainText: String) { - var unknownIndex = 0 - fromPlainText.enumerateLines() { line, _ in - if !line.isEmpty { - var (key, value) = Password.getKeyValuePair(from: line) - if key == nil { - unknownIndex += 1 - key = "\(Password.UNKNOWN) \(unknownIndex)" - } - self.additions.append(AdditionField(title: key!, content: value)) - } + private func checkPasswordForOtpToken() { + let (key, value) = Password.getKeyValuePair(from: self.password) + if Password.OTP_KEYWORDS.contains(key ?? "") { + firstLineIsOTPField = true + self.additions.append(AdditionField(title: key!, content: value)) + } else { + firstLineIsOTPField = false } } diff --git a/passKitTests/PasswordTests.swift b/passKitTests/PasswordTests.swift index 5fc256f8..49e81529 100644 --- a/passKitTests/PasswordTests.swift +++ b/passKitTests/PasswordTests.swift @@ -22,6 +22,7 @@ class PasswordTest: XCTestCase { static let LOGIN_FIELD = AdditionField(title: "login", content: "login name") static let USERNAME_FIELD = AdditionField(title: "username", content: "some username") static let NOTE_FIELD = AdditionField(title: "note", content: "A NOTE") + static let HINT_FIELD = AdditionField(title: "some hints", content: "äöüß // €³ %% −° && @²` | [{\\}],.<>") func testUrl() { let password1 = getPasswordObjectWith(content: PasswordTest.EMPTY_STRING) @@ -87,10 +88,10 @@ class PasswordTest: XCTestCase { XCTAssertEqual(password.plainData, fileContent.data(using: .utf8)) XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(urlField, loginField, usernameField, noteField)) - XCTAssertTrue(does(password: password, contain: urlField)) - XCTAssertFalse(does(password: password, contain: loginField)) - XCTAssertFalse(does(password: password, contain: usernameField)) - XCTAssertTrue(does(password: password, contain: noteField)) + XCTAssertTrue(does(password, contain: urlField)) + XCTAssertFalse(does(password, contain: loginField)) + XCTAssertFalse(does(password, contain: usernameField)) + XCTAssertTrue(does(password, contain: noteField)) XCTAssertEqual(password.urlString, urlField.content) XCTAssertEqual(password.login, loginField.content) @@ -112,8 +113,8 @@ class PasswordTest: XCTestCase { XCTAssertEqual(password.plainData, fileContent.data(using: .utf8)) XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(secondPasswordString, urlField.asString)) - XCTAssertTrue(does(password: password, contain: urlField)) - XCTAssertTrue(does(password: password, contain: AdditionField(title: "unknown 1", content: secondPasswordString))) + XCTAssertTrue(does(password, contain: urlField)) + XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 1", content: secondPasswordString))) XCTAssertNil(password.username) XCTAssertEqual(password.urlString, urlField.content) @@ -133,7 +134,7 @@ class PasswordTest: XCTestCase { XCTAssertEqual(password.plainData, fileContent.data(using: .utf8)) XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(noteField)) - XCTAssertTrue(does(password: password, contain: noteField)) + XCTAssertTrue(does(password, contain: noteField)) XCTAssertNil(password.username) XCTAssertNil(password.urlString) @@ -155,8 +156,8 @@ class PasswordTest: XCTestCase { XCTAssertEqual(password.plainData, fileContent.data(using: .utf8)) XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(urlField1, urlField2)) - XCTAssertTrue(does(password: password, contain: urlField1)) - XCTAssertTrue(does(password: password, contain: urlField2)) + XCTAssertTrue(does(password, contain: urlField1)) + XCTAssertTrue(does(password, contain: urlField2)) XCTAssertNil(password.username) XCTAssertEqual(password.urlString, urlField1.content) @@ -186,12 +187,12 @@ class PasswordTest: XCTestCase { XCTAssertEqual(password.plainData, fileContent.data(using: .utf8)) XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(value1, noteField.asString, value2, value3, urlField.asString, value4)) - XCTAssertTrue(does(password: password, contain: AdditionField(title: "unknown 1", content: value1))) - XCTAssertTrue(does(password: password, contain: noteField)) - XCTAssertTrue(does(password: password, contain: AdditionField(title: "unknown 2", content: value2))) - XCTAssertTrue(does(password: password, contain: AdditionField(title: "unknown 3", content: value3))) - XCTAssertTrue(does(password: password, contain: urlField)) - XCTAssertTrue(does(password: password, contain: AdditionField(title: "unknown 4", content: value4))) + XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 1", content: value1))) + XCTAssertTrue(does(password, contain: noteField)) + XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 2", content: value2))) + XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 3", content: value3))) + XCTAssertTrue(does(password, contain: urlField)) + XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 4", content: value4))) XCTAssertNil(password.username) XCTAssertEqual(password.urlString, urlField.content) @@ -252,11 +253,83 @@ class PasswordTest: XCTestCase { XCTAssertNil(password.getOtp()) } + func testEmptyMultilineValues() { + let passwordString = PasswordTest.PASSWORD_STRING + let lineBreakField1 = AdditionField(title: "with line breaks", content: "| \n") + let lineBreakField2 = AdditionField(title: "with line breaks", content: "| \n ") + let noteField = PasswordTest.NOTE_FIELD + let noLineBreakField = AdditionField(title: "without line breaks", content: " > ") + let fileContent = """ + \(passwordString) + \(lineBreakField1.asString) + \(lineBreakField2.asString) + \(noteField.asString) + \(noLineBreakField.asString) + """ + let password = getPasswordObjectWith(content: fileContent) + + XCTAssertEqual(password.password, passwordString) + XCTAssertEqual(password.plainData, fileContent.data(using: .utf8)) + + XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(lineBreakField1, lineBreakField2, noteField, noLineBreakField)) + XCTAssertTrue(does(password, contain: AdditionField(title: lineBreakField1.title, content: ""))) + XCTAssertTrue(does(password, contain: AdditionField(title: lineBreakField2.title, content: ""))) + XCTAssertTrue(does(password, contain: noteField)) + XCTAssertTrue(does(password, contain: AdditionField(title: noLineBreakField.title, content: ""))) + } + + func testMultilineValues() { + let passwordString = PasswordTest.PASSWORD_STRING + let noteField = PasswordTest.NOTE_FIELD + let lineBreakField = AdditionField(title: "with line breaks", content: "|\n This is \n text spread over \n multiple lines! ") + let noLineBreakField = AdditionField(title: "without line breaks", content: " > \n This is \n text spread over\n multiple lines!") + let fileContent = """ + \(passwordString) + \(lineBreakField.asString) + \(noteField.asString) + \(noLineBreakField.asString) + """ + let password = getPasswordObjectWith(content: fileContent) + + XCTAssertEqual(password.password, passwordString) + XCTAssertEqual(password.plainData, fileContent.data(using: .utf8)) + + XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(lineBreakField, noteField, noLineBreakField)) + XCTAssertTrue(does(password, contain: AdditionField(title: lineBreakField.title, content: "This is \n text spread over \nmultiple lines!"))) + XCTAssertTrue(does(password, contain: noteField)) + XCTAssertTrue(does(password, contain: AdditionField(title: noLineBreakField.title, content: "This is text spread over multiple lines!"))) + } + + func testMultilineValuesMixed() { + let passwordString = PasswordTest.PASSWORD_STRING + let hintField = PasswordTest.HINT_FIELD + let noteField = PasswordTest.NOTE_FIELD + let lineBreakField = AdditionField(title: "with line breaks", content: "|\n This is \n \(hintField.asString) spread over\n multiple lines!") + let noLineBreakField = AdditionField(title: "without line breaks", content: " > \n This is \n | \n text spread over\nmultiple lines!") + let fileContent = """ + \(passwordString) + \(lineBreakField.asString) + \(noLineBreakField.asString) + \(noteField.asString) + """ + let password = getPasswordObjectWith(content: fileContent) + + XCTAssertEqual(password.password, passwordString) + XCTAssertEqual(password.plainData, fileContent.data(using: .utf8)) + + XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(lineBreakField, noLineBreakField, noteField)) + XCTAssertTrue(does(password, contain: AdditionField(title: lineBreakField.title, content: "This is \n\(hintField.asString) spread over"))) + XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 1", content: " multiple lines!"))) + XCTAssertTrue(does(password, contain: AdditionField(title: noLineBreakField.title, content: "This is | text spread over"))) + XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 2", content: "multiple lines!"))) + XCTAssertTrue(does(password, contain: noteField)) + } + private func getPasswordObjectWith(content: String, url: URL? = PasswordTest.PASSWORD_URL) -> Password { return Password(name: PasswordTest.PASSWORD_NAME, url: url, plainText: content) } - private func does(password: Password, contain field: AdditionField) -> Bool { + private func does(_ password: Password, contain field: AdditionField) -> Bool { return password.getFilteredAdditions().contains(field) }