Skip to content

Instantly share code, notes, and snippets.

@nerdsupremacist
Last active April 20, 2021 09:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nerdsupremacist/bd0ffb7ab5e8a8e31259b13418dc27a6 to your computer and use it in GitHub Desktop.
Save nerdsupremacist/bd0ffb7ab5e8a8e31259b13418dc27a6 to your computer and use it in GitHub Desktop.
import Foundation
public protocol PasswordCheck {
associatedtype Check: PasswordCheck
@CheckBuilder
var body: Check { get }
}
public enum PasswordResult {
case allowed
case denied(reasons: [String])
}
extension PasswordCheck {
public func check(password: String) -> PasswordResult {
switch internalCheck().check(password: password) {
case .allow:
return .allowed
case .deny(let reasons):
return .denied(reasons: reasons)
case .skip:
return .denied(reasons: [])
}
}
}
public struct DenyIfTooFewFromCharacterSet: PasswordCheck {
let name: String
let min: Int
let characterSet: CharacterSet
public init(name: String, _ min: Int, _ characterSet: CharacterSet) {
self.name = name
self.min = min
self.characterSet = characterSet
}
public var body: some PasswordCheck {
DenyIf("Too few characters are \(name). Min \(min)") { password in
password.unicodeScalars.filter { characterSet.contains($0) }.count < min
}
}
}
public struct DenyIfLettersRepeated: PasswordCheck {
public init() { }
public var body: some PasswordCheck {
DenyIf { password in
let repeated = Dictionary(grouping: password) { $0 }.filter { $0.value.count > 1 }.keys.map(String.init)
if !repeated.isEmpty {
let joined = repeated.joined(separator: ", ")
return .deny(reason: "You're not allowed to repeat characters. You repeated \(joined)")
} else {
return .skip
}
}
}
}
public struct DenyIfTooLong: PasswordCheck {
let max: Int
public init(_ max: Int) {
self.max = max
}
public var body: some PasswordCheck {
DenyIf("Password is too long. Max (\(max))") { password in
password.count > max
}
}
}
public struct DenyIfTooShort: PasswordCheck {
let min: Int
public init(_ min: Int) {
self.min = min
}
public var body: some PasswordCheck {
DenyIf("Password is too short. Min (\(min))") { password in
password.count < min
}
}
}
public struct DenyIfNotInCharacterSet: PasswordCheck {
let characterSet: CharacterSet
public init(_ characterSet: CharacterSet) {
self.characterSet = characterSet
}
public var body: some PasswordCheck {
DenyIf { password in
let notInCharacterSet = password.unicodeScalars.filter { !characterSet.contains($0) }.map(String.init)
if notInCharacterSet.isEmpty {
return .skip
} else {
return .deny(reason: "Characters \(notInCharacterSet.joined(separator: ", ")) are not allowed")
}
}
}
}
private enum Decision {
case allow, skip
case deny(reasons: [String])
}
private protocol InternalCheck {
func check(password: String) -> Decision
}
extension Never: PasswordCheck {
public var body: Never {
return fatalError()
}
}
public struct AnyCheck: PasswordCheck, InternalCheck {
fileprivate let check: InternalCheck
public init<C : PasswordCheck>(_ check: C) {
self.check = check.internalCheck()
}
fileprivate func check(password: String) -> Decision {
return check.check(password: password)
}
public var body: Never {
return fatalError()
}
}
public struct CompoundCheck: PasswordCheck, InternalCheck {
fileprivate let checks: [InternalCheck]
public var body: Never {
return fatalError()
}
fileprivate func check(password: String) -> Decision {
var isDenied = false
var deniedReasons = [String]()
for check in checks {
switch check.check(password: password) {
case .allow where !isDenied:
return .allow
case .allow:
return .deny(reasons: deniedReasons)
case .deny(let reasons):
isDenied = true
deniedReasons += reasons
continue
case .skip:
continue
}
}
if isDenied {
return .deny(reasons: deniedReasons)
}
return .skip
}
}
extension PasswordCheck {
fileprivate func internalCheck() -> InternalCheck {
if let any = self as? AnyCheck {
return any.check
}
if let internalCheck = self as? InternalCheck {
return internalCheck
}
return body.internalCheck()
}
}
@_functionBuilder
public struct CheckBuilder {
public struct Expression {
fileprivate let check: InternalCheck
}
static func buildExpression<Check : PasswordCheck>(_ check: Check) -> Expression {
return Expression(check: check.internalCheck())
}
static func buildBlock(_ expressions: Expression...) -> CompoundCheck {
return CompoundCheck(checks: expressions.map(\.check))
}
}
public struct DenyIf: PasswordCheck, InternalCheck {
public enum DenyResult {
case deny(reason: String?)
case skip
}
let decision: (String) -> DenyResult
public init(_ decision: @escaping (String) -> DenyResult) {
self.decision = decision
}
public init(_ message: String? = nil, _ condition: @escaping (String) -> Bool) {
self.init { password in
if (condition(password)) {
return .deny(reason: message)
} else {
return .skip
}
}
}
public var body: Never {
return fatalError()
}
fileprivate func check(password: String) -> Decision {
switch decision(password) {
case .deny(let reason):
return .deny(reasons: reason.map { [$0] } ?? [])
case .skip:
return .skip
}
}
}
public struct AllowIf: PasswordCheck, InternalCheck {
let condition: (String) -> Bool
public init(_ condition: @escaping (String) -> Bool) {
self.condition = condition
}
public var body: Never {
return fatalError()
}
fileprivate func check(password: String) -> Decision {
if (condition(password)) {
return .allow
}
return .skip
}
}
public struct Allow: PasswordCheck, InternalCheck {
public init() { }
public var body: Never {
return fatalError()
}
fileprivate func check(password: String) -> Decision {
return .allow
}
}
public struct Deny: PasswordCheck, InternalCheck {
let message: String?
public init(_ message: String?) {
self.message = message
}
public var body: Never {
return fatalError()
}
fileprivate func check(password: String) -> Decision {
return .deny(reasons: message.map { [$0] } ?? [])
}
}
extension CharacterSet {
static let bmwPasswords = CharacterSet.lowercaseLetters.union(.decimalDigits).union(.symbols)
}
private let invalidPrefixes: Set<String> = [
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
"90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "appl", "ibm"
]
struct BMWPasswordCheck: PasswordCheck {
public var body: some PasswordCheck {
DenyIfNotInCharacterSet(.bmwPasswords)
DenyIfTooShort(8)
DenyIfTooLong(16)
DenyIfTooFewFromCharacterSet(name: "Lowercase Letters", 4, .lowercaseLetters)
DenyIfTooFewFromCharacterSet(name: "Numbers", 1, .decimalDigits)
DenyIfLettersRepeated()
DenyIf("Not allowed to use invalid prefixes") { password in
invalidPrefixes.contains { password.hasPrefix($0) }
}
Allow()
}
}
BMWPasswordCheck().check(password: "7q73*LUNb*ZxxVULYQ@R")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment