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}