View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.core.spring.policy;
20  
21  import java.io.UnsupportedEncodingException;
22  import java.net.URI;
23  import java.security.InvalidKeyException;
24  import java.security.NoSuchAlgorithmException;
25  import java.util.Optional;
26  import java.util.stream.Stream;
27  import javax.crypto.BadPaddingException;
28  import javax.crypto.IllegalBlockSizeException;
29  import javax.crypto.NoSuchPaddingException;
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.syncope.common.lib.policy.HaveIBeenPwnedPasswordRuleConf;
32  import org.apache.syncope.common.lib.policy.PasswordRuleConf;
33  import org.apache.syncope.common.lib.types.CipherAlgorithm;
34  import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
35  import org.apache.syncope.core.persistence.api.entity.user.User;
36  import org.apache.syncope.core.provisioning.api.rules.PasswordRule;
37  import org.apache.syncope.core.provisioning.api.rules.PasswordRuleConfClass;
38  import org.apache.syncope.core.spring.security.Encryptor;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  import org.springframework.http.HttpEntity;
42  import org.springframework.http.HttpHeaders;
43  import org.springframework.http.HttpMethod;
44  import org.springframework.http.ResponseEntity;
45  import org.springframework.transaction.annotation.Transactional;
46  import org.springframework.web.client.HttpStatusCodeException;
47  import org.springframework.web.client.RestTemplate;
48  
49  @PasswordRuleConfClass(HaveIBeenPwnedPasswordRuleConf.class)
50  public class HaveIBeenPwnedPasswordRule implements PasswordRule {
51  
52      protected static final Logger LOG = LoggerFactory.getLogger(HaveIBeenPwnedPasswordRule.class);
53  
54      private static final Encryptor ENCRYPTOR = Encryptor.getInstance();
55  
56      private HaveIBeenPwnedPasswordRuleConf conf;
57  
58      @Override
59      public HaveIBeenPwnedPasswordRuleConf getConf() {
60          return conf;
61      }
62  
63      @Override
64      public void setConf(final PasswordRuleConf conf) {
65          if (conf instanceof HaveIBeenPwnedPasswordRuleConf) {
66              this.conf = (HaveIBeenPwnedPasswordRuleConf) conf;
67          } else {
68              throw new IllegalArgumentException(
69                      HaveIBeenPwnedPasswordRuleConf.class.getName() + " expected, got " + conf.getClass().getName());
70          }
71      }
72  
73      protected void enforce(final String clearPassword) {
74          try {
75              String sha1 = ENCRYPTOR.encode(clearPassword, CipherAlgorithm.SHA1);
76  
77              HttpHeaders headers = new HttpHeaders();
78              headers.set(HttpHeaders.USER_AGENT, "Apache Syncope");
79              ResponseEntity<String> response = new RestTemplate().exchange(
80                      URI.create("https://api.pwnedpasswords.com/range/" + sha1.substring(0, 5)),
81                      HttpMethod.GET,
82                      new HttpEntity<>(null, headers),
83                      String.class);
84              if (StringUtils.isNotBlank(response.getBody())) {
85                  if (Stream.of(response.getBody().split("\\n")).anyMatch(line
86                          -> sha1.equals(sha1.substring(0, 5) + StringUtils.substringBefore(line, ":")))) {
87  
88                      throw new PasswordPolicyException("Password pwned");
89                  }
90              }
91          } catch (UnsupportedEncodingException | InvalidKeyException | NoSuchAlgorithmException
92                  | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException e) {
93  
94              LOG.error("Could not encode the password value as SHA1", e);
95          } catch (HttpStatusCodeException e) {
96              LOG.error("Error while contacting the PwnedPasswords service", e);
97          }
98      }
99  
100     @Override
101     public void enforce(final String username, final String clearPassword) {
102         Optional.ofNullable(clearPassword).ifPresent(this::enforce);
103     }
104 
105     @Transactional(readOnly = true)
106     @Override
107     public void enforce(final User user, final String clearPassword) {
108         Optional.ofNullable(clearPassword).ifPresent(this::enforce);
109     }
110 
111     @Transactional(readOnly = true)
112     @Override
113     public void enforce(final LinkedAccount account) {
114         if (account.getPassword() != null) {
115             String clearPassword = null;
116             if (account.canDecodeSecrets()) {
117                 try {
118                     clearPassword = ENCRYPTOR.decode(account.getPassword(), account.getCipherAlgorithm());
119                 } catch (Exception e) {
120                     LOG.error("Could not decode password for {}", account, e);
121                 }
122             }
123 
124             if (clearPassword != null) {
125                 enforce(clearPassword);
126             }
127         }
128     }
129 }