Last active
November 12, 2024 15:30
-
-
Save Codelaby/beeb3a1438ca2c7990a74256d7408741 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class PasswordViewModel: ObservableObject { | |
@Published var password: String = "" { | |
didSet { | |
validatePassword() | |
} | |
} | |
@Published var isPasswordValid: Bool = false | |
@Published var complianceSummary: String = "" | |
@Published var compliantRules: [PasswordRule] = [] | |
private let rules: [PasswordRule] = [ | |
.allowed(.custom(Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_"))), // Solo caracteres permitidos | |
.required(.upper), // Al menos una letra mayúscula | |
.required(.lower), // Al menos una letra minúscula | |
.required(.digits), // Al menos un dígito | |
.required(.special), // Al menos un carácter especial | |
.requiredLength(8), // Longitud mínima de 8 caracteres | |
.maxLength(16), // Longitud máxima de 16 caracteres | |
.maxConsecutive(2) // Máximo de 2 caracteres consecutivos iguales | |
] | |
// Generador de descripción para las reglas | |
var rulesDescription: String { | |
PasswordRule.descriptor(for: rules) | |
} | |
// Valida la contraseña actual y actualiza las propiedades de estado | |
private func validatePassword() { | |
let result = PasswordRule.evaluate(password: password, rules: rules) | |
isPasswordValid = result.allRequiredMet | |
complianceSummary = "\(result.complianceCount) / \(result.totalRules) reglas cumplidas." | |
compliantRules = result.compliantRules | |
} | |
} | |
// Vista SwiftUI que interactúa con PasswordViewModel | |
struct SecureStronghtField: View { | |
@StateObject private var viewModel = PasswordViewModel() | |
var body: some View { | |
VStack(alignment: .leading, spacing: 16) { | |
Text("Contraseña") | |
.font(.headline) | |
TextField("Ingresa tu contraseña", text: $viewModel.password) | |
.textContentType(.password) | |
.autocorrectionDisabled() | |
.autocapitalization(.none) | |
.textFieldStyle(RoundedBorderTextFieldStyle()) | |
Text(viewModel.complianceSummary) | |
.foregroundColor(viewModel.isPasswordValid ? .green : .red) | |
.font(.caption) | |
if !viewModel.isPasswordValid { | |
VStack(alignment: .leading) { | |
Text("Reglas de la contraseña:") | |
.font(.subheadline) | |
Text(viewModel.rulesDescription) | |
.font(.footnote) | |
.foregroundColor(.secondary) | |
} | |
} | |
} | |
.padding() | |
} | |
} | |
#Preview { | |
SecureStronghtField() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
fileprivate extension String { | |
func containsConsecutiveCharacters(length: Int) -> Bool { | |
let characters = Array(self) | |
for i in 0..<(characters.count - length + 1) { | |
let substring = String(characters[i..<(i + length)]) | |
if substring.allSatisfy({ $0 == substring.first }) { | |
return true | |
} | |
} | |
return false | |
} | |
} | |
fileprivate extension PasswordRule.CharacterClass { | |
func contains(_ character: Character) -> Bool { | |
switch self { | |
case .upper: return character.isUppercase | |
case .lower: return character.isLowercase | |
case .digits: return character.isNumber | |
case .special: return "!@#$%^&*()_".contains(character) | |
case .asciiPrintable: return character.isASCII && character.unicodeScalars.allSatisfy { $0.isASCII && $0.value >= 32 && $0.value <= 126 } | |
case .unicode: return true | |
case .custom(let characters): return characters.contains(character) | |
} | |
} | |
} | |
enum PasswordRule: CustomStringConvertible { | |
enum CharacterClass { | |
case upper | |
case lower | |
case digits | |
case special | |
case asciiPrintable | |
case unicode | |
case custom(Set<Character>) | |
} | |
case required(CharacterClass) | |
case allowed(CharacterClass) | |
case maxConsecutive(UInt) | |
case requiredLength(UInt) | |
case maxLength(UInt) | |
case range(CharacterClass, min: UInt, max: UInt) | |
static func descriptor(for rules: [PasswordRule]) -> String { | |
return rules.map { "\($0.description);" }.joined(separator: " ") | |
} | |
static func evaluate(password: String, rules: [PasswordRule]) -> (allRequiredMet: Bool, complianceCount: Int, totalRules: Int, compliantRules: [PasswordRule]) { | |
var complianceCount = 0 | |
var compliantRules: [PasswordRule] = [] | |
var allRequiredMet = true | |
guard password.count >= 3 else { | |
return (false, 0, rules.count, compliantRules) | |
} | |
for rule in rules { | |
if rule.isCompliant(password: password) { | |
complianceCount += 1 | |
compliantRules.append(rule) | |
} else if rule.isRequired { | |
allRequiredMet = false | |
} else if case .allowed = rule { | |
allRequiredMet = false | |
} | |
} | |
return (allRequiredMet, complianceCount, rules.count, compliantRules) | |
} | |
private func isCompliant(password: String) -> Bool { | |
switch self { | |
case .required(let characterClass): | |
return password.contains { characterClass.contains($0) } | |
case .allowed(let characterClass): | |
return password.allSatisfy { characterClass.contains($0) } | |
case .maxConsecutive(let length): | |
return !password.containsConsecutiveCharacters(length: Int(length)) | |
case .requiredLength(let length): | |
return password.count >= Int(length) | |
case .maxLength(let length): | |
return password.count <= Int(length) | |
case .range(let characterClass, let min, let max): | |
let count = password.filter { characterClass.contains($0) }.count | |
return count >= Int(min) && count <= Int(max) | |
} | |
} | |
var isRequired: Bool { | |
switch self { | |
case .required, .requiredLength, .maxLength: | |
return true | |
default: | |
return false | |
} | |
} | |
var description: String { | |
switch self { | |
case .required(let characterClass): | |
return "required: \(characterClass)" | |
case .allowed(let characterClass): | |
return "allowed: \(characterClass)" | |
case .maxConsecutive(let length): | |
return "max-consecutive: \(length)" | |
case .requiredLength(let length): | |
return "required-length: \(length)" | |
case .maxLength(let length): | |
return "max-length: \(length)" | |
case .range(let characterClass, let min, let max): | |
return "range: \(characterClass) min: \(min) max: \(max)" | |
} | |
} | |
} | |
extension String { | |
func containsConsecutiveCharacters(length: Int) -> Bool { | |
let characters = Array(self) | |
for i in 0..<(characters.count - length + 1) { | |
let substring = String(characters[i..<(i + length)]) | |
if substring.allSatisfy({ $0 == substring.first }) { | |
return true | |
} | |
} | |
return false | |
} | |
} | |
extension PasswordRule.CharacterClass { | |
func contains(_ character: Character) -> Bool { | |
switch self { | |
case .upper: return character.isUppercase | |
case .lower: return character.isLowercase | |
case .digits: return character.isNumber | |
case .special: return "!@#$%^&*()".contains(character) | |
case .asciiPrintable: return character.isASCII && character.unicodeScalars.allSatisfy { $0.isASCII && $0.value >= 32 && $0.value <= 126 } | |
case .unicode: return true | |
case .custom(let characters): return characters.contains(character) | |
} | |
} | |
} | |
// Definición de las reglas de contraseña | |
let passwordRules: [PasswordRule] = [ | |
.required(.upper), | |
.required(.lower), | |
.required(.digits), | |
.required(.special), | |
.allowed(.unicode), | |
.maxConsecutive(3), | |
.requiredLength(8), | |
.maxLength(16), | |
.required(.custom(Set("!@#$%^&*()"))), | |
.range(.upper, min: 2, max: 4), | |
.range(.lower, min: 2, max: 4), | |
.range(.digits, min: 2, max: 4), | |
.range(.special, min: 2, max: 4) | |
] | |
// Evaluar diferentes contraseñas | |
let passwords = [ | |
"_", | |
"aaaabc", | |
"Aa1!Bb2@", | |
// "Abc123!", | |
// "abcdefgh", | |
// "ABCDEFGH", | |
// "12345678", | |
// "Aa1!Aa1!", | |
// "Aa1!Bb2@Cc3#", | |
// "Aa1!Bb2@Cc3#Dd4$", | |
// "Aa1!Bb2@Cc3#Dd4$Ee5%", | |
// "Aa1!Bb2@Cc3#Dd4$Ee5%Ff6^" | |
] | |
for password in passwords { | |
print("Evaluating password: \(password)") | |
let evaluation = PasswordRule.evaluate(password: password, rules: passwordRules) | |
print("Compliance Count: \(evaluation.complianceCount) / Total Rules: \(evaluation.totalRules)") | |
print("All Required Met: \(evaluation.allRequiredMet)") | |
print("Compliant Rules: \(evaluation.compliantRules.map { $0.description })") | |
print("---") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
struct PasswordRuleSet: OptionSet { | |
let rawValue: Int | |
static let requiresUppercase = PasswordRuleSet(rawValue: 1 << 0) | |
static let requiresLowercase = PasswordRuleSet(rawValue: 1 << 1) | |
static let requiresDigits = PasswordRuleSet(rawValue: 1 << 2) | |
static let requiresSpecial = PasswordRuleSet(rawValue: 1 << 3) | |
static let allowsUnicode = PasswordRuleSet(rawValue: 1 << 4) | |
static let maxConsecutive = PasswordRuleSet(rawValue: 1 << 5) | |
static let minLength = PasswordRuleSet(rawValue: 1 << 6) | |
static let maxLength = PasswordRuleSet(rawValue: 1 << 7) | |
static let rangeUppercase = PasswordRuleSet(rawValue: 1 << 8) | |
static let rangeLowercase = PasswordRuleSet(rawValue: 1 << 9) | |
static let rangeDigits = PasswordRuleSet(rawValue: 1 << 10) | |
static let rangeSpecial = PasswordRuleSet(rawValue: 1 << 11) | |
} | |
struct PasswordConfig { | |
var options: PasswordRuleSet | |
var minLength: UInt = 8 | |
var maxLength: UInt = 16 | |
var maxConsecutive: UInt = 3 | |
var rangeUppercase: (min: UInt, max: UInt) = (2, 4) | |
var rangeLowercase: (min: UInt, max: UInt) = (2, 4) | |
var rangeDigits: (min: UInt, max: UInt) = (2, 4) | |
var rangeSpecial: (min: UInt, max: UInt) = (2, 4) | |
} | |
// Extensión para detectar caracteres consecutivos | |
extension String { | |
func containsConsecutiveCharacters(length: Int) -> Bool { | |
let characters = Array(self) | |
for i in 0..<(characters.count - length + 1) { | |
let substring = String(characters[i..<(i + length)]) | |
if substring.allSatisfy({ $0 == substring.first }) { | |
return true | |
} | |
} | |
return false | |
} | |
} | |
// Definimos una configuración para las reglas de la contraseña | |
let passwordConfig = PasswordConfig( | |
options: [.requiresUppercase, .requiresLowercase, .requiresDigits, .requiresSpecial, .allowsUnicode, .maxConsecutive, .minLength, .maxLength, .rangeUppercase, .rangeLowercase, .rangeDigits, .rangeSpecial], | |
minLength: 8, | |
maxLength: 16, | |
maxConsecutive: 3, | |
rangeUppercase: (2, 4), | |
rangeLowercase: (2, 4), | |
rangeDigits: (2, 4), | |
rangeSpecial: (2, 4) | |
) | |
// Modificación de la función para manejar el caso de contraseñas vacías y sin reglas cumplidas | |
func evaluatePassword(_ password: String, with config: PasswordConfig) -> (isValid: Bool, complianceSet: PasswordRuleSet, complianceCount: Int) { | |
// Si la contraseña está vacía, devuelve inmediatamente que no cumple ninguna regla | |
guard password.count >= 3 else { | |
return (isValid: false, complianceSet: [], complianceCount: 0) | |
} | |
let options = config.options | |
var complianceSet: PasswordRuleSet = [] | |
var totalRulesSet: PasswordRuleSet = [] | |
// Verificar longitud mínima | |
if options.contains(.minLength) { | |
totalRulesSet.insert(.minLength) | |
if password.count >= config.minLength { | |
complianceSet.insert(.minLength) | |
} | |
} | |
// Verificar longitud máxima | |
if options.contains(.maxLength) { | |
totalRulesSet.insert(.maxLength) | |
if password.count <= config.maxLength { | |
complianceSet.insert(.maxLength) | |
} | |
} | |
// Verificar caracteres consecutivos | |
if options.contains(.maxConsecutive) { | |
totalRulesSet.insert(.maxConsecutive) | |
if !password.containsConsecutiveCharacters(length: Int(config.maxConsecutive)) { | |
complianceSet.insert(.maxConsecutive) | |
} | |
} | |
// Verificar clases de caracteres con rango | |
if options.contains(.rangeUppercase) { | |
// Asegurarse de que min <= max para rangeUppercase | |
guard config.rangeUppercase.min <= config.rangeUppercase.max else { | |
fatalError("Rango de mayúsculas no válido: min > max") | |
} | |
totalRulesSet.insert(.rangeUppercase) | |
let uppercaseCount = password.filter({ $0.isUppercase }).count | |
if uppercaseCount >= config.rangeUppercase.min && uppercaseCount <= config.rangeUppercase.max { | |
complianceSet.insert(.rangeUppercase) | |
} | |
} | |
if options.contains(.rangeLowercase) { | |
// Asegurarse de que min <= max para rangeLowercase | |
guard config.rangeLowercase.min <= config.rangeLowercase.max else { | |
fatalError("Rango de minúsculas no válido: min > max") | |
} | |
totalRulesSet.insert(.rangeLowercase) | |
let lowercaseCount = password.filter({ $0.isLowercase }).count | |
if lowercaseCount >= config.rangeLowercase.min && lowercaseCount <= config.rangeLowercase.max { | |
complianceSet.insert(.rangeLowercase) | |
} | |
} | |
if options.contains(.rangeDigits) { | |
// Asegurarse de que min <= max para rangeDigits | |
guard config.rangeDigits.min <= config.rangeDigits.max else { | |
fatalError("Rango de dígitos no válido: min > max") | |
} | |
totalRulesSet.insert(.rangeDigits) | |
let digitCount = password.filter({ $0.isNumber }).count | |
if digitCount >= config.rangeDigits.min && digitCount <= config.rangeDigits.max { | |
complianceSet.insert(.rangeDigits) | |
} | |
} | |
if options.contains(.rangeSpecial) { | |
// Asegurarse de que min <= max para rangeSpecial | |
guard config.rangeSpecial.min <= config.rangeSpecial.max else { | |
fatalError("Rango de caracteres especiales no válido: min > max") | |
} | |
totalRulesSet.insert(.rangeSpecial) | |
let specialCount = password.filter({ "!@#$%^&*()".contains($0) }).count | |
if specialCount >= config.rangeSpecial.min && specialCount <= config.rangeSpecial.max { | |
complianceSet.insert(.rangeSpecial) | |
} | |
} | |
// Comparar rawValue para determinar si la contraseña cumple todas las reglas | |
let isValid = complianceSet.rawValue == totalRulesSet.rawValue | |
let complianceCount = complianceSet.rawValue.nonzeroBitCount // Número de reglas cumplidas | |
return (isValid, complianceSet, complianceCount) | |
} | |
// Ejemplos de contraseñas | |
let passwords = [ | |
"____", | |
"aaa", | |
"A1a@Bc2!", | |
"Aaaaa123!", | |
"Aa1!Aa1!", | |
"Ab1@Def2", | |
"ABCDEFGH" | |
] | |
// Total de reglas definidas en el PasswordConfig | |
let totalRulesCount = passwordConfig.options.rawValue.nonzeroBitCount | |
// Evaluar cada contraseña | |
for password in passwords { | |
let result = evaluatePassword(password, with: passwordConfig) | |
print("Password: \(password)") | |
print("Is valid? \(result.isValid)") | |
print("Compliance Set: \(result.complianceSet)") // ComplianceValue que indica las reglas cumplidas | |
print("Compliance Count: \(result.complianceCount) of \(totalRulesCount)") // Número de reglas cumplidas | |
print("---") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment