From ff014a56994daec2626fb119f117eab17c9b6c74 Mon Sep 17 00:00:00 2001 From: Danny Moesch Date: Fri, 28 Feb 2020 19:04:53 +0100 Subject: [PATCH] Add logic for more customizable password generator --- pass.xcodeproj/project.pbxproj | 36 +++++- passKit/Extensions/Array+Slices.swift | 28 ++++ passKit/Helpers/DefaultsKeys.swift | 4 +- passKit/Helpers/PasswordGeneratorFlavor.swift | 86 ------------- passKit/Passwords/PasswordGenerator.swift | 121 ++++++++++++++++++ .../Passwords/PasswordGeneratorFlavor.swift | 38 ++++++ .../Extensions/Array+SlicesTest.swift | 29 +++++ .../Helpers/PasswordGeneratorFlavorTest.swift | 41 ------ .../PasswordGeneratorFlavorTest.swift | 21 +++ .../Passwords/PasswordGeneratorTest.swift | 79 ++++++++++++ 10 files changed, 352 insertions(+), 131 deletions(-) create mode 100644 passKit/Extensions/Array+Slices.swift delete mode 100644 passKit/Helpers/PasswordGeneratorFlavor.swift create mode 100644 passKit/Passwords/PasswordGenerator.swift create mode 100644 passKit/Passwords/PasswordGeneratorFlavor.swift create mode 100644 passKitTests/Extensions/Array+SlicesTest.swift delete mode 100644 passKitTests/Helpers/PasswordGeneratorFlavorTest.swift create mode 100644 passKitTests/Passwords/PasswordGeneratorFlavorTest.swift create mode 100644 passKitTests/Passwords/PasswordGeneratorTest.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 429414d..ec79d41 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 3032328E22CBD4CD009EBD9C /* CryptographicKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3032328D22CBD4CD009EBD9C /* CryptographicKeys.swift */; }; 30650E7123F82AF8005CCD5E /* SSHKeyFileImportTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30650E7023F82AF8005CCD5E /* SSHKeyFileImportTableViewController.swift */; }; 30650E7323F847FC005CCD5E /* KeyImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30650E7223F847FC005CCD5E /* KeyImporter.swift */; }; + 306623332406F1A8000E2AD6 /* PasswordGeneratorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306623322406F1A7000E2AD6 /* PasswordGeneratorTest.swift */; }; 3066AD6823EE0D6500F65535 /* PGPKeyImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3066AD6723EE0D6500F65535 /* PGPKeyImporter.swift */; }; 30697C2A21F63C5A0064FCAC /* NotificationNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30697C2321F63C580064FCAC /* NotificationNames.swift */; }; 30697C2B21F63C5A0064FCAC /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30697C2421F63C590064FCAC /* Globals.swift */; }; @@ -58,6 +59,7 @@ 30A1D2AC21B32C2A00E2D1F7 /* TokenBuilderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A1D2AB21B32C2A00E2D1F7 /* TokenBuilderTest.swift */; }; 30A86F95230F237000F821A4 /* CryptoFrameworkTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A86F94230F237000F821A4 /* CryptoFrameworkTest.swift */; }; 30B04860209A5141001013CA /* PasswordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B0485F209A5141001013CA /* PasswordTest.swift */; }; + 30B4C7BA24084AAA008B86F7 /* PasswordGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B4C7B924084AAA008B86F7 /* PasswordGenerator.swift */; }; 30BAC8C622E3BAAF00438475 /* TestBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAC8C422E3BAAF00438475 /* TestBase.swift */; }; 30BAC8C722E3BAAF00438475 /* TestPGPKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAC8C522E3BAAF00438475 /* TestPGPKeys.swift */; }; 30BAC8CB22E3BB6C00438475 /* DictBasedKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAC8CA22E3BB6C00438475 /* DictBasedKeychain.swift */; }; @@ -71,6 +73,8 @@ 30CCA91623258C380048CA51 /* PgpInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCA91523258C380048CA51 /* PgpInterface.swift */; }; 30CCA91823258E760048CA51 /* GopenPgp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCA91723258E760048CA51 /* GopenPgp.swift */; }; 30CCA91A232591320048CA51 /* ObjectivePgp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CCA919232591320048CA51 /* ObjectivePgp.swift */; }; + 30DAFD4A240985A7002456E7 /* Array+Slices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DAFD49240985A7002456E7 /* Array+Slices.swift */; }; + 30DAFD4C240985E3002456E7 /* Array+SlicesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DAFD4B240985E3002456E7 /* Array+SlicesTest.swift */; }; 30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */; }; 3EA2386CD0E9CE2A702A0B3E /* Pods_pass.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE627E8F3DACEDD8FA220081 /* Pods_pass.framework */; }; 556EC3D322335C5F00934F9C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 30BF5ECA21EA8FB5000E4154 /* Localizable.strings */; }; @@ -247,6 +251,7 @@ 3032328D22CBD4CD009EBD9C /* CryptographicKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptographicKeys.swift; sourceTree = ""; }; 30650E7023F82AF8005CCD5E /* SSHKeyFileImportTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHKeyFileImportTableViewController.swift; sourceTree = ""; }; 30650E7223F847FC005CCD5E /* KeyImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyImporter.swift; sourceTree = ""; }; + 306623322406F1A7000E2AD6 /* PasswordGeneratorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordGeneratorTest.swift; sourceTree = ""; }; 3066AD6723EE0D6500F65535 /* PGPKeyImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PGPKeyImporter.swift; sourceTree = ""; }; 30697C2321F63C580064FCAC /* NotificationNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationNames.swift; sourceTree = ""; }; 30697C2421F63C590064FCAC /* Globals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; @@ -282,6 +287,7 @@ 30A1D2AB21B32C2A00E2D1F7 /* TokenBuilderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBuilderTest.swift; sourceTree = ""; }; 30A86F94230F237000F821A4 /* CryptoFrameworkTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFrameworkTest.swift; sourceTree = ""; }; 30B0485F209A5141001013CA /* PasswordTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTest.swift; sourceTree = ""; }; + 30B4C7B924084AAA008B86F7 /* PasswordGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordGenerator.swift; sourceTree = ""; }; 30BAC8C422E3BAAF00438475 /* TestBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestBase.swift; sourceTree = ""; }; 30BAC8C522E3BAAF00438475 /* TestPGPKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestPGPKeys.swift; sourceTree = ""; }; 30BAC8CA22E3BB6C00438475 /* DictBasedKeychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictBasedKeychain.swift; sourceTree = ""; }; @@ -300,6 +306,8 @@ 30CCA91523258C380048CA51 /* PgpInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PgpInterface.swift; sourceTree = ""; }; 30CCA91723258E760048CA51 /* GopenPgp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GopenPgp.swift; sourceTree = ""; }; 30CCA919232591320048CA51 /* ObjectivePgp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectivePgp.swift; sourceTree = ""; }; + 30DAFD49240985A7002456E7 /* Array+Slices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Slices.swift"; sourceTree = ""; }; + 30DAFD4B240985E3002456E7 /* Array+SlicesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+SlicesTest.swift"; sourceTree = ""; }; 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = ""; }; 3B2B2F844061EFA534FE9506 /* Pods_passKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_passKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 62DEE9943E0F2B8C79E3FC5B /* Pods-passExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-passExtension/Pods-passExtension.release.xcconfig"; sourceTree = ""; }; @@ -460,7 +468,6 @@ 301F6464216164670071A4CE /* Helpers */ = { isa = PBXGroup; children = ( - 30A1D29B21AF451E00E2D1F7 /* PasswordGeneratorFlavorTest.swift */, 3032328922C9FBA2009EBD9C /* KeyFileManagerTest.swift */, ); path = Helpers; @@ -481,10 +488,20 @@ name = Pods; sourceTree = ""; }; + 30662336240835D0000E2AD6 /* Passwords */ = { + isa = PBXGroup; + children = ( + 30B4C7B924084AAA008B86F7 /* PasswordGenerator.swift */, + 30697C2621F63C590064FCAC /* PasswordGeneratorFlavor.swift */, + ); + path = Passwords; + sourceTree = ""; + }; 30697C5521F63F870064FCAC /* Extensions */ = { isa = PBXGroup; children = ( 30697C5E21F674800064FCAC /* String+UtilitiesTest.swift */, + 30DAFD4B240985E3002456E7 /* Array+SlicesTest.swift */, ); path = Extensions; sourceTree = ""; @@ -498,9 +515,19 @@ path = Crypto; sourceTree = ""; }; + 30B4C7BB24085A3C008B86F7 /* Passwords */ = { + isa = PBXGroup; + children = ( + 30A1D29B21AF451E00E2D1F7 /* PasswordGeneratorFlavorTest.swift */, + 306623322406F1A7000E2AD6 /* PasswordGeneratorTest.swift */, + ); + path = Passwords; + sourceTree = ""; + }; 30B6AABA21F49095006B352D /* Extensions */ = { isa = PBXGroup; children = ( + 30DAFD49240985A7002456E7 /* Array+Slices.swift */, 30CCA90A2325119C0048CA51 /* Data+Mutable.swift */, 30697C3621F63C990064FCAC /* String+Localization.swift */, 30697C3921F63C990064FCAC /* String+Utilities.swift */, @@ -612,6 +639,7 @@ A2F4E20F1EED7F0A0011986E /* Helpers */, A2F4E20E1EED7F040011986E /* Models */, 30C015A3214ECF2B005BB6DF /* Parser */, + 30662336240835D0000E2AD6 /* Passwords */, A23DD0DB233FB46900E6CD83 /* Assets.xcassets */, A260757A1EEC6F34005DB03E /* passKit.h */, A26075A51EEC7125005DB03E /* pass.xcdatamodeld */, @@ -629,6 +657,7 @@ 301F6464216164670071A4CE /* Helpers */, 30C015A7214ED378005BB6DF /* Models */, 30C015A6214ED32A005BB6DF /* Parser */, + 30B4C7BB24085A3C008B86F7 /* Passwords */, A26075871EEC6F34005DB03E /* passKitTests.swift */, A26075891EEC6F34005DB03E /* Info.plist */, ); @@ -684,7 +713,6 @@ 3032327322C7F710009EBD9C /* KeyFileManager.swift */, 30BAC8CC22E3BB9700438475 /* KeyStore.swift */, 30697C2321F63C580064FCAC /* NotificationNames.swift */, - 30697C2621F63C590064FCAC /* PasswordGeneratorFlavor.swift */, 302202EE222F14E400555236 /* SearchBarScope.swift */, 30697C2721F63C590064FCAC /* Utils.swift */, ); @@ -1324,10 +1352,12 @@ 302E85612125ECC70031BA64 /* Parser.swift in Sources */, 30CCA91A232591320048CA51 /* ObjectivePgp.swift in Sources */, 30697C4621F63CAB0064FCAC /* GitCredential.swift in Sources */, + 30B4C7BA24084AAA008B86F7 /* PasswordGenerator.swift in Sources */, 30A1D2A621B2D46100E2D1F7 /* OtpType.swift in Sources */, 3032328E22CBD4CD009EBD9C /* CryptographicKeys.swift in Sources */, 30697C2A21F63C5A0064FCAC /* NotificationNames.swift in Sources */, 30CCA91623258C380048CA51 /* PgpInterface.swift in Sources */, + 30DAFD4A240985A7002456E7 /* Array+Slices.swift in Sources */, 30697C4721F63CAB0064FCAC /* PasscodeLock.swift in Sources */, A2699ACD2402631400F36323 /* PasswordTableEntry.swift in Sources */, 30697C3421F63C8B0064FCAC /* PasscodeLockViewController.swift in Sources */, @@ -1350,6 +1380,7 @@ files = ( 30A86F95230F237000F821A4 /* CryptoFrameworkTest.swift in Sources */, 30A1D2AC21B32C2A00E2D1F7 /* TokenBuilderTest.swift in Sources */, + 30DAFD4C240985E3002456E7 /* Array+SlicesTest.swift in Sources */, 301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */, 30BAC8CB22E3BB6C00438475 /* DictBasedKeychain.swift in Sources */, A2699ACF24027D9500F36323 /* PasswordTableEntryTest.swift in Sources */, @@ -1359,6 +1390,7 @@ 30B04860209A5141001013CA /* PasswordTest.swift in Sources */, 30697C5F21F674800064FCAC /* String+UtilitiesTest.swift in Sources */, 3032328A22C9FBA2009EBD9C /* KeyFileManagerTest.swift in Sources */, + 306623332406F1A8000E2AD6 /* PasswordGeneratorTest.swift in Sources */, 30BAC8C722E3BAAF00438475 /* TestPGPKeys.swift in Sources */, 30A1D2AA21B32A0100E2D1F7 /* OtpTypeTest.swift in Sources */, 301F6468216165290071A4CE /* ConstantsTest.swift in Sources */, diff --git a/passKit/Extensions/Array+Slices.swift b/passKit/Extensions/Array+Slices.swift new file mode 100644 index 0000000..25cb44f --- /dev/null +++ b/passKit/Extensions/Array+Slices.swift @@ -0,0 +1,28 @@ +// +// Array+Slices.swift +// passKit +// +// Created by Danny Moesch on 28.02.20. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +extension Array { + + func slices(count: UInt) -> [ArraySlice] { + guard count != 0 else { + return [] + } + let sizeEach = Int(self.count / Int(count)) + var currentIndex = startIndex + var slices = [ArraySlice]() + for _ in 0 ..< count { + let toIndex = index(currentIndex, offsetBy: sizeEach, limitedBy: endIndex) ?? endIndex + slices.append(self[currentIndex ..< toIndex]) + currentIndex = toIndex + } + if currentIndex != endIndex { + slices[slices.endIndex - 1].append(contentsOf: self[currentIndex ..< endIndex]) + } + return slices + } +} diff --git a/passKit/Helpers/DefaultsKeys.swift b/passKit/Helpers/DefaultsKeys.swift index 05b961a..7f4f4d4 100644 --- a/passKit/Helpers/DefaultsKeys.swift +++ b/passKit/Helpers/DefaultsKeys.swift @@ -20,7 +20,7 @@ public enum GitAuthenticationMethod: String, DefaultsSerializable { } extension SearchBarScope: DefaultsSerializable {} -extension PasswordGeneratorFlavor: DefaultsSerializable {} +extension PasswordGenerator: DefaultsSerializable {} public extension DefaultsKeys { var pgpKeySource: DefaultsKey { .init("pgpKeySource") } @@ -53,7 +53,7 @@ public extension DefaultsKeys { var isShowFolderOn: DefaultsKey { .init("isShowFolderOn", defaultValue: true) } var isHidePasswordImagesOn: DefaultsKey { .init("isHidePasswordImagesOn", defaultValue: false) } var searchDefault: DefaultsKey { .init("searchDefault", defaultValue: .all) } - var passwordGeneratorFlavor: DefaultsKey { .init("passwordGeneratorFlavor", defaultValue: .apple) } + var passwordGenerator: DefaultsKey { .init("passwordGenerator", defaultValue: PasswordGenerator()) } var encryptInArmored: DefaultsKey { .init("encryptInArmored", defaultValue: false) } } diff --git a/passKit/Helpers/PasswordGeneratorFlavor.swift b/passKit/Helpers/PasswordGeneratorFlavor.swift deleted file mode 100644 index 881709a..0000000 --- a/passKit/Helpers/PasswordGeneratorFlavor.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// PasswordGeneratorFlavor.swift -// passKit -// -// Created by Danny Moesch on 28.11.18. -// Copyright © 2018 Bob Sun. All rights reserved. -// - -import KeychainAccess - -public enum PasswordGeneratorFlavor: String { - case apple = "Apple" - case random = "Random" - case xkcd = "XKCD" - - private static let words: [String] = { - let bundle = Bundle(identifier: Globals.passKitBundleIdentifier)! - return ["eff_long_wordlist", "eff_short_wordlist"] - .map { name -> String in - guard let asset = NSDataAsset(name: name, bundle: bundle), - let data = String(data: asset.data, encoding: .utf8) else { - return "" - } - return data - } - .joined(separator: "\n") - .splitByNewline() - }() - - public var localized: String { - return rawValue.localize() - } - - public var longNameLocalized: String { - switch self { - case .apple: - return "ApplesKeychainStyle".localize() - case .random: - return "RandomString".localize() - case .xkcd: - return "XKCDStyle".localize() - } - } - - public var defaultLength: (min: Int, max: Int, def: Int) { - switch self { - case .apple: - return (15, 15, 15) - case .random: - return (4, 64, 16) - case .xkcd: - return (2, 5, 3) - } - } - - public func generate(length: Int) -> String { - switch self { - case .apple: - return Keychain.generatePassword() - case .random: - return Self.generateRandom(length: length) - case .xkcd: - return Self.generateXKCD(length: length) - } - } - - private static func generateRandom(length: Int) -> String { - let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*_+-=" - return String((0.. String { - let delimiters = "0123456789!@#$%^&*_+-=" - var password = "" - (0..?@[\\]^_`{|}~" + + private static let words: [String] = { + let bundle = Bundle(identifier: Globals.passKitBundleIdentifier)! + return ["eff_long_wordlist", "eff_short_wordlist"] + .map { name -> String in + guard let asset = NSDataAsset(name: name, bundle: bundle), + let data = String(data: asset.data, encoding: .utf8) else { + return "" + } + return data + } + .joined(separator: "\n") + .splitByNewline() + }() + + public var flavor = PasswordGeneratorFlavor.random + public var length = 15 + public var varyCases = true + public var useDigits = true + public var useSpecialSymbols = true + public var groups = 4 + + public var limitedLength: Int { + let lengthLimits = flavor.lengthLimits + return max(lengthLimits.min, min(lengthLimits.max, length)) + } + + private var characters: String { + var characters = Self.letters + if varyCases { + characters.append(Self.capitalLetters) + } + if useDigits { + characters.append(Self.digits) + } + if useSpecialSymbols { + characters.append(Self.specialSymbols) + } + return characters + } + + private var delimiters: String { + var delimiters = "" + if useDigits { + delimiters.append(Self.digits) + } + if useSpecialSymbols { + delimiters.append(Self.specialSymbols) + } + return delimiters + } + + public func generate() -> String { + switch flavor { + case .random: + return generateRandom() + case .xkcd: + return generateXkcd() + } + } + + public func isAcceptable(groups: Int) -> Bool { + guard flavor == .random, groups > 0, groups < length else { + return false + } + return (length + 1) % groups == 0 + } + + private func generateRandom() -> String { + let currentCharacters = characters + if groups > 1, isAcceptable(groups: groups) { + return selectRandomly(count: length - groups + 1, from: currentCharacters) + .slices(count: UInt(groups)) + .map { String($0) } + .joined(separator: "-") + } + return String(selectRandomly(count: length, from: currentCharacters)) + } + + private func generateXkcd() -> String { + let currentDelimiters = delimiters + return getRandomDelimiter(from: currentDelimiters) + (0 ..< length) + .map { _ in getRandomWord() + getRandomDelimiter(from: currentDelimiters) } + .joined() + } + + private func getRandomDelimiter(from delimiters: String) -> String { + if delimiters.isEmpty { + return "" + } + return String(delimiters.randomElement()!) + } + + private func getRandomWord() -> String { + let word = Self.words.randomElement()! + if varyCases, Bool.random() { + return word.uppercased() + } + return word + } + + private func selectRandomly(count: Int, from string: String) -> [Character] { + return (0 ..< count).map { _ in string.randomElement()! } + } +} + +extension PasswordGeneratorFlavor: Codable {} diff --git a/passKit/Passwords/PasswordGeneratorFlavor.swift b/passKit/Passwords/PasswordGeneratorFlavor.swift new file mode 100644 index 0000000..bd0843b --- /dev/null +++ b/passKit/Passwords/PasswordGeneratorFlavor.swift @@ -0,0 +1,38 @@ +// +// PasswordGeneratorFlavor.swift +// passKit +// +// Created by Danny Moesch on 28.11.18. +// Copyright © 2018 Bob Sun. All rights reserved. +// + +public typealias LengthLimits = (min: Int, max: Int) + +public enum PasswordGeneratorFlavor: String { + case random = "Random" + case xkcd = "XKCD" + + public var localized: String { + return rawValue.localize() + } + + public var longNameLocalized: String { + switch self { + case .random: + return "RandomString".localize() + case .xkcd: + return "XKCDStyle".localize() + } + } + + public var lengthLimits: LengthLimits { + switch self { + case .random: + return (4, 64) + case .xkcd: + return (2, 5) + } + } +} + +extension PasswordGeneratorFlavor: CaseIterable {} diff --git a/passKitTests/Extensions/Array+SlicesTest.swift b/passKitTests/Extensions/Array+SlicesTest.swift new file mode 100644 index 0000000..f0aa4e8 --- /dev/null +++ b/passKitTests/Extensions/Array+SlicesTest.swift @@ -0,0 +1,29 @@ +// +// Array+SlicesTest.swift +// passKitTests +// +// Created by Danny Moesch on 28.02.20. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import XCTest + +@testable import passKit + +class ArraySlicesTest: XCTestCase { + + func testZeroCount() { + XCTAssertEqual([1, 2, 3].slices(count: 0), []) + } + + func testEmptyArray() { + XCTAssertEqual(([] as [String]).slices(count: 4), [[], [], [], []]) + } + + func testSlices() { + XCTAssertEqual([1, 2, 3].slices(count: 3), [[1], [2], [3]]) + XCTAssertEqual([1, 2, 3, 4].slices(count: 3), [[1], [2], [3, 4]]) + XCTAssertEqual([1, 2, 3, 4].slices(count: 2), [[1, 2], [3, 4]]) + XCTAssertEqual([1, 2, 3, 4, 5].slices(count: 2), [[1, 2], [3, 4, 5]]) + } +} diff --git a/passKitTests/Helpers/PasswordGeneratorFlavorTest.swift b/passKitTests/Helpers/PasswordGeneratorFlavorTest.swift deleted file mode 100644 index fcd76f5..0000000 --- a/passKitTests/Helpers/PasswordGeneratorFlavorTest.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// PasswordGeneratorFlavorTest.swift -// passKitTests -// -// Created by Danny Moesch on 28.11.18. -// Copyright © 2018 Bob Sun. All rights reserved. -// - -import KeychainAccess -import XCTest - -@testable import passKit - -class PasswordGeneratorFlavorTest: XCTestCase { - - private let KEYCHAIN_PASSWORD_LENGTH = Keychain.generatePassword().count - - func testLocalizedName() { - XCTAssertEqual(PasswordGeneratorFlavor.apple.localized, "Apple".localize()) - XCTAssertEqual(PasswordGeneratorFlavor.random.localized, "Random".localize()) - } - - func testDefaultLength() { - // Ensure properly chosen default length values. So this check no longer needs to be performed in the code. - PasswordGeneratorFlavor.allCases.map { $0.defaultLength }.forEach { defaultLength in - XCTAssertLessThanOrEqual(defaultLength.min, defaultLength.max) - XCTAssertLessThanOrEqual(defaultLength.def, defaultLength.max) - XCTAssertGreaterThanOrEqual(defaultLength.def, defaultLength.min) - } - } - - func testGeneratePassword() { - let apple = PasswordGeneratorFlavor.apple - let random = PasswordGeneratorFlavor.random - - XCTAssertEqual(apple.generate(length: 4).count, KEYCHAIN_PASSWORD_LENGTH) - XCTAssertEqual(random.generate(length: 0).count, 0) - XCTAssertEqual(random.generate(length: 4).count, 4) - XCTAssertEqual(random.generate(length: 100).count, 100) - } -} diff --git a/passKitTests/Passwords/PasswordGeneratorFlavorTest.swift b/passKitTests/Passwords/PasswordGeneratorFlavorTest.swift new file mode 100644 index 0000000..2289db0 --- /dev/null +++ b/passKitTests/Passwords/PasswordGeneratorFlavorTest.swift @@ -0,0 +1,21 @@ +// +// PasswordGeneratorFlavorTest.swift +// passKitTests +// +// Created by Danny Moesch on 28.11.18. +// Copyright © 2018 Bob Sun. All rights reserved. +// + +import XCTest + +@testable import passKit + +class PasswordGeneratorFlavorTest: XCTestCase { + + func testLengthLimits() { + // Ensure properly chosen length limits. So this check no longer needs to be performed in the code. + PasswordGeneratorFlavor.allCases.map { $0.lengthLimits }.forEach { + XCTAssertLessThanOrEqual($0.min, $0.max) + } + } +} diff --git a/passKitTests/Passwords/PasswordGeneratorTest.swift b/passKitTests/Passwords/PasswordGeneratorTest.swift new file mode 100644 index 0000000..ae38992 --- /dev/null +++ b/passKitTests/Passwords/PasswordGeneratorTest.swift @@ -0,0 +1,79 @@ +// +// PasswordGeneratorTest.swift +// passKitTests +// +// Created by Danny Moesch on 26.02.20. +// Copyright © 2020 Bob Sun. All rights reserved. +// + +import XCTest + +@testable import passKit + +class PasswordGeneratorTest: XCTestCase { + + func testLimitedLength() { + [ + PasswordGenerator(length: 15), + PasswordGenerator(length: -3), + PasswordGenerator(length: 128), + ].forEach { generator in + XCTAssertLessThanOrEqual(generator.limitedLength, generator.flavor.lengthLimits.max) + XCTAssertGreaterThanOrEqual(generator.limitedLength, generator.flavor.lengthLimits.min) + } + } + + func testAcceptableGroups() { + [ + (15, 4), + (19, 4), + (9, 5), + (11, 6), + (259, 13), + ].forEach { length, groups in + XCTAssertTrue(PasswordGenerator(length: length).isAcceptable(groups: groups)) + } + } + + func testNotAcceptableGroups() { + [ + (15, 0), + (19, 20), + (9, 9), + (11, -1), + ].forEach { length, groups in + XCTAssertFalse(PasswordGenerator(length: length).isAcceptable(groups: groups)) + } + } + + func testGroupsAreNotcceptableForXKCDStyle() { + var generator = PasswordGenerator(length: 15) + + XCTAssertTrue(generator.isAcceptable(groups: 4)) + + generator.flavor = .xkcd + XCTAssertFalse(generator.isAcceptable(groups: 4)) + } + + func testRandomPasswordLength() { + [ + PasswordGenerator(), + PasswordGenerator(groups: 1), + PasswordGenerator(length: 25), + PasswordGenerator(length: 47, groups: 12), + PasswordGenerator(useDigits: true), + ].forEach { generator in + XCTAssertEqual(generator.generate().count, generator.length) + } + } + + func testXKCDPasswordGeneration() { + let typicalPassword = PasswordGenerator(flavor: .xkcd).generate() + XCTAssertFalse(typicalPassword.isEmpty) + XCTAssertFalse(typicalPassword.trimmingCharacters(in: .letters).isEmpty) + + let passwordWithoutSeparators = PasswordGenerator(flavor: .xkcd, useDigits: false, useSpecialSymbols: false).generate() + XCTAssertFalse(passwordWithoutSeparators.isEmpty) + XCTAssertTrue(passwordWithoutSeparators.trimmingCharacters(in: .letters).isEmpty) + } +}