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