001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 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; 040import javax.inject.Inject; 041 042@ApplicationScoped 043public class PasswordStringPolicyLogic { 044 045 @Inject 046 PartyControl partyControl; 047 048 @Inject 049 UomControl uomControl; 050 051 @Inject 052 UserControl userControl; 053 054 protected PasswordStringPolicyLogic() { 055 super(); 056 } 057 058 public static PasswordStringPolicyLogic getInstance() { 059 return CDI.current().select(PasswordStringPolicyLogic.class).get(); 060 } 061 062 private void checkAllowChange(final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail) { 063 if(!policyDetail.getAllowChange()) { 064 ema.addExecutionError(ExecutionErrors.PasswordChangeNotAllowed.name()); 065 } 066 } 067 068 private void checkPasswordHistory(final ExecutionErrorAccumulator ema, 069 final PartyTypePasswordStringPolicyDetail policyDetail, final UserLoginPassword ulp, final String password) { 070 var passwordHistory = policyDetail.getPasswordHistory(); 071 072 if(passwordHistory != null) { 073 for(var userLoginPasswordString: userControl.getUserLoginPasswordStringHistory(ulp, passwordHistory)) { 074 var salt = userLoginPasswordString.getSalt(); 075 076 if(Sha1Utils.getInstance().encode(salt, password).equals(userLoginPasswordString.getPassword())) { 077 ema.addExecutionError(ExecutionErrors.PasswordInRecentHistory.name(), passwordHistory); 078 break; 079 } 080 } 081 } 082 } 083 084 private void checkMinimumPasswordLifetime(final Session session, final UserVisit userVisit, 085 final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail, final UserLoginPasswordStringValue ulpsv) { 086 var minimumPasswordLifetime = policyDetail.getMinimumPasswordLifetime(); 087 088 if(minimumPasswordLifetime != null) { 089 var currentPasswordLifetime = session.getStartTime() - ulpsv.getChangedTime(); 090 091 if(currentPasswordLifetime < minimumPasswordLifetime) { 092 var timeUnitOfMeasureKind = uomControl.getUnitOfMeasureKindByUnitOfMeasureKindUseTypeUsingNames(UomConstants.UnitOfMeasureKindUseType_TIME); 093 var fmtMinimumPasswordLifetime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(userVisit, 094 timeUnitOfMeasureKind, minimumPasswordLifetime); 095 var fmtCurrentPasswordLifetime = UnitOfMeasureUtils.getInstance().formatUnitOfMeasure(userVisit, 096 timeUnitOfMeasureKind, Long.valueOf(currentPasswordLifetime)); 097 098 ema.addExecutionError(ExecutionErrors.PasswordMinimumLifetimeNotMet.name(), fmtMinimumPasswordLifetime, fmtCurrentPasswordLifetime); 099 } 100 } 101 } 102 103 private void checkLength(final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail, 104 final String password) { 105 var length = password.length(); 106 var minimumLegnth = policyDetail.getMinimumLength(); 107 var maximumLength = policyDetail.getMaximumLength(); 108 109 if(minimumLegnth != null && length < minimumLegnth) { 110 ema.addExecutionError(ExecutionErrors.PasswordLessThanMinimumLength.name(), minimumLegnth); 111 } 112 113 if(maximumLength != null && length < maximumLength) { 114 ema.addExecutionError(ExecutionErrors.PasswordGreaterThanMaximumLength.name(), maximumLength); 115 } 116 } 117 118 private int getTypeCount(final Map<Integer, Integer> types, final byte type) { 119 var count = types.get(Integer.valueOf(type)); 120 121 return count == null? 0: count; 122 } 123 124 private void checkCharacterTypes(final ExecutionErrorAccumulator ema, final PartyTypePasswordStringPolicyDetail policyDetail, 125 final String password) { 126 var requiredDigitCount = policyDetail.getRequiredDigitCount(); 127 var requiredLetterCount = policyDetail.getRequiredLetterCount(); 128 var requiredUpperCaseCount = policyDetail.getRequiredUpperCaseCount(); 129 var requiredLowerCaseCount = policyDetail.getRequiredLowerCaseCount(); 130 var maximumRepeated = policyDetail.getMaximumRepeated(); 131 var minimumCharacterTypes = policyDetail.getMinimumCharacterTypes(); 132 Map<Integer, Integer> types = new HashMap<>(); 133 var lastCh = 0; 134 var repeat = 0; 135 var maxRepeat = 0; 136 137 for(int ch : StringUtils.getInstance().codePoints(password)) { 138 Integer type = Character.getType(ch); 139 var count = types.get(type); 140 141 if(count == null) { 142 types.put(type, 1); 143 } else { 144 types.put(type, count + 1); 145 } 146 147 if(ch == lastCh) { 148 repeat++; 149 } else { 150 lastCh = ch; 151 152 if(repeat > maxRepeat) { 153 maxRepeat = repeat; 154 } 155 156 repeat = 1; 157 } 158 } 159 160 if(repeat > maxRepeat) { 161 maxRepeat = repeat; 162 } 163 164 var upperCaseCount = getTypeCount(types, Character.UPPERCASE_LETTER); 165 var lowerCaseCount = getTypeCount(types, Character.LOWERCASE_LETTER); 166 var letterCount = upperCaseCount + lowerCaseCount; 167 168 if(requiredDigitCount != null) { 169 var digitCount = getTypeCount(types, Character.DECIMAL_DIGIT_NUMBER); 170 171 if(digitCount < requiredDigitCount) { 172 ema.addExecutionError(ExecutionErrors.PasswordRequiredDigitCountNotMet.name(), requiredDigitCount); 173 } 174 } 175 176 if(requiredLetterCount != null) { 177 if(letterCount < requiredLetterCount) { 178 ema.addExecutionError(ExecutionErrors.PasswordRequiredLetterCountNotMet.name(), requiredLetterCount); 179 } 180 } 181 182 if(requiredUpperCaseCount != null) { 183 if(upperCaseCount < requiredUpperCaseCount) { 184 ema.addExecutionError(ExecutionErrors.PasswordRequiredUpperCaseCountNotMet.name(), requiredUpperCaseCount); 185 } 186 } 187 188 if(requiredLowerCaseCount != null) { 189 if(lowerCaseCount < requiredLowerCaseCount) { 190 ema.addExecutionError(ExecutionErrors.PasswordRequiredLowerCaseCountNotMet.name(), requiredLowerCaseCount); 191 } 192 } 193 194 if(maximumRepeated != null) { 195 if(maxRepeat < maximumRepeated) { 196 ema.addExecutionError(ExecutionErrors.PasswordMaximumRepeatedExceeded.name(), maximumRepeated); 197 } 198 } 199 200 if(minimumCharacterTypes != null) { 201 var characterTypes = types.size(); 202 203 if(characterTypes < minimumCharacterTypes) { 204 ema.addExecutionError(ExecutionErrors.PasswordMinimumCharacterTypesNotMet.name(), minimumCharacterTypes); 205 } 206 } 207 } 208 209 public PartyTypePasswordStringPolicy checkStringPassword(final Session session, final UserVisit userVisit, final ExecutionErrorAccumulator ema, 210 final PartyType partyType, final UserLoginPassword ulp, final UserLoginPasswordStringValue ulpsv, final String password) { 211 var policy = partyControl.getPartyTypePasswordStringPolicy(partyType); 212 213 if(policy != null) { 214 var policyDetail = policy.getLastDetail(); 215 216 if(ulp != null) { 217 checkPasswordHistory(ema, policyDetail, ulp, password); 218 } 219 220 if(ulpsv != null) { 221 checkAllowChange(ema, policyDetail); 222 checkMinimumPasswordLifetime(session, userVisit, ema, policyDetail, ulpsv); 223 } 224 225 checkLength(ema, policyDetail, password); 226 checkCharacterTypes(ema, policyDetail, password); 227 } 228 229 return policy; 230 } 231 232 public PartyTypePasswordStringPolicy checkStringPassword(final Session session, final UserVisit userVisit, final ExecutionErrorAccumulator ema, 233 final Party party, final UserLoginPassword ulp, final UserLoginPasswordStringValue ulpsv, final String password) { 234 var partyType = party.getLastDetail().getPartyType(); 235 236 return checkStringPassword(session, userVisit, ema, partyType, ulp, ulpsv, password); 237 } 238 239}