Add logic for more customizable password generator

This commit is contained in:
Danny Moesch 2020-02-28 19:04:53 +01:00 committed by Mingshen Sun
parent 49a371d495
commit ff014a5699
10 changed files with 352 additions and 131 deletions

View file

@ -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<Element>] {
guard count != 0 else {
return []
}
let sizeEach = Int(self.count / Int(count))
var currentIndex = startIndex
var slices = [ArraySlice<Element>]()
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
}
}

View file

@ -20,7 +20,7 @@ public enum GitAuthenticationMethod: String, DefaultsSerializable {
}
extension SearchBarScope: DefaultsSerializable {}
extension PasswordGeneratorFlavor: DefaultsSerializable {}
extension PasswordGenerator: DefaultsSerializable {}
public extension DefaultsKeys {
var pgpKeySource: DefaultsKey<KeySource?> { .init("pgpKeySource") }
@ -53,7 +53,7 @@ public extension DefaultsKeys {
var isShowFolderOn: DefaultsKey<Bool> { .init("isShowFolderOn", defaultValue: true) }
var isHidePasswordImagesOn: DefaultsKey<Bool> { .init("isHidePasswordImagesOn", defaultValue: false) }
var searchDefault: DefaultsKey<SearchBarScope?> { .init("searchDefault", defaultValue: .all) }
var passwordGeneratorFlavor: DefaultsKey<PasswordGeneratorFlavor> { .init("passwordGeneratorFlavor", defaultValue: .apple) }
var passwordGenerator: DefaultsKey<PasswordGenerator> { .init("passwordGenerator", defaultValue: PasswordGenerator()) }
var encryptInArmored: DefaultsKey<Bool> { .init("encryptInArmored", defaultValue: false) }
}

View file

@ -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..<length).map { _ in chars.randomElement()! })
}
private static func generateXKCD(length: Int) -> String {
let delimiters = "0123456789!@#$%^&*_+-="
var password = ""
(0..<length).forEach { _ in
var word = words.randomElement()!
if Bool.random() {
word = word.uppercased()
}
password += word + String(delimiters.randomElement()!)
}
return password
}
}
extension PasswordGeneratorFlavor: CaseIterable {}

View file

@ -0,0 +1,121 @@
//
// PasswordGenerator.swift
// passKit
//
// Created by Danny Moesch on 27.02.20.
// Copyright © 2020 Bob Sun. All rights reserved.
//
public struct PasswordGenerator: Codable {
private static let digits = "0123456789"
private static let letters = "abcdefghijklmnopqrstuvwxyz"
private static let capitalLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
private static let specialSymbols = "!\"#$%&'()*+,./:;<=>?@[\\]^_`{|}~"
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 {}

View file

@ -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 {}