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("${spring.security.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
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();
89 }
90
91
92
93
94
95
96
97
98
99 public String encryptParentPassword(final String application, final String parentApplication, final String parentPassword) {
100
101
102
103
104 checkArgument(isNotBlank(application), "blank application");
105 checkArgument(isNotBlank(application), "blank parentApplication");
106 checkArgument(isNotBlank(application), "blank parentPassword");
107
108
109
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
116 final String plainText = randomString + ":" + encodedApplication + ":" + encodedParentApplication + ":" + encodedParentPassword;
117
118
119 final String encrypted = this.encryptor.encrypt(plainText);
120
121
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
135
136
137
138
139
140
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
153 final String encrypted = this.hmacVerifier.verify(rawToken).getClaim(TOKEN_CLAIM).asString();
154
155
156 final String plainText = this.encryptor.decrypt(encrypted);
157
158
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 }