Separate parser and helpers from Password class for better testability
This commit is contained in:
parent
2abbceb2e9
commit
7c12263458
17 changed files with 913 additions and 537 deletions
|
|
@ -9,7 +9,16 @@
|
|||
/* 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 */; };
|
||||
30B04860209A5141001013CA /* PasswordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B0485F209A5141001013CA /* PasswordTests.swift */; };
|
||||
301F6463216162550071A4CE /* AdditionField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F6462216162550071A4CE /* AdditionField.swift */; };
|
||||
301F6466216164830071A4CE /* PasswordHelpersTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F6465216164830071A4CE /* PasswordHelpersTest.swift */; };
|
||||
301F6468216165290071A4CE /* ConstantsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F6467216165290071A4CE /* ConstantsTest.swift */; };
|
||||
301F646A216166000071A4CE /* StringExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F6469216166000071A4CE /* StringExtensionTest.swift */; };
|
||||
301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F646C216166AA0071A4CE /* AdditionFieldTest.swift */; };
|
||||
302E85612125ECC70031BA64 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302E85602125ECC70031BA64 /* Parser.swift */; };
|
||||
302E85632125EE550031BA64 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302E85622125EE550031BA64 /* Constants.swift */; };
|
||||
30AAC05321989DCE00F656CE /* PasswordHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AAC05221989DCE00F656CE /* PasswordHelpers.swift */; };
|
||||
30B04860209A5141001013CA /* PasswordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B0485F209A5141001013CA /* PasswordTest.swift */; };
|
||||
30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FD2F77214D9E0E005E0A92 /* ParserTest.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 */; };
|
||||
A2168A7F1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2168A7E1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift */; };
|
||||
|
|
@ -175,7 +184,16 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
30B0485F209A5141001013CA /* PasswordTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTests.swift; sourceTree = "<group>"; };
|
||||
301F6462216162550071A4CE /* AdditionField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionField.swift; sourceTree = "<group>"; };
|
||||
301F6465216164830071A4CE /* PasswordHelpersTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordHelpersTest.swift; sourceTree = "<group>"; };
|
||||
301F6467216165290071A4CE /* ConstantsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsTest.swift; sourceTree = "<group>"; };
|
||||
301F6469216166000071A4CE /* StringExtensionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensionTest.swift; sourceTree = "<group>"; };
|
||||
301F646C216166AA0071A4CE /* AdditionFieldTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionFieldTest.swift; sourceTree = "<group>"; };
|
||||
302E85602125ECC70031BA64 /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
|
||||
302E85622125EE550031BA64 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||
30AAC05221989DCE00F656CE /* PasswordHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasswordHelpers.swift; path = Helpers/PasswordHelpers.swift; sourceTree = "<group>"; };
|
||||
30B0485F209A5141001013CA /* PasswordTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTest.swift; sourceTree = "<group>"; };
|
||||
30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
3A5620D17DF5E86B61761D0E /* Pods_pass.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_pass.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
666769E0B255666D02945C15 /* Pods-passKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-passKitTests/Pods-passKitTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
|
|
@ -361,6 +379,43 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
301F6464216164670071A4CE /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
301F6465216164830071A4CE /* PasswordHelpersTest.swift */,
|
||||
301F6469216166000071A4CE /* StringExtensionTest.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30C015A3214ECF2B005BB6DF /* Parser */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
301F6462216162550071A4CE /* AdditionField.swift */,
|
||||
302E85622125EE550031BA64 /* Constants.swift */,
|
||||
302E85602125ECC70031BA64 /* Parser.swift */,
|
||||
);
|
||||
path = Parser;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30C015A6214ED32A005BB6DF /* Parser */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
301F646C216166AA0071A4CE /* AdditionFieldTest.swift */,
|
||||
301F6467216165290071A4CE /* ConstantsTest.swift */,
|
||||
30FD2F77214D9E0E005E0A92 /* ParserTest.swift */,
|
||||
);
|
||||
path = Parser;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
30C015A7214ED378005BB6DF /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
30B0485F209A5141001013CA /* PasswordTest.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A2168A801EFD431A005EA873 /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -413,6 +468,7 @@
|
|||
A2F4E20F1EED7F0A0011986E /* Helpers */,
|
||||
A260757B1EEC6F34005DB03E /* Info.plist */,
|
||||
A2F4E20E1EED7F040011986E /* Models */,
|
||||
30C015A3214ECF2B005BB6DF /* Parser */,
|
||||
A26075A51EEC7125005DB03E /* pass.xcdatamodeld */,
|
||||
A260757A1EEC6F34005DB03E /* passKit.h */,
|
||||
);
|
||||
|
|
@ -422,9 +478,11 @@
|
|||
A26075861EEC6F34005DB03E /* passKitTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
301F6464216164670071A4CE /* Helpers */,
|
||||
30C015A7214ED378005BB6DF /* Models */,
|
||||
30C015A6214ED32A005BB6DF /* Parser */,
|
||||
A26075871EEC6F34005DB03E /* passKitTests.swift */,
|
||||
A26075891EEC6F34005DB03E /* Info.plist */,
|
||||
30B0485F209A5141001013CA /* PasswordTests.swift */,
|
||||
);
|
||||
path = passKitTests;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -455,8 +513,8 @@
|
|||
A2F4E20E1EED7F040011986E /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A2C532BA201E5A9600DB9F53 /* PasscodeLock.swift */,
|
||||
A2F4E2101EED800F0011986E /* GitCredential.swift */,
|
||||
A2C532BA201E5A9600DB9F53 /* PasscodeLock.swift */,
|
||||
A2F4E2111EED800F0011986E /* Password.swift */,
|
||||
A2F4E2121EED800F0011986E /* PasswordEntity.swift */,
|
||||
A2F4E2131EED800F0011986E /* PasswordStore.swift */,
|
||||
|
|
@ -469,13 +527,14 @@
|
|||
children = (
|
||||
A2F4E2181EED80160011986E /* AppError.swift */,
|
||||
A2F4E2191EED80160011986E /* DefaultsKeys.swift */,
|
||||
A239F5202157B75E00576CBF /* FileManagerExtension.swift */,
|
||||
A2F4E21A1EED80160011986E /* Globals.swift */,
|
||||
A2F4E21B1EED80160011986E /* NotificationNames.swift */,
|
||||
A2F4E21D1EED80160011986E /* Utils.swift */,
|
||||
30AAC05221989DCE00F656CE /* PasswordHelpers.swift */,
|
||||
A239F51E2157B72700576CBF /* StringExtension.swift */,
|
||||
A2F4E21C1EED80160011986E /* UITextFieldExtension.swift */,
|
||||
A2BEC1BA207D2EFE00F3051C /* UIViewExtension.swift */,
|
||||
A239F51E2157B72700576CBF /* StringExtension.swift */,
|
||||
A239F5202157B75E00576CBF /* FileManagerExtension.swift */,
|
||||
A2F4E21D1EED80160011986E /* Utils.swift */,
|
||||
);
|
||||
name = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -577,15 +636,15 @@
|
|||
DC917BCA1E2E8231000FDF54 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DC917BD51E2E8231000FDF54 /* pass */,
|
||||
A26075791EEC6F34005DB03E /* passKit */,
|
||||
A26700251EEC466A00176B8A /* passExtension */,
|
||||
A239F5972158C08C00576CBF /* passAutoFillExtension */,
|
||||
DC13B14F1E8640810097803F /* passTests */,
|
||||
A26075861EEC6F34005DB03E /* passKitTests */,
|
||||
DC917BD41E2E8231000FDF54 /* Products */,
|
||||
DC917BED1E2F38C4000FDF54 /* Frameworks */,
|
||||
DC917BD51E2E8231000FDF54 /* pass */,
|
||||
A239F5972158C08C00576CBF /* passAutoFillExtension */,
|
||||
A26700251EEC466A00176B8A /* passExtension */,
|
||||
A26075791EEC6F34005DB03E /* passKit */,
|
||||
A26075861EEC6F34005DB03E /* passKitTests */,
|
||||
DC13B14F1E8640810097803F /* passTests */,
|
||||
A51B01737D08DB47BB58F85A /* Pods */,
|
||||
DC917BD41E2E8231000FDF54 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -1088,6 +1147,8 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A2BEC1BB207D2EFE00F3051C /* UIViewExtension.swift in Sources */,
|
||||
302E85632125EE550031BA64 /* Constants.swift in Sources */,
|
||||
301F6463216162550071A4CE /* AdditionField.swift in Sources */,
|
||||
A2C532BB201E5A9600DB9F53 /* PasscodeLock.swift in Sources */,
|
||||
A2F4E2151EED800F0011986E /* Password.swift in Sources */,
|
||||
A26075AD1EEC7125005DB03E /* pass.xcdatamodeld in Sources */,
|
||||
|
|
@ -1095,10 +1156,12 @@
|
|||
A239F5212157B75E00576CBF /* FileManagerExtension.swift in Sources */,
|
||||
A2F4E21E1EED80160011986E /* AppError.swift in Sources */,
|
||||
A2F4E2171EED800F0011986E /* PasswordStore.swift in Sources */,
|
||||
302E85612125ECC70031BA64 /* Parser.swift in Sources */,
|
||||
A2F4E2211EED80160011986E /* NotificationNames.swift in Sources */,
|
||||
A2F4E2221EED80160011986E /* UITextFieldExtension.swift in Sources */,
|
||||
A2C532BF201E5AA100DB9F53 /* PasscodeLockPresenter.swift in Sources */,
|
||||
A2C532BE201E5AA100DB9F53 /* PasscodeLockViewController.swift in Sources */,
|
||||
30AAC05321989DCE00F656CE /* PasswordHelpers.swift in Sources */,
|
||||
A2F4E2201EED80160011986E /* Globals.swift in Sources */,
|
||||
A2F4E2231EED80160011986E /* Utils.swift in Sources */,
|
||||
A2F4E21F1EED80160011986E /* DefaultsKeys.swift in Sources */,
|
||||
|
|
@ -1111,7 +1174,12 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
30B04860209A5141001013CA /* PasswordTests.swift in Sources */,
|
||||
301F646A216166000071A4CE /* StringExtensionTest.swift in Sources */,
|
||||
301F6466216164830071A4CE /* PasswordHelpersTest.swift in Sources */,
|
||||
301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */,
|
||||
30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */,
|
||||
30B04860209A5141001013CA /* PasswordTest.swift in Sources */,
|
||||
301F6468216165290071A4CE /* ConstantsTest.swift in Sources */,
|
||||
A26075881EEC6F34005DB03E /* passKitTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class EditPasswordTableViewController: PasswordEditorTableViewController {
|
|||
tableData = [
|
||||
[[.type: PasswordEditorCellType.nameCell, .title: "name", .content: password!.namePath]],
|
||||
[[.type: PasswordEditorCellType.fillPasswordCell, .title: "password", .content: password!.password]],
|
||||
[[.type: PasswordEditorCellType.additionsCell, .title: "additions", .content: password!.getAdditionsPlainText()]],
|
||||
[[.type: PasswordEditorCellType.additionsCell, .title: "additions", .content: password!.additionsPlainText]],
|
||||
[[.type: PasswordEditorCellType.scanQRCodeCell],
|
||||
[.type: PasswordEditorCellType.deletePasswordCell]]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
|
|||
}
|
||||
|
||||
@objc func tapHideOTPSwitchDetailButton(_ sender: Any?) {
|
||||
let keywordsString = Password.OTP_KEYWORDS.joined(separator: ",")
|
||||
let keywordsString = Constants.OTP_KEYWORDS.joined(separator: ",")
|
||||
let alertMessage = "Turn on this switch to hide the fields related to one time passwords (i.e., \(keywordsString))."
|
||||
let alertTitle = "Hide One Time Password Fields"
|
||||
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)
|
||||
|
|
|
|||
|
|
@ -185,8 +185,7 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl
|
|||
// generate password, copy to pasteboard, and set the cell
|
||||
// check whether the current password looks like an OTP field
|
||||
func generateAndCopyPassword() {
|
||||
if let currentPassword = fillPasswordCell?.getContent(),
|
||||
Password.LooksLikeOTP(line: currentPassword) {
|
||||
if let currentPassword = fillPasswordCell?.getContent(), Constants.isOtpRelated(line: currentPassword) {
|
||||
let alert = UIAlertController(title: "Overwrite?", message: "Overwrite the one-time password configuration?", preferredStyle: UIAlertControllerStyle.alert)
|
||||
alert.addAction(UIAlertAction(title: "Yes", style: UIAlertActionStyle.destructive, handler: {_ in
|
||||
self.generateAndCopyPasswordNoOtpCheck()
|
||||
|
|
|
|||
30
passKit/Helpers/PasswordHelpers.swift
Normal file
30
passKit/Helpers/PasswordHelpers.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// PasswordHelpers.swift
|
||||
// passKit
|
||||
//
|
||||
// Created by Danny Moesch on 17.08.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
import OneTimePassword
|
||||
|
||||
public enum OtpType {
|
||||
case totp, hotp, none
|
||||
|
||||
init(token: Token?) {
|
||||
switch token?.generator.factor {
|
||||
case .some(.counter):
|
||||
self = .hotp
|
||||
case .some(.timer):
|
||||
self = .totp
|
||||
default:
|
||||
self = .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PasswordChange: Int {
|
||||
case path = 0x01
|
||||
case content = 0x02
|
||||
case none = 0x00
|
||||
}
|
||||
|
|
@ -16,3 +16,9 @@ public extension String {
|
|||
return addingPercentEncoding(withAllowedCharacters: allowed)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
static func | (left: String, right: String) -> String {
|
||||
return right.isEmpty ? left : left + "\n" + right
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,163 +6,99 @@
|
|||
// Copyright © 2017 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftyUserDefaults
|
||||
import OneTimePassword
|
||||
import Base32
|
||||
import KeychainAccess
|
||||
|
||||
public struct AdditionField: Equatable {
|
||||
public var title: String = ""
|
||||
public var content: String = ""
|
||||
|
||||
var asString: String { return title.isEmpty ? content : title + ": " + content }
|
||||
var asTuple: (String, String) { return (title, content) }
|
||||
|
||||
public static func == (first: AdditionField, second: AdditionField) -> Bool {
|
||||
return first.asTuple == second.asTuple
|
||||
}
|
||||
}
|
||||
|
||||
public enum OtpType {
|
||||
case totp, hotp, none
|
||||
|
||||
static func from(token: Token?) -> OtpType {
|
||||
switch token?.generator.factor {
|
||||
case .some(.counter):
|
||||
return .hotp
|
||||
case .some(.timer):
|
||||
return .totp
|
||||
default:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PasswordChange: Int {
|
||||
case path = 0x01
|
||||
case content = 0x02
|
||||
case none = 0x00
|
||||
}
|
||||
|
||||
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"
|
||||
private static let USERNAME_KEYWORD = "username"
|
||||
private static let LOGIN_KEYWORD = "login"
|
||||
private static let URL_KEYWORD = "url"
|
||||
private static let UNKNOWN = "unknown"
|
||||
|
||||
|
||||
public var name: String
|
||||
public var url: URL
|
||||
public var namePath: String { return url.deletingPathExtension().path }
|
||||
public var plainText: String
|
||||
|
||||
public var password = ""
|
||||
public var changed: Int = 0
|
||||
public var plainText = ""
|
||||
public var plainData: Data { return plainText.data(using: .utf8)! }
|
||||
public var username: String? { return getAdditionValue(withKey: Password.USERNAME_KEYWORD, caseSensitive: false) }
|
||||
public var login: String? { return getAdditionValue(withKey: Password.LOGIN_KEYWORD, caseSensitive: false) }
|
||||
public var urlString: String? { return getAdditionValue(withKey: Password.URL_KEYWORD, caseSensitive: false) }
|
||||
public var otpType: OtpType = .none
|
||||
|
||||
private var parser = Parser(plainText: "")
|
||||
private var additions = [AdditionField]()
|
||||
private var firstLineIsOTPField = false
|
||||
private var otpToken: Token?
|
||||
public var otpType: OtpType { return OtpType.from(token: self.otpToken) }
|
||||
private var otpToken: Token? {
|
||||
didSet {
|
||||
otpType = OtpType(token: otpToken)
|
||||
}
|
||||
}
|
||||
|
||||
public var namePath: String {
|
||||
return url.deletingPathExtension().path
|
||||
}
|
||||
|
||||
public var password: String {
|
||||
return parser.firstLine
|
||||
}
|
||||
|
||||
public var plainData: Data {
|
||||
return plainText.data(using: .utf8)!
|
||||
}
|
||||
|
||||
public var additionsPlainText: String {
|
||||
return parser.additionsSection
|
||||
}
|
||||
|
||||
public var username: String? {
|
||||
return getAdditionValue(withKey: Constants.USERNAME_KEYWORD)
|
||||
}
|
||||
|
||||
public var login: String? {
|
||||
return getAdditionValue(withKey: Constants.LOGIN_KEYWORD)
|
||||
}
|
||||
|
||||
public var urlString: String? {
|
||||
return getAdditionValue(withKey: Constants.URL_KEYWORD)
|
||||
}
|
||||
|
||||
public init(name: String, url: URL, plainText: String) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.initEverything(name: name, url: url, plainText: plainText)
|
||||
}
|
||||
|
||||
public func updatePassword(name: String, url: URL, plainText: String) {
|
||||
if self.plainText != plainText || self.url != url {
|
||||
if self.plainText != plainText {
|
||||
changed = changed|PasswordChange.content.rawValue
|
||||
}
|
||||
if self.url != url {
|
||||
changed = changed|PasswordChange.path.rawValue
|
||||
}
|
||||
self.initEverything(name: name, url: url, plainText: plainText)
|
||||
}
|
||||
}
|
||||
|
||||
private func initEverything(name: String, url: URL, plainText: String) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.plainText = plainText
|
||||
additions.removeAll()
|
||||
|
||||
// split the plain text
|
||||
let plainTextSplit = self.plainText
|
||||
.split(omittingEmptySubsequences: false) { $0 == "\n" || $0 == "\r\n" }
|
||||
.map(String.init)
|
||||
initEverything()
|
||||
}
|
||||
|
||||
|
||||
// get password
|
||||
password = plainTextSplit.first ?? ""
|
||||
public func updatePassword(name: String, url: URL, plainText: String) {
|
||||
guard self.plainText != plainText || self.url != url else {
|
||||
return
|
||||
}
|
||||
|
||||
// get remaining lines (filter out empty lines)
|
||||
let additionalLines = plainTextSplit[1...].filter { !$0.isEmpty }
|
||||
if self.plainText != plainText {
|
||||
self.plainText = plainText
|
||||
changed = changed|PasswordChange.content.rawValue
|
||||
}
|
||||
if self.url != url {
|
||||
self.url = url
|
||||
changed = changed|PasswordChange.path.rawValue
|
||||
}
|
||||
|
||||
// parse lines to get key-value pairs
|
||||
parseDataFrom(lines: additionalLines)
|
||||
self.name = name
|
||||
initEverything()
|
||||
}
|
||||
|
||||
// check whether the first line looks like an otp entry
|
||||
private func initEverything() {
|
||||
parser = Parser(plainText: self.plainText)
|
||||
additions = parser.additionFields
|
||||
|
||||
// Check whether the first line looks like an otp entry.
|
||||
checkPasswordForOtpToken()
|
||||
|
||||
// construct the otp token
|
||||
// Construct the otp token.
|
||||
updateOtpToken()
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return result.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
private func checkPasswordForOtpToken() {
|
||||
let (key, value) = Password.getKeyValuePair(from: self.password)
|
||||
if Password.OTP_KEYWORDS.contains(key ?? "") {
|
||||
let (key, value) = Parser.getKeyValuePair(from: password)
|
||||
if Constants.OTP_KEYWORDS.contains(key ?? "") {
|
||||
firstLineIsOTPField = true
|
||||
self.additions.append(AdditionField(title: key!, content: value))
|
||||
additions.append(key! => value)
|
||||
} else {
|
||||
firstLineIsOTPField = false
|
||||
}
|
||||
|
|
@ -170,48 +106,17 @@ public class Password {
|
|||
|
||||
public func getFilteredAdditions() -> [AdditionField] {
|
||||
return additions.filter { field in
|
||||
field.title.lowercased() != Password.USERNAME_KEYWORD
|
||||
&& field.title.lowercased() != Password.LOGIN_KEYWORD
|
||||
&& field.title.lowercased() != Password.PASSWORD_KEYWORD
|
||||
&& (!field.title.hasPrefix(Password.UNKNOWN) || !SharedDefaults[.isHideUnknownOn])
|
||||
&& (!Password.OTP_KEYWORDS.contains(field.title) || !SharedDefaults[.isHideOTPOn])
|
||||
field.title.lowercased() != Constants.USERNAME_KEYWORD
|
||||
&& field.title.lowercased() != Constants.LOGIN_KEYWORD
|
||||
&& field.title.lowercased() != Constants.PASSWORD_KEYWORD
|
||||
&& (!field.title.hasPrefix(Constants.UNKNOWN) || !SharedDefaults[.isHideUnknownOn])
|
||||
&& (!Constants.OTP_KEYWORDS.contains(field.title) || !SharedDefaults[.isHideOTPOn])
|
||||
}
|
||||
}
|
||||
|
||||
// return a key-value pair from the line
|
||||
// key might be nil, if there is no ":" in the line
|
||||
private static func getKeyValuePair(from line: String) -> (key: String?, value: String) {
|
||||
let items = line.components(separatedBy: ": ").map{String($0).trimmingCharacters(in: .whitespaces)}
|
||||
var key : String? = nil
|
||||
var value = ""
|
||||
if items.count == 1 || (items[0].isEmpty && items[1].isEmpty) {
|
||||
// no ": " found, or empty on both sides of ": "
|
||||
value = line
|
||||
// otpauth special case
|
||||
if value.hasPrefix(Password.OTPAUTH_URL_START) {
|
||||
key = Password.OTPAUTH
|
||||
}
|
||||
} else {
|
||||
if !items[0].isEmpty {
|
||||
key = items[0]
|
||||
}
|
||||
value = items[1]
|
||||
}
|
||||
return (key, value)
|
||||
}
|
||||
|
||||
public func getAdditionsPlainText() -> String {
|
||||
// lines starting from the second
|
||||
let plainTextSplit = plainText.split(maxSplits: 1, omittingEmptySubsequences: false) {
|
||||
$0 == "\n" || $0 == "\r\n"
|
||||
}.map(String.init)
|
||||
return plainTextSplit.count == 1 ? "" : plainTextSplit[1]
|
||||
}
|
||||
|
||||
private func getAdditionValue(withKey key: String, caseSensitive: Bool = true) -> String? {
|
||||
let searchKey = caseSensitive ? key : key.lowercased()
|
||||
let matchingField = additions.first { (caseSensitive ? $0.title : $0.title.lowercased()) == searchKey }
|
||||
return matchingField?.content
|
||||
|
||||
private func getAdditionValue(withKey key: String, caseSensitive: Bool = false) -> String? {
|
||||
let toLowercase = { (string: String) -> String in return caseSensitive ? string : string.lowercased() }
|
||||
return additions.first(where: { toLowercase($0.title) == toLowercase(key) })?.content
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -239,9 +144,9 @@ public class Password {
|
|||
self.otpToken = nil
|
||||
|
||||
// get otpauth, if we are able to generate a token, return
|
||||
if var otpauthString = getAdditionValue(withKey: Password.OTPAUTH) {
|
||||
if !otpauthString.hasPrefix("\(Password.OTPAUTH):") {
|
||||
otpauthString = "\(Password.OTPAUTH):\(otpauthString)"
|
||||
if var otpauthString = getAdditionValue(withKey: Constants.OTPAUTH, caseSensitive: true) {
|
||||
if !otpauthString.hasPrefix("\(Constants.OTPAUTH):") {
|
||||
otpauthString = "\(Constants.OTPAUTH):\(otpauthString)"
|
||||
}
|
||||
if let otpauthUrl = URL(string: otpauthString),
|
||||
let token = Token(url: otpauthUrl) {
|
||||
|
|
@ -358,10 +263,10 @@ public class Password {
|
|||
|
||||
var lines : [String] = []
|
||||
self.plainText.enumerateLines() { line, _ in
|
||||
let (key, _) = Password.getKeyValuePair(from: line)
|
||||
if !Password.OTP_KEYWORDS.contains(key ?? "") {
|
||||
let (key, _) = Parser.getKeyValuePair(from: line)
|
||||
if !Constants.OTP_KEYWORDS.contains(key ?? "") {
|
||||
lines.append(line)
|
||||
} else if key == Password.OTPAUTH && newOtpauth != nil {
|
||||
} else if key == Constants.OTPAUTH && newOtpauth != nil {
|
||||
lines.append(newOtpauth!)
|
||||
// set to nil to prevent duplication
|
||||
newOtpauth = nil
|
||||
|
|
@ -376,11 +281,6 @@ public class Password {
|
|||
return self.otpToken?.currentPassword
|
||||
}
|
||||
|
||||
public static func LooksLikeOTP(line: String) -> Bool {
|
||||
let (key, _) = getKeyValuePair(from: line)
|
||||
return Password.OTP_KEYWORDS.contains(key ?? "")
|
||||
}
|
||||
|
||||
public static func generatePassword(length: Int) -> String{
|
||||
switch SharedDefaults[.passwordGeneratorFlavor] {
|
||||
case "Random":
|
||||
|
|
|
|||
47
passKit/Parser/AdditionField.swift
Normal file
47
passKit/Parser/AdditionField.swift
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// AdditionField.swift
|
||||
// passKit
|
||||
//
|
||||
// Created by Danny Moesch on 30.09.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
public struct AdditionField: Hashable {
|
||||
|
||||
public let title: String, content: String
|
||||
|
||||
var asString: String {
|
||||
return title.isEmpty ? content : title + ": " + content
|
||||
}
|
||||
|
||||
var asTuple: (String, String) {
|
||||
return (title, content)
|
||||
}
|
||||
}
|
||||
|
||||
extension AdditionField {
|
||||
|
||||
static func | (left: String, right: AdditionField) -> String {
|
||||
return left | right.asString
|
||||
}
|
||||
|
||||
static func | (left: AdditionField, right: String) -> String {
|
||||
return left.asString | right
|
||||
}
|
||||
|
||||
static func | (left: AdditionField, right: AdditionField) -> String {
|
||||
return left.asString | right
|
||||
}
|
||||
}
|
||||
|
||||
extension AdditionField: Equatable {
|
||||
|
||||
public static func == (first: AdditionField, second: AdditionField) -> Bool {
|
||||
return first.asTuple == second.asTuple
|
||||
}
|
||||
}
|
||||
|
||||
infix operator =>: MultiplicationPrecedence
|
||||
func => (key: String, value: String) -> AdditionField {
|
||||
return AdditionField(title: key, content: value)
|
||||
}
|
||||
47
passKit/Parser/Constants.swift
Normal file
47
passKit/Parser/Constants.swift
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// Constants.swift
|
||||
// passKit
|
||||
//
|
||||
// Created by Danny Moesch on 16.08.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
public struct Constants {
|
||||
|
||||
public static let OTP_KEYWORDS = [
|
||||
"otp_secret",
|
||||
"otp_type",
|
||||
"otp_algorithm",
|
||||
"otp_period",
|
||||
"otp_digits",
|
||||
"otp_counter",
|
||||
"otpauth",
|
||||
]
|
||||
|
||||
static let BLANK = " "
|
||||
static let MULTILINE_WITH_LINE_BREAK_INDICATOR = "|"
|
||||
static let MULTILINE_WITH_LINE_BREAK_SEPARATOR = "\n"
|
||||
static let MULTILINE_WITHOUT_LINE_BREAK_INDICATOR = ">"
|
||||
static let MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR = BLANK
|
||||
|
||||
static let OTPAUTH = "otpauth"
|
||||
static let OTPAUTH_URL_START = "\(OTPAUTH)://"
|
||||
static let PASSWORD_KEYWORD = "password"
|
||||
static let USERNAME_KEYWORD = "username"
|
||||
static let LOGIN_KEYWORD = "login"
|
||||
static let URL_KEYWORD = "url"
|
||||
static let UNKNOWN = "unknown"
|
||||
|
||||
public static func isOtpRelated(line: String) -> Bool {
|
||||
let (key, _) = Parser.getKeyValuePair(from: line)
|
||||
return OTP_KEYWORDS.contains(key ?? "")
|
||||
}
|
||||
|
||||
static func unknown(_ number: UInt) -> String {
|
||||
return "\(UNKNOWN) \(number)"
|
||||
}
|
||||
|
||||
static func getSeparator(breakingLines: Bool) -> String {
|
||||
return breakingLines ? MULTILINE_WITH_LINE_BREAK_SEPARATOR : MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR
|
||||
}
|
||||
}
|
||||
93
passKit/Parser/Parser.swift
Normal file
93
passKit/Parser/Parser.swift
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// Parser.swift
|
||||
// passKit
|
||||
//
|
||||
// Created by Danny Moesch on 16.08.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
class Parser {
|
||||
|
||||
let firstLine: String
|
||||
let additionsSection: String
|
||||
let purgedAdditionalLines: [String]
|
||||
|
||||
// The parsing process is expensive. This field makes sure it is only done once if actually needed.
|
||||
private(set) lazy var additionFields = getAdditionFields()
|
||||
|
||||
init(plainText: String) {
|
||||
let splittedPlainText = plainText
|
||||
.split(omittingEmptySubsequences: false) { $0 == "\n" || $0 == "\r\n" }
|
||||
.map(String.init)
|
||||
|
||||
firstLine = splittedPlainText.first!
|
||||
additionsSection = splittedPlainText[1...].joined(separator: "\n")
|
||||
purgedAdditionalLines = splittedPlainText[1...].filter { !$0.isEmpty }
|
||||
}
|
||||
|
||||
private func getAdditionFields() -> [AdditionField] {
|
||||
var additions: [AdditionField] = []
|
||||
var unknownIndex: UInt = 0
|
||||
var i = purgedAdditionalLines.startIndex
|
||||
while i < purgedAdditionalLines.count {
|
||||
let line = purgedAdditionalLines[i]
|
||||
i += 1
|
||||
var (key, value) = Parser.getKeyValuePair(from: line)
|
||||
if key == nil {
|
||||
unknownIndex += 1
|
||||
key = Constants.unknown(unknownIndex)
|
||||
} else if value == Constants.MULTILINE_WITH_LINE_BREAK_INDICATOR {
|
||||
value = gatherMultilineValue(startingAt: &i, removingLineBreaks: false)
|
||||
} else if value == Constants.MULTILINE_WITHOUT_LINE_BREAK_INDICATOR {
|
||||
value = gatherMultilineValue(startingAt: &i, removingLineBreaks: true)
|
||||
}
|
||||
additions.append(key! => value)
|
||||
}
|
||||
return additions
|
||||
}
|
||||
|
||||
private func gatherMultilineValue(startingAt i: inout Int, removingLineBreaks: Bool) -> String {
|
||||
var result = ""
|
||||
guard i < purgedAdditionalLines.count else {
|
||||
return result
|
||||
}
|
||||
let numberInitialBlanks = purgedAdditionalLines[i].enumerated().first {
|
||||
$1 != Character(Constants.BLANK)
|
||||
}?.0 ?? purgedAdditionalLines[i].count
|
||||
guard numberInitialBlanks != 0 else {
|
||||
return result
|
||||
}
|
||||
let initialBlanks = String(repeating: Constants.BLANK, count: numberInitialBlanks)
|
||||
|
||||
while i < purgedAdditionalLines.count && purgedAdditionalLines[i].starts(with: initialBlanks) {
|
||||
result.append(String(purgedAdditionalLines[i].dropFirst(numberInitialBlanks)))
|
||||
result.append(Constants.getSeparator(breakingLines: !removingLineBreaks))
|
||||
i += 1
|
||||
}
|
||||
return result.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
/// Split line from password file in to a key-value pair separted by `: `.
|
||||
///
|
||||
/// - Parameter line: Line from a password file
|
||||
/// - Returns: Pair of two `String`s of which the first one can be 'nil'
|
||||
static func getKeyValuePair(from line: String) -> (key: String?, value: String) {
|
||||
let items = line.components(separatedBy: ": ").map { String($0).trimmingCharacters(in: .whitespaces) }
|
||||
var key: String?
|
||||
var value = ""
|
||||
if items.count == 1 || (items[0].isEmpty && items[1].isEmpty) {
|
||||
// No ': ' found, or empty on both sides of ': '.
|
||||
value = line
|
||||
// "otpauth" special case
|
||||
if value.hasPrefix(Constants.OTPAUTH_URL_START) {
|
||||
key = Constants.OTPAUTH
|
||||
}
|
||||
} else {
|
||||
if !items[0].isEmpty {
|
||||
key = items[0]
|
||||
}
|
||||
value = items[1]
|
||||
}
|
||||
return (key, value)
|
||||
}
|
||||
}
|
||||
29
passKitTests/Helpers/PasswordHelpersTest.swift
Normal file
29
passKitTests/Helpers/PasswordHelpersTest.swift
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// PasswordHelpersTest.swift
|
||||
// passKitTests
|
||||
//
|
||||
// Created by Danny Moesch on 30.09.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
import OneTimePassword
|
||||
import XCTest
|
||||
|
||||
@testable import passKit
|
||||
|
||||
class PasswordHelpersTest: XCTestCase {
|
||||
|
||||
func testOtpType() {
|
||||
let secret = "secret".data(using: .utf8)!
|
||||
|
||||
let totpGenerator = Generator(factor: .timer(period: 30.0), secret: secret, algorithm: .sha1, digits: 6)!
|
||||
let totpToken = Token(name: "", issuer: "", generator: totpGenerator)
|
||||
XCTAssertEqual(OtpType(token: totpToken), .totp)
|
||||
|
||||
let hotpGenerator = Generator(factor: .counter(4), secret: secret, algorithm: .sha1, digits: 6)!
|
||||
let hotpToken = Token(name: "", issuer: "", generator: hotpGenerator)
|
||||
XCTAssertEqual(OtpType(token: hotpToken), .hotp)
|
||||
|
||||
XCTAssertEqual(OtpType(token: nil), .none)
|
||||
}
|
||||
}
|
||||
35
passKitTests/Helpers/StringExtensionTest.swift
Normal file
35
passKitTests/Helpers/StringExtensionTest.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// StringExtensionTest.swift
|
||||
// passKitTests
|
||||
//
|
||||
// Created by Danny Moesch on 30.09.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import passKit
|
||||
|
||||
class StringExtensionTest: XCTestCase {
|
||||
|
||||
func testStringByAddingPercentEncodingForRFC3986() {
|
||||
[
|
||||
("!#$&'()*+,/:;=?@[]^", "%21%23%24%26%27%28%29%2A%2B%2C/%3A%3B%3D?%40%5B%5D%5E"),
|
||||
("-._~/?", "-._~/?"),
|
||||
("A*b!c", "A%2Ab%21c"),
|
||||
].forEach { unencoded, encoded in
|
||||
XCTAssertEqual(unencoded.stringByAddingPercentEncodingForRFC3986(), encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func testConcatenateAsLines() {
|
||||
[
|
||||
("a" | "b", "a\nb"),
|
||||
("" | "b", "\nb"),
|
||||
("a" | "", "a"),
|
||||
("" | "", ""),
|
||||
].forEach { concatenated, result in
|
||||
XCTAssertEqual(concatenated, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
276
passKitTests/Models/PasswordTest.swift
Normal file
276
passKitTests/Models/PasswordTest.swift
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
//
|
||||
// PasswordTests.swift
|
||||
// passKitTests
|
||||
//
|
||||
// Created by Danny Moesch on 02.05.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import passKit
|
||||
|
||||
class PasswordTest: XCTestCase {
|
||||
|
||||
private let PASSWORD_PATH = "/path/to/password"
|
||||
private let PASSWORD_URL = URL(fileURLWithPath: "/path/to/password")
|
||||
private let PASSWORD_STRING = "abcd1234"
|
||||
private let OTP_TOKEN = "otpauth://totp/email@email.com?secret=abcd1234"
|
||||
|
||||
private let SECURE_URL_FIELD = "url" => "https://secure.com"
|
||||
private let INSECURE_URL_FIELD = "url" => "http://insecure.com"
|
||||
private let LOGIN_FIELD = "login" => "login name"
|
||||
private let USERNAME_FIELD = "username" => "some username"
|
||||
private let NOTE_FIELD = "note" => "A NOTE"
|
||||
private let HINT_FIELD = "some hints" => "äöüß // €³ %% −° && @²` | [{\\}],.<>"
|
||||
|
||||
func testUrl() {
|
||||
let password = getPasswordObjectWith(content: "")
|
||||
XCTAssertEqual(password.url, PASSWORD_URL)
|
||||
XCTAssertEqual(password.namePath, PASSWORD_PATH)
|
||||
}
|
||||
|
||||
func testEmptyFile() {
|
||||
[
|
||||
"",
|
||||
"\n",
|
||||
].forEach { fileContent in
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, "")
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
|
||||
XCTAssertEqual(password.additionsPlainText, "")
|
||||
XCTAssertTrue(password.getFilteredAdditions().isEmpty)
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertNil(password.urlString)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
}
|
||||
|
||||
func testEmptyPassword() {
|
||||
let fileContent = "\n\(LOGIN_FIELD.asString)"
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, "")
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, LOGIN_FIELD.asString)
|
||||
|
||||
XCTAssertFalse(does(password, contain: LOGIN_FIELD))
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertNil(password.urlString)
|
||||
XCTAssertEqual(password.login, LOGIN_FIELD.content)
|
||||
}
|
||||
|
||||
func testSimplePasswordFile() {
|
||||
let additions = SECURE_URL_FIELD | LOGIN_FIELD | USERNAME_FIELD | NOTE_FIELD
|
||||
let fileContent = PASSWORD_STRING | additions
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, PASSWORD_STRING)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, additions)
|
||||
|
||||
XCTAssertTrue(does(password, contain: SECURE_URL_FIELD))
|
||||
XCTAssertFalse(does(password, contain: LOGIN_FIELD))
|
||||
XCTAssertFalse(does(password, contain: USERNAME_FIELD))
|
||||
XCTAssertTrue(does(password, contain: NOTE_FIELD))
|
||||
|
||||
XCTAssertEqual(password.urlString, SECURE_URL_FIELD.content)
|
||||
XCTAssertEqual(password.login, LOGIN_FIELD.content)
|
||||
XCTAssertEqual(password.username, USERNAME_FIELD.content)
|
||||
}
|
||||
|
||||
func testTwoPasswords() {
|
||||
let additions = "efgh5678" | INSECURE_URL_FIELD
|
||||
let fileContent = PASSWORD_STRING | additions
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, PASSWORD_STRING)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, additions)
|
||||
|
||||
XCTAssertTrue(does(password, contain: INSECURE_URL_FIELD))
|
||||
XCTAssertTrue(does(password, contain: Constants.unknown(1) => "efgh5678"))
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertEqual(password.urlString, INSECURE_URL_FIELD.content)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
|
||||
func testNoPassword() {
|
||||
let fileContent = SECURE_URL_FIELD | NOTE_FIELD
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, SECURE_URL_FIELD.asString)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, NOTE_FIELD.asString)
|
||||
|
||||
XCTAssertTrue(does(password, contain: NOTE_FIELD))
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertNil(password.urlString)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
|
||||
func testDuplicateKeys() {
|
||||
let additions = SECURE_URL_FIELD | INSECURE_URL_FIELD
|
||||
let fileContent = PASSWORD_STRING | additions
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, PASSWORD_STRING)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, additions)
|
||||
|
||||
XCTAssertTrue(does(password, contain: SECURE_URL_FIELD))
|
||||
XCTAssertTrue(does(password, contain: INSECURE_URL_FIELD))
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertEqual(password.urlString, SECURE_URL_FIELD.content)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
|
||||
func testUnknownKeys() {
|
||||
let value1 = "value 1"
|
||||
let value2 = "value 2"
|
||||
let value3 = "value 3"
|
||||
let value4 = "value 4"
|
||||
let additions = value1 | NOTE_FIELD | value2 | value3 | SECURE_URL_FIELD | value4
|
||||
let fileContent = PASSWORD_STRING | additions
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, PASSWORD_STRING)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, additions)
|
||||
|
||||
XCTAssertTrue(does(password, contain: Constants.unknown(1) => value1))
|
||||
XCTAssertTrue(does(password, contain: NOTE_FIELD))
|
||||
XCTAssertTrue(does(password, contain: Constants.unknown(2) => value2))
|
||||
XCTAssertTrue(does(password, contain: Constants.unknown(3) => value3))
|
||||
XCTAssertTrue(does(password, contain: SECURE_URL_FIELD))
|
||||
XCTAssertTrue(does(password, contain: Constants.unknown(4) => value4))
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertEqual(password.urlString, SECURE_URL_FIELD.content)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
|
||||
func testPasswordFileWithOtpToken() {
|
||||
let additions = NOTE_FIELD | OTP_TOKEN
|
||||
let fileContent = PASSWORD_STRING | additions
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, PASSWORD_STRING)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, additions)
|
||||
|
||||
XCTAssertEqual(password.otpType, OtpType.totp)
|
||||
XCTAssertNotNil(password.getOtp())
|
||||
}
|
||||
|
||||
func testFirstLineIsOtpToken() {
|
||||
let password = getPasswordObjectWith(content: OTP_TOKEN)
|
||||
|
||||
XCTAssertEqual(password.password, OTP_TOKEN)
|
||||
XCTAssertEqual(password.plainData, OTP_TOKEN.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, "")
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertNil(password.urlString)
|
||||
XCTAssertNil(password.login)
|
||||
|
||||
XCTAssertEqual(password.otpType, OtpType.totp)
|
||||
XCTAssertNotNil(password.getOtp())
|
||||
}
|
||||
|
||||
func testWrongOtpToken() {
|
||||
let fileContent = "otpauth://htop/blabla"
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, fileContent)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertTrue(password.additionsPlainText.isEmpty)
|
||||
|
||||
XCTAssertEqual(password.otpType, OtpType.none)
|
||||
XCTAssertNil(password.getOtp())
|
||||
}
|
||||
|
||||
func testEmptyMultilineValues() {
|
||||
let lineBreakField1 = "with line breaks" => "| \n"
|
||||
let lineBreakField2 = "with line breaks" => "| \n "
|
||||
let noLineBreakField = "without line breaks" => " > "
|
||||
let additions = lineBreakField1 | lineBreakField2 | NOTE_FIELD | noLineBreakField
|
||||
let fileContent = PASSWORD_STRING | additions
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, PASSWORD_STRING)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, additions)
|
||||
|
||||
XCTAssertTrue(does(password, contain: lineBreakField1.title => ""))
|
||||
XCTAssertTrue(does(password, contain: lineBreakField2.title => ""))
|
||||
XCTAssertTrue(does(password, contain: NOTE_FIELD))
|
||||
XCTAssertTrue(does(password, contain: noLineBreakField.title => ""))
|
||||
}
|
||||
|
||||
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!"
|
||||
let additions = lineBreakField | NOTE_FIELD | noLineBreakField
|
||||
let fileContent = PASSWORD_STRING | additions
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, PASSWORD_STRING)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, additions)
|
||||
|
||||
XCTAssertTrue(does(password, contain: lineBreakField.title => "This is \n text spread over \nmultiple lines!"))
|
||||
XCTAssertTrue(does(password, contain: NOTE_FIELD))
|
||||
XCTAssertTrue(does(password, contain: noLineBreakField.title => "This is text spread over multiple lines!"))
|
||||
}
|
||||
|
||||
func testMultilineValuesMixed() {
|
||||
let lineBreakField = "with line breaks" => "|\n This is \n \(HINT_FIELD.asString) spread over\n multiple lines!"
|
||||
let noLineBreakField = "without line breaks" => " > \n This is \n | \n text spread over\nmultiple lines!"
|
||||
let additions = lineBreakField | noLineBreakField | NOTE_FIELD
|
||||
let fileContent = PASSWORD_STRING | additions
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, PASSWORD_STRING)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.additionsPlainText, additions)
|
||||
|
||||
XCTAssertTrue(does(password, contain: lineBreakField.title => "This is \n\(HINT_FIELD.asString) spread over"))
|
||||
XCTAssertTrue(does(password, contain: Constants.unknown(1) => " multiple lines!"))
|
||||
XCTAssertTrue(does(password, contain: noLineBreakField.title => "This is | text spread over"))
|
||||
XCTAssertTrue(does(password, contain: Constants.unknown(2) => "multiple lines!"))
|
||||
XCTAssertTrue(does(password, contain: NOTE_FIELD))
|
||||
}
|
||||
|
||||
func testUpdatePassword() {
|
||||
let password = getPasswordObjectWith(content: "")
|
||||
XCTAssertEqual(password.changed, 0)
|
||||
|
||||
password.updatePassword(name: "password", url: PASSWORD_URL, plainText: "")
|
||||
XCTAssertEqual(password.changed, 0)
|
||||
|
||||
password.updatePassword(name: "", url: PASSWORD_URL, plainText: "a")
|
||||
XCTAssertEqual(password.changed, 2)
|
||||
|
||||
password.updatePassword(name: "", url: URL(fileURLWithPath: "/some/path/"), plainText: "a")
|
||||
XCTAssertEqual(password.changed, 3)
|
||||
|
||||
password.updatePassword(name: "", url: PASSWORD_URL, plainText: "")
|
||||
XCTAssertEqual(password.changed, 3)
|
||||
}
|
||||
|
||||
private func getPasswordObjectWith(content: String, url: URL? = nil) -> Password {
|
||||
return Password(name: "password", url: url ?? PASSWORD_URL, plainText: content)
|
||||
}
|
||||
|
||||
private func does(_ password: Password, contain field: AdditionField) -> Bool {
|
||||
return password.getFilteredAdditions().contains(field)
|
||||
}
|
||||
}
|
||||
52
passKitTests/Parser/AdditionFieldTest.swift
Normal file
52
passKitTests/Parser/AdditionFieldTest.swift
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// AdditionFieldTest.swift
|
||||
// passKitTests
|
||||
//
|
||||
// Created by Danny Moesch on 30.09.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import passKit
|
||||
|
||||
class AdditionFieldTest: XCTestCase {
|
||||
|
||||
func testAdditionField() {
|
||||
let field1 = "key" => "value"
|
||||
let field2 = "some other key" => "some other value"
|
||||
let field3 = "" => "no title"
|
||||
|
||||
XCTAssertEqual(field1.asString, "key: value")
|
||||
XCTAssertEqual(field2.asString, "some other key: some other value")
|
||||
XCTAssertEqual(field3.asString, "no title")
|
||||
|
||||
XCTAssertTrue(field1.asTuple == ("key", "value"))
|
||||
XCTAssertTrue(field2.asTuple == ("some other key", "some other value"))
|
||||
XCTAssertTrue(field3.asTuple == ("", "no title"))
|
||||
}
|
||||
|
||||
func testAdditionFieldEquals() {
|
||||
XCTAssertEqual("key" => "value", "key" => "value")
|
||||
XCTAssertNotEqual("key" => "value", "key" => "some other value")
|
||||
}
|
||||
|
||||
func testInfixAdditionFieldInitialization() {
|
||||
XCTAssertEqual("key" => "value", AdditionField(title: "key", content: "value"))
|
||||
}
|
||||
|
||||
func testAdditionFieldOperators() {
|
||||
let field1 = "key" => "value"
|
||||
let field2 = "some other key" => "some other value"
|
||||
let field3 = "" => "no title"
|
||||
|
||||
XCTAssertEqual("start" | field1, "start\nkey: value")
|
||||
XCTAssertEqual("" | field1, "\nkey: value")
|
||||
XCTAssertEqual(field1 | "end", "key: value\nend")
|
||||
XCTAssertEqual(field1 | "", "key: value")
|
||||
XCTAssertEqual("start" | field1 | field2, "start\nkey: value\nsome other key: some other value")
|
||||
XCTAssertEqual(field1 | field2 | "end", "key: value\nsome other key: some other value\nend")
|
||||
XCTAssertEqual(field1 | field2 | field3, "key: value\nsome other key: some other value\nno title")
|
||||
XCTAssertEqual("check" => "for right" | "operator" => "precedence", "check: for right\noperator: precedence")
|
||||
}
|
||||
}
|
||||
31
passKitTests/Parser/ConstantsTest.swift
Normal file
31
passKitTests/Parser/ConstantsTest.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// ConstantsTest.swift
|
||||
// passKitTests
|
||||
//
|
||||
// Created by Danny Moesch on 30.09.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import passKit
|
||||
|
||||
class ConstantsTest: XCTestCase {
|
||||
|
||||
func testIsOtpRelated() {
|
||||
XCTAssertTrue(Constants.isOtpRelated(line: "otpauth://something"))
|
||||
XCTAssertTrue(Constants.isOtpRelated(line: "otp_algorithm: algorithm"))
|
||||
XCTAssertFalse(Constants.isOtpRelated(line: "otp: something"))
|
||||
XCTAssertFalse(Constants.isOtpRelated(line: "otp"))
|
||||
}
|
||||
|
||||
func testUnknown() {
|
||||
XCTAssertEqual(Constants.unknown(0), "unknown 0")
|
||||
XCTAssertEqual(Constants.unknown(10), "unknown 10")
|
||||
}
|
||||
|
||||
func testGetSeparator() {
|
||||
XCTAssertEqual(Constants.getSeparator(breakingLines: true), "\n")
|
||||
XCTAssertEqual(Constants.getSeparator(breakingLines: false), " ")
|
||||
}
|
||||
}
|
||||
102
passKitTests/Parser/ParserTest.swift
Normal file
102
passKitTests/Parser/ParserTest.swift
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
//
|
||||
// ParserTest.swift
|
||||
// passKitTests
|
||||
//
|
||||
// Created by Danny Moesch on 18.08.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import passKit
|
||||
import XCTest
|
||||
|
||||
class ParserTest: XCTestCase {
|
||||
|
||||
private let FIELD = "key" => "value"
|
||||
private let LOGIN_FIELD = "login" => "login name"
|
||||
private let SECURE_URL_FIELD = "url" => "https://secure.com"
|
||||
private let INSECURE_URL_FIELD = "url" => "http://insecure.com"
|
||||
private let USERNAME_FIELD = "username" => "微 分 方 程"
|
||||
private let NOTE_FIELD = "note" => "A NOTE"
|
||||
private let MULTILINE_BLOCK_START = "multiline block" => "|"
|
||||
private let MULTILINE_LINE_START = "multiline line" => ">"
|
||||
|
||||
func testInit() {
|
||||
[
|
||||
("", "", "", []),
|
||||
("a", "a", "", []),
|
||||
("a\nb", "a", "b", ["b"]),
|
||||
("a\n\nb", "a", "\nb", ["b"]),
|
||||
("a\r\nb", "a", "b", ["b"]),
|
||||
("a\nb\nc\n\nd", "a", "b\nc\n\nd", ["b", "c", "d"]),
|
||||
].forEach { plainText, firstLine, additionsSection, purgedAdditionalLines in
|
||||
let parser = Parser(plainText: plainText)
|
||||
XCTAssertEqual(parser.firstLine, firstLine)
|
||||
XCTAssertEqual(parser.additionsSection, additionsSection)
|
||||
XCTAssertEqual(parser.purgedAdditionalLines, purgedAdditionalLines)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetKeyValuePair() {
|
||||
XCTAssertTrue(Parser.getKeyValuePair(from: "key: value") == ("key", "value"))
|
||||
XCTAssertTrue(Parser.getKeyValuePair(from: "a key: a value") == ("a key", "a value"))
|
||||
XCTAssertTrue(Parser.getKeyValuePair(from: "key:value") == (nil, "key:value"))
|
||||
XCTAssertTrue(Parser.getKeyValuePair(from: ": value") == (nil, "value"))
|
||||
XCTAssertTrue(Parser.getKeyValuePair(from: "key: ") == ("key", ""))
|
||||
XCTAssertTrue(Parser.getKeyValuePair(from: "otpauth://value") == ("otpauth", "otpauth://value"))
|
||||
}
|
||||
|
||||
func testEmptyFiles() {
|
||||
XCTAssertEqual(Parser(plainText: "").additionFields, [])
|
||||
XCTAssertEqual(Parser(plainText: "\n").additionFields, [])
|
||||
}
|
||||
|
||||
func testSimpleKeyValueLines() {
|
||||
let fields0 = Parser(plainText: "" | FIELD | LOGIN_FIELD | SECURE_URL_FIELD).additionFields
|
||||
let fields1 = Parser(plainText: "" | FIELD | "" | SECURE_URL_FIELD).additionFields
|
||||
|
||||
XCTAssertEqual(fields0, [FIELD, LOGIN_FIELD, SECURE_URL_FIELD])
|
||||
XCTAssertEqual(fields1, [FIELD, SECURE_URL_FIELD])
|
||||
}
|
||||
|
||||
func testLinesWithoutKey() {
|
||||
let fields0 = Parser(plainText: "" | "value").additionFields
|
||||
let fields1 = Parser(plainText: "" | LOGIN_FIELD | "value only" | INSECURE_URL_FIELD).additionFields
|
||||
let fields2 = Parser(plainText: "" | LOGIN_FIELD | USERNAME_FIELD | "value:only").additionFields
|
||||
let fields3 = Parser(plainText: "" | LOGIN_FIELD | "value 1" | "value 2").additionFields
|
||||
|
||||
XCTAssertEqual(fields0, [Constants.unknown(1) => "value"])
|
||||
XCTAssertEqual(fields1, [LOGIN_FIELD, Constants.unknown(1) => "value only", INSECURE_URL_FIELD])
|
||||
XCTAssertEqual(fields2, [LOGIN_FIELD, USERNAME_FIELD, Constants.unknown(1) => "value:only"])
|
||||
XCTAssertEqual(fields3, [LOGIN_FIELD, Constants.unknown(1) => "value 1", Constants.unknown(2) => "value 2"])
|
||||
}
|
||||
|
||||
func testMultilineValues() {
|
||||
[
|
||||
// Normal with one leading space
|
||||
(" a b" | " cd", "a b\ncd", []),
|
||||
// Normal with two leading spaces
|
||||
(" a b" | " cd", "a b\ncd", []),
|
||||
// Changing leading space lenght
|
||||
(" a b" | " cd", "a b\n cd", []),
|
||||
// First leading space longer than others
|
||||
(" a b" | " cd", "a b", [Constants.unknown(1) => " cd"]),
|
||||
// Empty first line
|
||||
(" " | " cd", "cd", []),
|
||||
// No leading space
|
||||
("a b" | "cd", "", [Constants.unknown(1) => "a b", Constants.unknown(2) => "cd"]),
|
||||
// Characters with special meaning in value
|
||||
(" a: b" | " c: |" | " d", "a: b\nc: |\nd", []),
|
||||
// Empty value at end
|
||||
("", "", []),
|
||||
// Empty value in between
|
||||
("" | NOTE_FIELD, "", [NOTE_FIELD]),
|
||||
].forEach { wrappedMultilineValue, content, additionalFields in
|
||||
let blockField = Parser(plainText: "" | MULTILINE_BLOCK_START | wrappedMultilineValue).additionFields
|
||||
XCTAssertEqual(blockField, [MULTILINE_BLOCK_START.title => content] + additionalFields)
|
||||
|
||||
let lineField = Parser(plainText: "" | MULTILINE_LINE_START | wrappedMultilineValue).additionFields
|
||||
let contentWithoutLineBreaks = content.replacingOccurrences(of: "\n", with: " ")
|
||||
XCTAssertEqual(lineField, [MULTILINE_LINE_START.title => contentWithoutLineBreaks] + additionalFields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,339 +0,0 @@
|
|||
//
|
||||
// PasswordTests.swift
|
||||
// passKitTests
|
||||
//
|
||||
// Created by Danny Mösch on 02.05.18.
|
||||
// Copyright © 2018 Bob Sun. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import passKit
|
||||
|
||||
class PasswordTest: XCTestCase {
|
||||
static let EMPTY_STRING = ""
|
||||
static let PASSWORD_NAME = "password"
|
||||
static let PASSWORD_PATH = "/path/to/\(PASSWORD_NAME)"
|
||||
static let PASSWORD_URL = URL(fileURLWithPath: PASSWORD_PATH)
|
||||
static let PASSWORD_STRING = "abcd1234"
|
||||
static let OTP_TOKEN = "otpauth://totp/email@email.com?secret=abcd1234"
|
||||
|
||||
static let SECURE_URL_FIELD = AdditionField(title: "url", content: "https://secure.com")
|
||||
static let INSECURE_URL_FIELD = AdditionField(title: "url", content: "http://insecure.com")
|
||||
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)
|
||||
XCTAssertEqual(password1.namePath, PasswordTest.PASSWORD_PATH)
|
||||
}
|
||||
|
||||
func testLooksLikeOTP() {
|
||||
XCTAssertTrue(Password.LooksLikeOTP(line: PasswordTest.OTP_TOKEN))
|
||||
XCTAssertFalse(Password.LooksLikeOTP(line: "no_auth://totp/blabla"))
|
||||
}
|
||||
|
||||
func testEmptyFile() {
|
||||
let fileContent = PasswordTest.EMPTY_STRING
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, PasswordTest.EMPTY_STRING)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
|
||||
XCTAssertEqual(password.getAdditionsPlainText(), PasswordTest.EMPTY_STRING)
|
||||
XCTAssertTrue(password.getFilteredAdditions().isEmpty)
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertNil(password.urlString)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
|
||||
func testOneEmptyLine() {
|
||||
let fileContent = """
|
||||
|
||||
"""
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, PasswordTest.EMPTY_STRING)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
|
||||
XCTAssertEqual(password.getAdditionsPlainText(), PasswordTest.EMPTY_STRING)
|
||||
XCTAssertTrue(password.getFilteredAdditions().isEmpty)
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertNil(password.urlString)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
|
||||
func testSimplePasswordFile() {
|
||||
let passwordString = PasswordTest.PASSWORD_STRING
|
||||
let urlField = PasswordTest.SECURE_URL_FIELD
|
||||
let loginField = PasswordTest.LOGIN_FIELD
|
||||
let usernameField = PasswordTest.USERNAME_FIELD
|
||||
let noteField = PasswordTest.NOTE_FIELD
|
||||
let fileContent = """
|
||||
\(passwordString)
|
||||
\(urlField.asString)
|
||||
\(loginField.asString)
|
||||
\(usernameField.asString)
|
||||
\(noteField.asString)
|
||||
"""
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, passwordString)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
|
||||
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(urlField, loginField, usernameField, 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)
|
||||
XCTAssertEqual(password.username, usernameField.content)
|
||||
}
|
||||
|
||||
func testTwoPasswords() {
|
||||
let firstPasswordString = PasswordTest.PASSWORD_STRING
|
||||
let secondPasswordString = "efgh5678"
|
||||
let urlField = PasswordTest.INSECURE_URL_FIELD
|
||||
let fileContent = """
|
||||
\(firstPasswordString)
|
||||
\(secondPasswordString)
|
||||
\(urlField.asString)
|
||||
"""
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, firstPasswordString)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(secondPasswordString, urlField.asString))
|
||||
|
||||
XCTAssertTrue(does(password, contain: urlField))
|
||||
XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 1", content: secondPasswordString)))
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertEqual(password.urlString, urlField.content)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
|
||||
func testNoPassword() {
|
||||
let urlField = PasswordTest.SECURE_URL_FIELD
|
||||
let noteField = PasswordTest.NOTE_FIELD
|
||||
let fileContent = """
|
||||
\(urlField.asString)
|
||||
\(noteField.asString)
|
||||
"""
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, urlField.asString)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
|
||||
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(noteField))
|
||||
XCTAssertTrue(does(password, contain: noteField))
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertNil(password.urlString)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
|
||||
func testDuplicateKeys() {
|
||||
let passwordString = PasswordTest.PASSWORD_STRING
|
||||
let urlField1 = PasswordTest.SECURE_URL_FIELD
|
||||
let urlField2 = PasswordTest.INSECURE_URL_FIELD
|
||||
let fileContent = """
|
||||
\(passwordString)
|
||||
\(urlField1.asString)
|
||||
\(urlField2.asString)
|
||||
"""
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, passwordString)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
|
||||
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(urlField1, urlField2))
|
||||
XCTAssertTrue(does(password, contain: urlField1))
|
||||
XCTAssertTrue(does(password, contain: urlField2))
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertEqual(password.urlString, urlField1.content)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
|
||||
func testUnknownKeys() {
|
||||
let passwordString = PasswordTest.PASSWORD_STRING
|
||||
let value1 = "value 1"
|
||||
let value2 = "value 2"
|
||||
let value3 = "value 3"
|
||||
let value4 = "value 4"
|
||||
let noteField = PasswordTest.NOTE_FIELD
|
||||
let urlField = PasswordTest.SECURE_URL_FIELD
|
||||
let fileContent = """
|
||||
\(passwordString)
|
||||
\(value1)
|
||||
\(noteField.asString)
|
||||
\(value2)
|
||||
\(value3)
|
||||
\(urlField.asString)
|
||||
\(value4)
|
||||
"""
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, passwordString)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
|
||||
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(value1, noteField.asString, value2, value3, urlField.asString, 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)
|
||||
XCTAssertNil(password.login)
|
||||
}
|
||||
|
||||
func testPasswordFileWithOtpToken() {
|
||||
let passwordString = PasswordTest.PASSWORD_STRING
|
||||
let noteField = PasswordTest.NOTE_FIELD
|
||||
let otpToken = PasswordTest.OTP_TOKEN
|
||||
let fileContent = """
|
||||
\(passwordString)
|
||||
\(noteField.asString)
|
||||
\(otpToken)
|
||||
"""
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, passwordString)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
|
||||
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(noteField.asString, otpToken))
|
||||
|
||||
XCTAssertEqual(password.otpType, OtpType.totp)
|
||||
XCTAssertNotNil(password.getOtp())
|
||||
}
|
||||
|
||||
func testFirstLineIsOtpToken() {
|
||||
let otpToken = PasswordTest.OTP_TOKEN
|
||||
let fileContent = """
|
||||
\(otpToken)
|
||||
"""
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, otpToken)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
|
||||
XCTAssertEqual(password.getAdditionsPlainText(), PasswordTest.EMPTY_STRING)
|
||||
|
||||
XCTAssertNil(password.username)
|
||||
XCTAssertNil(password.urlString)
|
||||
XCTAssertNil(password.login)
|
||||
|
||||
XCTAssertEqual(password.otpType, OtpType.totp)
|
||||
XCTAssertNotNil(password.getOtp())
|
||||
}
|
||||
|
||||
func testWrongOtpToken() {
|
||||
let otpToken = "otpauth://htop/blabla"
|
||||
let fileContent = """
|
||||
\(otpToken)
|
||||
"""
|
||||
let password = getPasswordObjectWith(content: fileContent)
|
||||
|
||||
XCTAssertEqual(password.password, otpToken)
|
||||
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
|
||||
|
||||
XCTAssertEqual(password.otpType, OtpType.none)
|
||||
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 {
|
||||
return password.getFilteredAdditions().contains(field)
|
||||
}
|
||||
|
||||
private func asPlainText(_ strings: String...) -> String {
|
||||
return strings.joined(separator: "\n")
|
||||
}
|
||||
private func asPlainText(_ fields: AdditionField...) -> String {
|
||||
return fields.map { $0.asString }.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue