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