001/*
002 * Copyright 2024 Emmanuel Bourg
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package net.jsign.jca;
018
019import java.io.File;
020import java.io.IOException;
021import java.nio.file.Files;
022import java.security.GeneralSecurityException;
023import java.security.KeyException;
024import java.security.KeyFactory;
025import java.security.PrivateKey;
026import java.security.PublicKey;
027import java.security.interfaces.RSAPrivateCrtKey;
028import java.security.spec.RSAPublicKeySpec;
029import java.util.Properties;
030import java.util.stream.Collectors;
031import java.util.stream.IntStream;
032
033import net.jsign.DigestAlgorithm;
034import net.jsign.PrivateKeyUtils;
035
036/**
037 * Oracle Cloud credentials loaded from the <code>.oci/config</code> file or from the environment variables.
038 * 
039 * @since 7.0
040 */
041public class OracleCloudCredentials {
042
043    private String user;
044    private String tenancy;
045    private String region;
046    private String keyfile;
047    private String fingerprint;
048    private String passphrase;
049    private PrivateKey privateKey;
050
051    public String getUser() {
052        return user;
053    }
054
055    public String getTenancy() {
056        return tenancy;
057    }
058
059    public String getRegion() {
060        return region;
061    }
062
063    public String getKeyfile() {
064        return keyfile;
065    }
066
067    public String getFingerprint() {
068        if (fingerprint == null) {
069            try {
070                fingerprint = getFingerprint(getPrivateKey());
071            } catch (GeneralSecurityException e) {
072                throw new RuntimeException("Unable to compute the OCI API key fingerprint", e);
073            }
074        }
075        return fingerprint;
076    }
077
078    /**
079     * Compute the fingerprint of the specified key (i.e. the MD5 hash of the public key in DER format)
080     * @see <a href="https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm#four">How to Get the Key's Fingerprint</a>
081     */
082    String getFingerprint(PrivateKey privateKey) throws GeneralSecurityException {
083        RSAPrivateCrtKey key = (RSAPrivateCrtKey) privateKey;
084        RSAPublicKeySpec publicKeySpec = new java.security.spec.RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
085        PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(publicKeySpec);
086        byte[] digest = DigestAlgorithm.MD5.getMessageDigest().digest(publicKey.getEncoded());
087        return IntStream.range(0, digest.length).mapToObj(i -> String.format("%02x", digest[i])).collect(Collectors.joining(":"));
088    }
089
090    public String getPassphrase() {
091        return passphrase;
092    }
093
094    public void setPassphrase(String passphrase) {
095        this.passphrase = passphrase;
096    }
097
098    public String getKeyId() {
099        return getTenancy() + "/" + getUser() + "/" + getFingerprint();
100    }
101
102    PrivateKey getPrivateKey() {
103        if (privateKey == null) {
104            try {
105                privateKey = PrivateKeyUtils.load(new File(getKeyfile()), getPassphrase());
106            } catch (KeyException e) {
107                throw new RuntimeException("Unable to load the private key", e);
108            }
109        }
110        return privateKey;
111    }
112
113    /**
114     * Loads the credentials from the specified file.
115     *
116     * @param file    the configuration file (null for the default location)
117     * @param profile the name of the profile (null for the default profile)
118     */
119    public void load(File file, String profile) throws IOException {
120        if (file == null) {
121            file = getConfigFile();
122        }
123        if (profile == null) {
124            profile = getDefaultProfile();
125        }
126
127        Properties properties = new Properties();
128
129        // parse le lines of the file
130        boolean profileFound = false;
131        for (String line : Files.readAllLines(file.toPath())) {
132            if (profileFound && line.startsWith("[")) {
133                break; // end of the profile
134            }
135
136            if (line.equals("[" + profile + "]")) {
137                profileFound = true;
138                continue;
139            }
140
141            if (profileFound) {
142                String[] elements = line.split("=", 2);
143                if (elements.length == 2) {
144                    properties.setProperty(elements[0].trim(), elements[1].trim());
145                }
146            }
147        }
148
149        if (!profileFound) {
150            throw new IOException("Profile '" + profile + "' not found in " + file);
151        }
152
153        user = properties.getProperty("user");
154        tenancy = properties.getProperty("tenancy");
155        region = properties.getProperty("region");
156        keyfile = properties.getProperty("key_file");
157        fingerprint = properties.getProperty("fingerprint");
158        passphrase = properties.getProperty("pass_phrase");
159    }
160
161    /**
162     * Loads the credentials from the environment variables.
163     * 
164     * @see <a href="https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/clienvironmentvariables.htm">CLI Environment Variables</a>
165     */
166    public void loadFromEnvironment() {
167        if (getenv("OCI_CLI_USER") != null) {
168            user = getenv("OCI_CLI_USER");
169        }
170        if (getenv("OCI_CLI_TENANCY") != null) {
171            tenancy = getenv("OCI_CLI_TENANCY");
172        }
173        if (getenv("OCI_CLI_REGION") != null) {
174            region = getenv("OCI_CLI_REGION");
175        }
176        if (getenv("OCI_CLI_KEY_FILE") != null) {
177            keyfile = getenv("OCI_CLI_KEY_FILE");
178        }
179        if (getenv("OCI_CLI_FINGERPRINT") != null) {
180            fingerprint = getenv("OCI_CLI_FINGERPRINT");
181        }
182        if (getenv("OCI_CLI_PASS_PHRASE") != null) {
183            passphrase = getenv("OCI_CLI_PASS_PHRASE");
184        }
185    }
186
187    /**
188     * Returns the default Oracle Cloud configuration.
189     */
190    public static OracleCloudCredentials getDefault() throws IOException {
191        OracleCloudCredentials credentials = new OracleCloudCredentials();
192        File config = getConfigFile();
193        if (config.exists()) {
194            credentials.load(config, getDefaultProfile());
195        }
196        credentials.loadFromEnvironment();
197        return credentials;
198    }
199
200    /**
201     * Returns the name of the default profile, either the value of the OCI_CLI_PROFILE environment variable or "DEFAULT".
202     */
203    public static String getDefaultProfile() {
204        String profile = getenv("OCI_CLI_PROFILE");
205        if (profile == null) {
206            profile = "DEFAULT";
207        }
208        return profile;
209    }
210
211    /**
212     * Returns the location of the configuration file, either the value of the OCI_CLI_CONFIG_FILE environment variable
213     * or <code>~/.oci/config</code>.
214     */
215    public static File getConfigFile() {
216        String config = getenv("OCI_CLI_CONFIG_FILE");
217        if (config != null) {
218            return new File(config);
219        } else {
220            return new File(System.getProperty("user.home"), ".oci/config");
221        }
222    }
223
224    static String getenv(String name) {
225        return System.getenv(name);
226    }
227}