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}