BigQueryConfig.java
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 privateKeyPkcs8throws 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 }