001// -------------------------------------------------------------------------------- 002// Copyright 2002-2025 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.model.control.party.server.logic; 018 019import com.echothree.model.control.party.server.control.PartyControl; 020import com.echothree.model.control.uom.common.UomConstants; 021import com.echothree.model.control.uom.server.control.UomControl; 022import com.echothree.model.control.user.server.control.UserControl; 023import com.echothree.model.data.party.server.entity.Party; 024import com.echothree.model.data.party.server.entity.PartyType; 025import com.echothree.model.data.party.server.entity.PartyTypePasswordStringPolicy; 026import com.echothree.model.data.party.server.entity.PartyTypePasswordStringPolicyDetail; 027import com.echothree.model.data.user.server.entity.UserLoginPassword; 028import com.echothree.model.data.user.server.entity.UserVisit; 029import com.echothree.model.data.user.server.value.UserLoginPasswordStringValue; 030import com.echothree.util.common.message.ExecutionErrors; 031import com.echothree.util.common.string.StringUtils; 032import com.echothree.util.server.message.ExecutionErrorAccumulator; 033import com.echothree.util.server.persistence.Session; 034import com.echothree.util.server.persistence.Sha1Utils; 035import com.echothree.util.server.string.UnitOfMeasureUtils; 036import java.util.HashMap; 037import java.util.Map; 038import javax.enterprise.context.ApplicationScoped; 039import javax.enterprise.inject.spi.CDI; 040 041@ApplicationScoped 042public class PasswordStringPolicyLogic { 043 044 protected PasswordStringPolicyLogic() { 045 super(); 046 } 047 048 public static PasswordStringPolicyLogic getInstance() { 049 return CDI.current().select(PasswordStringPolicyLogic.class).get(); 050 } 051 052 private void checkAllowChange(final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail) { 053 if(!policyDetail.getAllowChange()) { 054 ema.addExecutionError(ExecutionErrors.PasswordChangeNotAllowed.name()); 055 } 056 } 057 058 private void checkPasswordHistory(final ExecutionErrorAccumulator ema, 059 final PartyTypePasswordStringPolicyDetail policyDetail, final UserLoginPassword ulp, final String password) { 060 var passwordHistory = policyDetail.getPasswordHistory(); 061 062 if(passwordHistory != null) { 063 var userControl = Session.getModelController(UserControl.class); 064 065 for(var userLoginPasswordString: userControl.getUserLoginPasswordStringHistory(ulp, passwordHistory)) { 066 var salt = userLoginPasswordString.getSalt(); 067 068 if(Sha1Utils.getInstance().encode(salt, password).equals(userLoginPasswordString.getPassword())) { 069 ema.addExecutionError(ExecutionErrors.PasswordInRecentHistory.name(), passwordHistory); 070 break; 071 } 072 } 073 } 074 } 075 076 private void checkMinimumPasswordLifetime(final Session session, final UserVisit userVisit, 077 final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail, final UserLoginPasswordStringValue ulpsv) { 078 var minimumPasswordLifetime = policyDetail.getMinimumPasswordLifetime(); 079 080 if(minimumPasswordLifetime != null) { 081 var currentPasswordLifetime = session.START_TIME - ulpsv.getChangedTime(); 082 083 if(currentPasswordLifetime < minimumPasswordLifetime) { 084 var uomControl = Session.getModelController(UomControl.class); 085 var timeUnitOfMeasureKind = uomControl.getUnitOfMeasureKindByUnitOfMeasureKindUseTypeUsingNames(UomConstants.UnitOfMeasureKindUseType_TIME); 086 var fmtMinimumPasswordLifetime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(userVisit, 087 timeUnitOfMeasureKind, minimumPasswordLifetime); 088 var fmtCurrentPasswordLifetime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(userVisit, 089 timeUnitOfMeasureKind, Long.valueOf(currentPasswordLifetime)); 090 091 ema.addExecutionError(ExecutionErrors.PasswordMinimumLifetimeNotMet.name(), fmtMinimumPasswordLifetime, fmtCurrentPasswordLifetime); 092 } 093 } 094 } 095 096 private void checkLength(final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail, 097 final String password) { 098 var length = password.length(); 099 var minimumLegnth = policyDetail.getMinimumLength(); 100 var maximumLength = policyDetail.getMaximumLength(); 101 102 if(minimumLegnth != null && length < minimumLegnth) { 103 ema.addExecutionError(ExecutionErrors.PasswordLessThanMinimumLength.name(), minimumLegnth); 104 } 105 106 if(maximumLength != null && length < maximumLength) { 107 ema.addExecutionError(ExecutionErrors.PasswordGreaterThanMaximumLength.name(), maximumLength); 108 } 109 } 110 111 private int getTypeCount(final Map<Integer, Integer> types, final byte type) { 112 var count = types.get(Integer.valueOf(type)); 113 114 return count == null? 0: count; 115 } 116 117 private void checkCharacterTypes(final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail, 118 final String password) { 119 var requiredDigitCount = policyDetail.getRequiredDigitCount(); 120 var requiredLetterCount = policyDetail.getRequiredLetterCount(); 121 var requiredUpperCaseCount = policyDetail.getRequiredUpperCaseCount(); 122 var requiredLowerCaseCount = policyDetail.getRequiredLowerCaseCount(); 123 var maximumRepeated = policyDetail.getMaximumRepeated(); 124 var minimumCharacterTypes = policyDetail.getMinimumCharacterTypes(); 125 Map<Integer, Integer> types = new HashMap<>(); 126 var lastCh = 0; 127 var repeat = 0; 128 var maxRepeat = 0; 129 130 for(int ch : StringUtils.getInstance().codePoints(password)) { 131 Integer type = Character.getType(ch); 132 var count = types.get(type); 133 134 if(count == null) { 135 types.put(type, 1); 136 } else { 137 types.put(type, count + 1); 138 } 139 140 if(ch == lastCh) { 141 repeat++; 142 } else { 143 lastCh = ch; 144 145 if(repeat > maxRepeat) { 146 maxRepeat = repeat; 147 } 148 149 repeat = 1; 150 } 151 } 152 153 if(repeat > maxRepeat) { 154 maxRepeat = repeat; 155 } 156 157 var upperCaseCount = getTypeCount(types, Character.UPPERCASE_LETTER); 158 var lowerCaseCount = getTypeCount(types, Character.LOWERCASE_LETTER); 159 var letterCount = upperCaseCount + lowerCaseCount; 160 161 if(requiredDigitCount != null) { 162 var digitCount = getTypeCount(types, Character.DECIMAL_DIGIT_NUMBER); 163 164 if(digitCount < requiredDigitCount) { 165 ema.addExecutionError(ExecutionErrors.PasswordRequiredDigitCountNotMet.name(), requiredDigitCount); 166 } 167 } 168 169 if(requiredLetterCount != null) { 170 if(letterCount < requiredLetterCount) { 171 ema.addExecutionError(ExecutionErrors.PasswordRequiredLetterCountNotMet.name(), requiredLetterCount); 172 } 173 } 174 175 if(requiredUpperCaseCount != null) { 176 if(upperCaseCount < requiredUpperCaseCount) { 177 ema.addExecutionError(ExecutionErrors.PasswordRequiredUpperCaseCountNotMet.name(), requiredUpperCaseCount); 178 } 179 } 180 181 if(requiredLowerCaseCount != null) { 182 if(lowerCaseCount < requiredLowerCaseCount) { 183 ema.addExecutionError(ExecutionErrors.PasswordRequiredLowerCaseCountNotMet.name(), requiredLowerCaseCount); 184 } 185 } 186 187 if(maximumRepeated != null) { 188 if(maxRepeat < maximumRepeated) { 189 ema.addExecutionError(ExecutionErrors.PasswordMaximumRepeatedExceeded.name(), maximumRepeated); 190 } 191 } 192 193 if(minimumCharacterTypes != null) { 194 var characterTypes = types.size(); 195 196 if(characterTypes < minimumCharacterTypes) { 197 ema.addExecutionError(ExecutionErrors.PasswordMinimumCharacterTypesNotMet.name(), minimumCharacterTypes); 198 } 199 } 200 } 201 202 public PartyTypePasswordStringPolicy checkStringPassword(final Session session, final UserVisit userVisit, final ExecutionErrorAccumulator ema, 203 final PartyType partyType, final UserLoginPassword ulp, final UserLoginPasswordStringValue ulpsv, final String password) { 204 var partyControl = Session.getModelController(PartyControl.class); 205 var policy = partyControl.getPartyTypePasswordStringPolicy(partyType); 206 207 if(policy != null) { 208 var policyDetail = policy.getLastDetail(); 209 210 if(ulp != null) { 211 checkPasswordHistory(ema, policyDetail, ulp, password); 212 } 213 214 if(ulpsv != null) { 215 checkAllowChange(ema, policyDetail); 216 checkMinimumPasswordLifetime(session, userVisit, ema, policyDetail, ulpsv); 217 } 218 219 checkLength(ema, policyDetail, password); 220 checkCharacterTypes(ema, policyDetail, password); 221 } 222 223 return policy; 224 } 225 226 public PartyTypePasswordStringPolicy checkStringPassword(final Session session, final UserVisit userVisit, final ExecutionErrorAccumulator ema, 227 final Party party, final UserLoginPassword ulp, final UserLoginPasswordStringValue ulpsv, final String password) { 228 var partyType = party.getLastDetail().getPartyType(); 229 230 return checkStringPassword(session, userVisit, ema, partyType, ulp, ulpsv, password); 231 } 232 233}