package io.cattle.platform.framework.secret;
import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.core.addon.SecretReference;
import io.cattle.platform.core.constants.HostConstants;
import io.cattle.platform.core.dao.SecretDao;
import io.cattle.platform.core.model.Host;
import io.cattle.platform.core.model.Secret;
import io.cattle.platform.json.JsonMapper;
import io.cattle.platform.object.util.DataAccessor;
import io.cattle.platform.ssh.common.SshKeyGen;
import io.cattle.platform.token.impl.RSAKeyProvider;
import io.cattle.platform.token.impl.RSAPrivateKeyHolder;
import io.cattle.platform.util.type.CollectionUtils;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.fluent.Request;
import org.apache.http.entity.ContentType;
import com.netflix.config.DynamicStringProperty;
public class SecretsServiceImpl implements SecretsService {
private static final String CREATE_PATH = "/v1-secrets/secrets/create";
private static final String PURGE_PATH = "/v1-secrets/secrets/purge";
private static final String REWRAP = "/v1-secrets/secrets/rewrap";
private static final String BULK_PATH = "/v1-secrets/secrets/rewrap?action=bulk";
private static final DynamicStringProperty SECRETS_URL = ArchaiusUtil.getString("secrets.url");
private static final DynamicStringProperty SECRETS_BACKEND = ArchaiusUtil.getString("secrets.backend");
@Inject
SecretDao secretDao;
@Inject
JsonMapper jsonMapper;
@Inject
RSAKeyProvider rsaKeyProvider;
@Override
public String encrypt(long accountId, String value) throws IOException {
Map<String, Object> input = new HashMap<>();
input.put("backend", SECRETS_BACKEND.get());
input.put("clearText", value);
input.put("keyName", SECRETS_KEY_NAME.get());
return Request.Post(SECRETS_URL.get() + CREATE_PATH)
.bodyString(jsonMapper.writeValueAsString(input), ContentType.APPLICATION_JSON)
.execute().handleResponse(new ResponseHandler<String>() {
@Override
public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 300) {
throw new IOException("Failed to encrypt secret :" + response.getStatusLine().getReasonPhrase());
}
return IOUtils.toString(response.getEntity().getContent());
}
});
}
@Override
public void delete(long accountId, String value) throws IOException {
Request.Post(SECRETS_URL.get() + PURGE_PATH).bodyString(value, ContentType.APPLICATION_JSON).execute().handleResponse((response) -> {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 300 && statusCode != 404) {
throw new IOException("Failed to delete secret :" + response.getStatusLine().getReasonPhrase());
}
return IOUtils.toString(response.getEntity().getContent());
});
}
protected Map<Long, String> getValues(Collection<Secret> secrets, Host host) throws IOException {
List<Secret> secretsList = new ArrayList<>(secrets);
Map<Long, String> result = new HashMap<>();
Map<String, Object> hostInfo = DataAccessor.fieldMap(host, HostConstants.FIELD_INFO);
Object rewrapKey = CollectionUtils.getNestedValue(hostInfo, "hostKey", "data");
Map<String, Object> input = new HashMap<>();
input.put("data", toData(secretsList));
input.put("rewrapKey", rewrapKey);
Map<String, Object> response = Request.Post(SECRETS_URL.get() + BULK_PATH).
bodyString(jsonMapper.writeValueAsString(input), ContentType.APPLICATION_JSON)
.execute().handleResponse(new ResponseHandler<Map<String, Object>>() {
@Override
public Map<String, Object> handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 300) {
throw new IOException("Failed to rewrap secret :" + response.getStatusLine().getReasonPhrase());
}
return jsonMapper.readValue(response.getEntity().getContent());
}
});
List<?> wrapped = CollectionUtils.toList(response.get("data"));
for (int i = 0; i < secretsList.size(); i++) {
Secret secret = secretsList.get(i);
result.put(secret.getId(), CollectionUtils.toMap(wrapped.get(i)).get("rewrapText").toString());
}
return result;
}
protected List<Object> toData(List<Secret> secrets) throws IOException {
List<Object> result = new ArrayList<>(secrets.size());
for (Secret s : secrets) {
result.add(jsonMapper.readValue(s.getValue()));
}
return result;
}
@Override
public List<SecretValue> getValues(List<SecretReference> refs, Host host) throws IOException {
Map<Long, Secret> secrets = secretDao.getSecrets(refs);
Map<Long, String> values = getValues(secrets.values(), host);
List<SecretValue> result = new ArrayList<>();
for (SecretReference ref : refs) {
Secret secret = secrets.get(ref.getSecretId());
if (secret == null) {
continue;
}
SecretValue value = new SecretValue(ref, secret, values.get(secret.getId()));
result.add(value);
}
return result;
}
@Override
public String decrypt(long accountId, String value) throws Exception {
RSAPrivateKeyHolder holder = rsaKeyProvider.getPrivateKey();
PublicKey publicKey = rsaKeyProvider.getPublicKeys().get(holder.getKeyId());
String encoded = SshKeyGen.toPEM(publicKey);
Map<String, Object> input = jsonMapper.readValue(value);
input.put("rewrapKey", encoded);
String encrypted = Request.Post(SECRETS_URL.get() + REWRAP)
.bodyString(jsonMapper.writeValueAsString(input), ContentType.APPLICATION_JSON)
.execute().handleResponse(new ResponseHandler<String>() {
@Override
public String handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 300) {
throw new IOException("Failed to rewrap secret :" + response.getStatusLine().getReasonPhrase());
}
return IOUtils.toString(response.getEntity().getContent());
}
});
return unwrap(holder.getKey(), encrypted);
}
protected String unwrap(PrivateKey privateKey, String encrypted) throws Exception {
Map<String, Object> rewrappedSecrect = jsonMapper.readValue(encrypted);
Map<String, Object> encryptedObject = jsonMapper.readValue(Base64.decodeBase64((String) rewrappedSecrect.get("rewrapText")));
Map<String, Object> encryptedText = jsonMapper.readValue((String)encryptedObject.get("encryptedText"));
@SuppressWarnings("unchecked")
Map<String, Object> encryptedKey = (Map<String, Object>)encryptedObject.get("encryptedKey");
byte[] encryptionKey = getEncryptionKey(privateKey, encryptedKey);
byte[] nonce = Base64.decodeBase64((String)encryptedText.get("Nonce"));
byte[] cipherText = Base64.decodeBase64((String)encryptedText.get("CipherText"));
byte[] decrypted = decrypt(cipherText, encryptionKey, nonce);
return new String(decrypted);
}
protected byte[] getEncryptionKey(PrivateKey key, Map<String, Object> encryptedKey) throws Exception {
Cipher c = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding", "BC");
c.init(Cipher.DECRYPT_MODE, key);
return c.doFinal(Base64.decodeBase64((String) encryptedKey.get("encryptedText")));
}
protected byte[] decrypt(byte[] cipherText, byte[] key, byte[] nonce) throws Exception {
Cipher c = Cipher.getInstance("AES/GCM/NoPadding", "BC");
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(nonce));
return c.doFinal(cipherText);
}
}