001 /*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2020-2022 Agorapulse.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 * https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018 package com.agorapulse.micronaut.bigquery.aws;
019
020 import com.amazonaws.services.kms.AWSKMS;
021 import com.amazonaws.services.kms.AWSKMSClientBuilder;
022 import com.amazonaws.services.kms.model.DecryptRequest;
023 import com.amazonaws.services.kms.model.DecryptResult;
024 import com.amazonaws.util.Base64;
025 import com.google.api.client.util.PemReader;
026 import com.google.api.client.util.SecurityUtils;
027 import com.google.auth.oauth2.GoogleCredentials;
028 import com.google.auth.oauth2.ServiceAccountCredentials;
029 import com.google.cloud.bigquery.BigQuery;
030 import com.google.cloud.bigquery.BigQueryOptions;
031 import io.micronaut.context.annotation.ConfigurationProperties;
032 import io.micronaut.context.env.Environment;
033
034 import java.io.IOException;
035 import java.io.Reader;
036 import java.io.StringReader;
037 import java.net.URI;
038 import java.nio.ByteBuffer;
039 import java.nio.charset.StandardCharsets;
040 import java.security.KeyFactory;
041 import java.security.NoSuchAlgorithmException;
042 import java.security.PrivateKey;
043 import java.security.spec.InvalidKeySpecException;
044 import java.security.spec.PKCS8EncodedKeySpec;
045
046 @ConfigurationProperties("bigquery.credentials")
047 public class BigQueryConfig {
048
049 private final String decryptedPrivateKey;
050 private String projectId;
051 private String privateKeyId;
052 private String clientEmail;
053 private String clientId;
054
055 public BigQueryConfig(Environment environment) {
056 this.decryptedPrivateKey = decryptKMSIfRequired(environment.get("bigquery.credentials.private-key", String.class)
057 .orElse(""))
058 .replace("\\n", "\n");
059 }
060
061 public String getProjectId() {
062 return projectId;
063 }
064
065 public void setProjectId(String projectId) {
066 this.projectId = projectId;
067 }
068
069 public String getPrivateKeyId() {
070 return privateKeyId;
071 }
072
073 public void setPrivateKeyId(String privateKeyId) {
074 this.privateKeyId = privateKeyId;
075 }
076
077 public String getClientEmail() {
078 return clientEmail;
079 }
080
081 public void setClientEmail(String clientEmail) {
082 this.clientEmail = clientEmail;
083 }
084
085 public String getClientId() {
086 return clientId;
087 }
088
089 public void setClientId(String clientId) {
090 this.clientId = clientId;
091 }
092
093 public static String decryptKMSIfRequired(String encrypted) {
094 // for local development
095 if (encrypted.contains("-----BEGIN PRIVATE KEY-----")) {
096 return encrypted;
097 }
098
099
100 AWSKMS kms = AWSKMSClientBuilder.standard().build();
101
102 byte[] cipherBytes = Base64.decode(encrypted);
103
104 DecryptRequest request = new DecryptRequest().withCiphertextBlob(ByteBuffer.wrap(cipherBytes));
105 DecryptResult response = kms.decrypt(request);
106
107 return new String(response.getPlaintext().array(), StandardCharsets.UTF_8);
108 }
109
110 public BigQuery createInstance() {
111 try {
112 URI tokenUri = new URI("https://oauth2.googleapis.com/token");
113
114 GoogleCredentials credentials;
115
116 credentials = ServiceAccountCredentials.newBuilder()
117 .setClientId(clientId)
118 .setClientEmail(clientEmail)
119 .setPrivateKey(privateKeyFromPkcs8(decryptedPrivateKey))
120 .setPrivateKeyId(privateKeyId)
121 .setTokenServerUri(tokenUri)
122 .setProjectId(projectId)
123 .build();
124
125
126 return BigQueryOptions.newBuilder().setCredentials(credentials).build().getService();
127 } catch (Exception e) {
128 throw new IllegalArgumentException("Impossible to instantiate a service account with current parameters", e);
129 }
130 }
131
132 /** Helper to convert from a PKCS#8 String to an RSA private key */
133 private static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
134 // from ServiceAccountCredentials
135 Reader reader = new StringReader(privateKeyPkcs8);
136 PemReader.Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY");
137 if (section == null) {
138 throw new IOException("Invalid PKCS#8 data.");
139 }
140 byte[] bytes = section.getBase64DecodedBytes();
141 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
142 Exception unexpectedException;
143 try {
144 KeyFactory keyFactory = SecurityUtils.getRsaKeyFactory();
145 return keyFactory.generatePrivate(keySpec);
146 } catch (NoSuchAlgorithmException | InvalidKeySpecException exception) {
147 unexpectedException = exception;
148 }
149 throw new IOException("Unexpected exception reading PKCS#8 data", unexpectedException);
150 }
151 }
|