View Javadoc
1   package cn.home1.cloud.config.server.security;
2   
3   import static com.google.common.base.Preconditions.checkArgument;
4   import static java.lang.Boolean.FALSE;
5   import static org.apache.commons.lang3.RandomStringUtils.random;
6   import static org.apache.commons.lang3.StringUtils.isNotBlank;
7   import static org.apache.commons.lang3.StringUtils.isNotEmpty;
8   import static org.joda.time.DateTime.now;
9   
10  import com.google.common.collect.ImmutableMap;
11  
12  import com.auth0.jwt.JWT;
13  import com.auth0.jwt.JWTVerifier;
14  import com.auth0.jwt.algorithms.Algorithm;
15  
16  import lombok.Setter;
17  import lombok.SneakyThrows;
18  import lombok.extern.slf4j.Slf4j;
19  
20  import org.springframework.beans.factory.annotation.Autowired;
21  import org.springframework.beans.factory.annotation.Value;
22  import org.springframework.cloud.config.server.encryption.TextEncryptorLocator;
23  import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
24  import org.springframework.security.crypto.encrypt.TextEncryptor;
25  import org.springframework.security.crypto.password.PasswordEncoder;
26  
27  import java.security.SecureRandom;
28  import java.util.Iterator;
29  import java.util.regex.Pattern;
30  
31  import javax.annotation.PostConstruct;
32  
33  @Slf4j
34  public class ConfigSecurity {
35  
36      static final String TOKEN_PREFIX = "{token}";
37      private static final int BCRYPT_STRENGTH = -1;
38      private static final Pattern CONCAT_PATTERN = Pattern.compile(":");
39      private static final String TOKEN_CLAIM = "encrypted";
40      private static final int TOKEN_EXPIRE_DAYS = 365 * 5;
41      private static final String TOKEN_ISSUER = "config-server";
42      private final PasswordEncoder passwordEncoder;
43  
44      @Setter
45      private TextEncryptor encryptor;
46  
47      private Algorithm hmacAlgorithm;
48  
49      @Setter
50      @Value("${spring.cloud.config.encrypt.hmac-secret:secret}")
51      private String hmacSecret;
52  
53      private JWTVerifier hmacVerifier;
54  
55      @Setter
56      @Value("${security.basic.enabled:true}")
57      private Boolean securityEnabled;
58  
59      public ConfigSecurity() {
60          this.passwordEncoder = new BCryptPasswordEncoder(BCRYPT_STRENGTH);
61      }
62  
63      static String decryptProperty(final String value, final TextEncryptor encryptor) {
64          final String result;
65          if (isNotBlank(value) && value.startsWith("{cipher}")) {
66              final String base64 = value.replaceAll("\\{[^}]+\\}", "");
67              result = encryptor.decrypt(base64);
68          } else {
69              result = value;
70          }
71          return result;
72      }
73  
74      /**
75       * By default, this is a {@link org.springframework.cloud.config.server.encryption.KeyStoreTextEncryptorLocator} instance.
76       */
77      @Autowired
78      public void setEncryptorLocator(final TextEncryptorLocator encryptorLocator) {
79          this.encryptor = encryptorLocator.locate(ImmutableMap.of());
80      }
81  
82      @PostConstruct
83      @SneakyThrows
84      public void init() {
85          this.hmacAlgorithm = Algorithm.HMAC256(this.hmacSecret);
86          this.hmacVerifier = JWT.require(this.hmacAlgorithm)
87              .withIssuer(TOKEN_ISSUER)
88              .build(); //Reusable verifier instance
89      }
90  
91      /**
92       * Generate a password (token) valid only for given application to access a given parent
93       *
94       * @param application       from child config
95       * @param parentApplication from child config
96       * @param parentPassword    from child config
97       * @return token valid for application only
98       */
99      public String encryptParentPassword(final String application, final String parentApplication, final String parentPassword) {
100         //if (!this.securityEnabled) {
101         //  return "";
102         //}
103 
104         checkArgument(isNotBlank(application), "blank application");
105         checkArgument(isNotBlank(application), "blank parentApplication");
106         checkArgument(isNotBlank(application), "blank parentPassword");
107 
108         // digest/hash
109         // random string (length 16)
110         final String randomString = random(16, 0, 0, true, true, null, new SecureRandom());
111         final String encodedApplication = this.passwordEncoder.encode(application);
112         final String encodedParentApplication = this.passwordEncoder.encode(parentApplication);
113         final String encodedParentPassword = this.passwordEncoder.encode(parentPassword);
114 
115         // concat
116         final String plainText = randomString + ":" + encodedApplication + ":" + encodedParentApplication + ":" + encodedParentPassword;
117 
118         // encrypt
119         final String encrypted = this.encryptor.encrypt(plainText);
120 
121         // sign
122         final String token = JWT.create()
123             .withIssuer(TOKEN_ISSUER)
124             .withClaim(TOKEN_CLAIM, encrypted)
125             .withExpiresAt(now().plusDays(TOKEN_EXPIRE_DAYS).toDate())
126             .sign(this.hmacAlgorithm);
127 
128         log.info("Granted parent ({}) config access for application '{}', token: '{}'.", parentApplication, application, token);
129 
130         return TOKEN_PREFIX + token;
131     }
132 
133     /**
134      * Verify privilege
135      *
136      * @param application            from context (URL path)
137      * @param parentApplication      from child config
138      * @param token                  from child config
139      * @param expectedParentPassword from parent config (may need to decrypt before verify)
140      * @return whether token, application in token and password in token valid
141      */
142     public Boolean verifyParentPassword(final String application, final String parentApplication, final String token, final String expectedParentPassword) {
143         final Boolean result;
144         if (!this.securityEnabled) {
145             result = isNotEmpty(application) && isNotEmpty(parentApplication);
146         } else {
147             final String rawParentPassword = this.decryptProperty(expectedParentPassword);
148 
149             if (isNotEmpty(application) && isNotEmpty(token)) {
150                 if (token.startsWith(TOKEN_PREFIX)) {
151                     final String rawToken = token.replace(TOKEN_PREFIX, "");
152                     // verify signature
153                     final String encrypted = this.hmacVerifier.verify(rawToken).getClaim(TOKEN_CLAIM).asString();
154 
155                     // decrypt
156                     final String plainText = this.encryptor.decrypt(encrypted);
157 
158                     // split
159                     final Iterator<String> parts = CONCAT_PATTERN.splitAsStream(plainText).iterator();
160                     final String random = parts.next();
161                     final String encodedApplication = parts.next();
162                     final String encodedParentApplication = parts.next();
163                     final String encodedParentPassword = parts.next();
164 
165                     try {
166                         result = this.passwordEncoder.matches(application, encodedApplication) &&
167                             this.passwordEncoder.matches(parentApplication, encodedParentApplication) &&
168                             this.passwordEncoder.matches(rawParentPassword, encodedParentPassword);
169                     } catch (final Exception ignored) {
170                         return FALSE;
171                     }
172                 } else {
173                     final String rawToken = this.decryptProperty(token);
174                     result = this.isPasswordMatch(rawParentPassword, rawToken);
175                 }
176             } else {
177                 if (isNotEmpty(application) && isNotEmpty(parentApplication)) {
178                     result = this.isPasswordMatch(rawParentPassword, token);
179                 } else {
180                     result = FALSE;
181                 }
182             }
183         }
184         return result;
185     }
186 
187     Boolean isPasswordMatch(final String expected, final String actual) {
188         return (expected != null ? expected : "").equals(actual != null ? actual : "");
189     }
190 
191     String decryptProperty(final String value) {
192         return decryptProperty(value, this.encryptor);
193     }
194 }