IntelliJCacheAccessor.java
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.identity.implementation;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;
import com.azure.identity.AzureAuthorityHosts;
import com.azure.identity.CredentialUnavailableException;
import com.azure.identity.implementation.intellij.IntelliJKdbxDatabase;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.aad.msal4jextensions.persistence.mac.KeyChainAccessor;
import com.sun.jna.Platform;
import com.sun.jna.platform.win32.Crypt32Util;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* This class accesses IntelliJ Azure Tools credentials cache via JNA.
*/
public class IntelliJCacheAccessor {
private final ClientLogger logger = new ClientLogger(IntelliJCacheAccessor.class);
private final String keePassDatabasePath;
private static final byte[] CRYPTO_KEY = new byte[] {0x50, 0x72, 0x6f, 0x78, 0x79, 0x20, 0x43, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x20, 0x53, 0x65, 0x63};
private static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper();
private static final ObjectMapper DONT_FAIL_ON_UNKNOWN_PROPERTIES_MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private static final Pattern CACHED_AUTH_RESULT_PATTERN = Pattern.compile("cachedAuthResult@");
/**
* Creates an instance of {@link IntelliJCacheAccessor}
*
* @param keePassDatabasePath the KeePass database path.
*/
public IntelliJCacheAccessor(String keePassDatabasePath) {
this.keePassDatabasePath = keePassDatabasePath;
}
private List<String> getAzureToolsforIntelliJPluginConfigPaths() {
return Arrays.asList(Paths.get(System.getProperty("user.home"), "AzureToolsForIntelliJ").toString(),
Paths.get(System.getProperty("user.home"), ".AzureToolsForIntelliJ").toString());
}
/**
* Get the Device Code credential details of Azure Tools plugin in the IntelliJ IDE.
*
* @return the {@link JsonNode} holding the authentication details.
* @throws IOException If an I/O error occurs.
*/
public JsonNode getDeviceCodeCredentials() throws IOException {
if (Platform.isMac()) {
KeyChainAccessor accessor = new KeyChainAccessor(null, "ADAuthManager", "cachedAuthResult");
String jsonCred = new String(accessor.read(), StandardCharsets.UTF_8);
return DEFAULT_MAPPER.readTree(jsonCred);
} else if (Platform.isLinux()) {
LinuxKeyRingAccessor accessor = new LinuxKeyRingAccessor(
"com.intellij.credentialStore.Credential",
"service", "ADAuthManager",
"account", "cachedAuthResult");
String jsonCred = new String(accessor.read(), StandardCharsets.UTF_8);
if (jsonCred.startsWith("cachedAuthResult@")) {
jsonCred = CACHED_AUTH_RESULT_PATTERN.matcher(jsonCred).replaceFirst("");
}
return DEFAULT_MAPPER.readTree(jsonCred);
} else if (Platform.isWindows()) {
return getCredentialFromKdbx();
} else {
throw logger.logExceptionAsError(new RuntimeException(String.format("OS %s Platform not supported.",
Platform.getOSType())));
}
}
/**
* Get the Service Principal credential details of Azure Tools plugin in the IntelliJ IDE.
*
* @param credFilePath the file path holding authentication details
* @return the {@link HashMap} holding auth details.
* @throws IOException if an error is countered while reading the credential file.
*/
public Map<String, String> getIntellijServicePrincipalDetails(String credFilePath) throws IOException {
BufferedReader reader = null;
HashMap<String, String> servicePrincipalDetails = new HashMap<>(8);
try {
reader = new BufferedReader(new FileReader(credFilePath));
String line = reader.readLine();
while (line != null) {
String[] split = line.split("=");
split[1] = split[1].replace("\\", "");
servicePrincipalDetails.put(split[0], split[1]);
// read next line
line = reader.readLine();
}
} finally {
if (reader != null) {
reader.close();
}
}
return servicePrincipalDetails;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private JsonNode getCredentialFromKdbx() throws IOException {
if (CoreUtils.isNullOrEmpty(keePassDatabasePath)) {
throw logger.logExceptionAsError(
new CredentialUnavailableException("The KeePass database path is either empty or not configured."
+ " Please configure it on the builder. It is required to use "
+ "IntelliJ credential on the windows platform."));
}
String extractedpwd = getKdbxPassword();
SecretKeySpec key = new SecretKeySpec(CRYPTO_KEY, "AES");
String password;
byte[] dataToDecrypt = Crypt32Util.cryptUnprotectData(Base64.getDecoder().decode(extractedpwd));
ByteBuffer decryptBuffer = ByteBuffer.wrap(dataToDecrypt);
Cipher cipher;
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
int ivLen = decryptBuffer.getInt();
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(dataToDecrypt, decryptBuffer.position(), ivLen));
int dataOffset = decryptBuffer.position() + ivLen;
byte[] decrypted = cipher.doFinal(dataToDecrypt, dataOffset, dataToDecrypt.length - dataOffset);
password = new String(decrypted, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw logger.logExceptionAsError(new RuntimeException("Unable to access cache.", e));
}
try (InputStream inputStream = new FileInputStream(keePassDatabasePath)) {
IntelliJKdbxDatabase kdbxDatabase = IntelliJKdbxDatabase.parse(inputStream, password);
String jsonToken = kdbxDatabase.getDatabaseEntryValue("ADAuthManager");
if (CoreUtils.isNullOrEmpty(jsonToken)) {
throw logger.logExceptionAsError(new CredentialUnavailableException("No credentials found in the cache."
+ " Please login with IntelliJ Azure Tools plugin in the IDE."));
}
return DEFAULT_MAPPER.readTree(jsonToken);
} catch (Exception e) {
throw logger.logExceptionAsError(new RuntimeException("Failed to read KeePass database.", e));
}
}
private String getKdbxPassword() throws IOException {
String passwordFilePath = new File(keePassDatabasePath).getParent() + File.separator + "c.pwd";
String extractedpwd = "";
try (BufferedReader reader = new BufferedReader(new FileReader(passwordFilePath))) {
String line = reader.readLine();
while (line != null) {
if (line.contains("value")) {
String[] tokens = line.split(" ");
if (tokens.length == 3) {
extractedpwd = tokens[2];
break;
} else {
throw logger.logExceptionAsError(new RuntimeException("Password not found in the file."));
}
}
line = reader.readLine();
}
}
return extractedpwd;
}
/**
* Get the auth host of the specified {@code azureEnvironment}.
* @param azureEnvironment the specified Azure Environment
* @return the auth host.
*/
public String getAzureAuthHost(String azureEnvironment) {
switch (azureEnvironment) {
case "GLOBAL":
return AzureAuthorityHosts.AZURE_PUBLIC_CLOUD;
case "CHINA":
return AzureAuthorityHosts.AZURE_CHINA;
case "GERMAN":
return AzureAuthorityHosts.AZURE_GERMANY;
case "US_GOVERNMENT":
return AzureAuthorityHosts.AZURE_GOVERNMENT;
default:
return AzureAuthorityHosts.AZURE_PUBLIC_CLOUD;
}
}
/**
* Parse the auth details of the specified file.
* @param file the file input;
* @return the parsed {@link IntelliJAuthMethodDetails} from the file input.
* @throws IOException when invalid file path is specified.
*/
public IntelliJAuthMethodDetails parseAuthMethodDetails(File file) throws IOException {
return DONT_FAIL_ON_UNKNOWN_PROPERTIES_MAPPER.readValue(file, IntelliJAuthMethodDetails.class);
}
/**
* Get the current authentication method details of Azure Tools plugin in IntelliJ IDE.
*
* @return the {@link IntelliJAuthMethodDetails}
* @throws IOException if an error is encountered while reading the auth details file.
*/
public IntelliJAuthMethodDetails getAuthDetailsIfAvailable() throws IOException {
File authFile = null;
for (String metadataPath : getAzureToolsforIntelliJPluginConfigPaths()) {
String authMethodDetailsPath =
Paths.get(metadataPath, "AuthMethodDetails.json").toString();
authFile = new File(authMethodDetailsPath);
if (authFile.exists()) {
break;
}
}
if (authFile == null || !authFile.exists()) {
return null;
}
IntelliJAuthMethodDetails authMethodDetails = parseAuthMethodDetails(authFile);
String authType = authMethodDetails.getAuthMethod();
if (CoreUtils.isNullOrEmpty(authType)) {
return null;
}
if ("SP".equalsIgnoreCase(authType)) {
if (CoreUtils.isNullOrEmpty(authMethodDetails.getCredFilePath())) {
return null;
}
} else if ("DC".equalsIgnoreCase(authType)) {
if (CoreUtils.isNullOrEmpty(authMethodDetails.getAccountEmail())) {
return null;
}
}
return authMethodDetails;
}
}