001/**
002 * Copyright 2023 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;
018
019import java.io.File;
020import java.io.IOException;
021import java.nio.charset.StandardCharsets;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.security.KeyStore;
025import java.security.KeyStoreException;
026import java.security.Provider;
027import java.util.stream.Collectors;
028import java.util.stream.Stream;
029
030import static net.jsign.KeyStoreType.*;
031
032/**
033 * Keystore builder.
034 *
035 * <p>Example:</p>
036 *
037 * <pre>
038 *   KeyStore keystore = new KeyStoreBuilder().storetype(PKCS12).keystore("keystore.p12").storepass("password").build();
039 * </pre>
040 *
041 * @since 5.0
042 */
043public class KeyStoreBuilder {
044
045    /** The name used to refer to a configuration parameter */
046    private String parameterName = "parameter";
047
048    private String keystore;
049    private String storepass;
050    private KeyStoreType storetype;
051    private String keypass;
052    private File keyfile;
053    private File certfile;
054
055    /** The base directory to resolve the relative paths */
056    private File basedir = new File("empty").getParentFile();
057
058    private Provider provider;
059
060    public KeyStoreBuilder() {
061    }
062
063    KeyStoreBuilder(String parameterName) {
064        this.parameterName = parameterName;
065    }
066
067    String parameterName() {
068        return parameterName;
069    }
070
071    /**
072     * Sets the file containing the keystore.
073     */
074    public KeyStoreBuilder keystore(File keystore) {
075        return keystore(keystore.getPath());
076    }
077
078    /**
079     * Sets the name of the resource containing the keystore. Either the path of the keystore file,
080     * the SunPKCS11 configuration file or the cloud keystore name depending on the type of keystore.
081     */
082    public KeyStoreBuilder keystore(String keystore) {
083        this.keystore = keystore;
084        return this;
085    }
086
087    String keystore() {
088        return keystore;
089    }
090
091    /**
092     * Sets the password to access the keystore. The password can be loaded from a file by using the <code>file:</code>
093     * prefix followed by the path of the file, or from an environment variable by using the <code>env:</code> prefix
094     * followed by the name of the variable.
095     */
096    public KeyStoreBuilder storepass(String storepass) {
097        this.storepass = storepass;
098        return this;
099    }
100
101    String storepass() {
102        storepass = readPassword("storepass", storepass);
103        return storepass;
104    }
105
106    /**
107     * Sets the type of the keystore.
108     */
109    public KeyStoreBuilder storetype(KeyStoreType storetype) {
110        this.storetype = storetype;
111        return this;
112    }
113
114    /**
115     * Sets the type of the keystore.
116     *
117     * @param storetype the type of the keystore
118     * @throws IllegalArgumentException if the type is not recognized
119     */
120    public KeyStoreBuilder storetype(String storetype) {
121        try {
122            this.storetype = storetype != null ? KeyStoreType.valueOf(storetype) : null;
123        } catch (IllegalArgumentException e) {
124            String expectedTypes = Stream.of(KeyStoreType.values())
125                    .filter(type -> type != NONE).map(KeyStoreType::name)
126                    .collect(Collectors.joining(", "));
127            throw new IllegalArgumentException("Unknown keystore type '" + storetype + "' (expected types: " + expectedTypes + ")");
128        }
129        return this;
130    }
131
132    KeyStoreType storetype() {
133        if (storetype == null) {
134            if (keystore == null) {
135                // no keystore specified, keyfile and certfile are expected
136                storetype = NONE;
137            } else {
138                // the keystore type wasn't specified, let's try to guess it
139                storetype = KeyStoreType.of(createFile(keystore));
140                if (storetype == null) {
141                    throw new IllegalArgumentException("Keystore type of '" + keystore + "' not recognized");
142                }
143            }
144        }
145        return storetype;
146    }
147
148    /**
149     * Sets the password to access the private key. The password can be loaded from a file by using the <code>file:</code>
150     * prefix followed by the path of the file, or from an environment variable by using the <code>env:</code> prefix
151     * followed by the name of the variable.
152     */
153    public KeyStoreBuilder keypass(String keypass) {
154        this.keypass = keypass;
155        return this;
156    }
157
158    String keypass() throws SignerException {
159        keypass = readPassword("keypass", keypass);
160        return keypass;
161    }
162
163    /**
164     * Sets the file containing the private key.
165     */
166    public KeyStoreBuilder keyfile(String keyfile) {
167        return keyfile(createFile(keyfile));
168    }
169
170    /**
171     * Sets the file containing the private key.
172     */
173    public KeyStoreBuilder keyfile(File keyfile) {
174        this.keyfile = keyfile;
175        return this;
176    }
177
178    File keyfile() {
179        return keyfile;
180    }
181
182    /**
183     * Sets the file containing the certificate chain.
184     * The certificate used for signing must be the first one.
185     */
186    public KeyStoreBuilder certfile(String certfile) {
187        return certfile(createFile(certfile));
188    }
189
190    /**
191     * Sets the file containing the certificate chain.
192     * The certificate used for signing must be the first one.
193     */
194    public KeyStoreBuilder certfile(File certfile) {
195        this.certfile = certfile;
196        return this;
197    }
198
199    File certfile() {
200        return certfile;
201    }
202
203    void setBaseDir(File basedir) {
204        this.basedir = basedir;
205    }
206
207    File createFile(String file) {
208        if (file == null) {
209            return null;
210        }
211
212        if (new File(file).isAbsolute()) {
213            return new File(file);
214        } else {
215            return new File(basedir, file);
216        }
217    }
218
219    /**
220     * Read the password from the specified value. If the value is prefixed with <code>file:</code>
221     * the password is loaded from a file. If the value is prefixed with <code>env:</code> the password
222     * is loaded from an environment variable. Otherwise the value is returned as is.
223     *
224     * @param name  the name of the parameter
225     * @param value the value to parse
226     */
227    private String readPassword(String name, String value) {
228        if (value != null) {
229            if (value.startsWith("file:")) {
230                String filename = value.substring("file:".length());
231                Path path = createFile(filename).toPath();
232                try {
233                    value = String.join("\n", Files.readAllLines(path, StandardCharsets.UTF_8)).trim();
234                } catch (IOException e) {
235                    throw new IllegalArgumentException("Failed to read the " + name + " " + parameterName + " from the file '" + filename + "'", e);
236                }
237            } else if (value.startsWith("env:")) {
238                String variable = value.substring("env:".length());
239                if (!System.getenv().containsKey(variable)) {
240                    throw new IllegalArgumentException("Failed to read the " + name + " " + parameterName + ", the '" + variable + "' environment variable is not defined");
241                }
242                value = System.getenv(variable);
243            }
244        }
245
246        return value;
247    }
248
249    /**
250     * Validates the parameters.
251     */
252    void validate() throws IllegalArgumentException {
253        // keystore or keyfile, but not both
254        if (keystore != null && keyfile != null) {
255            throw new IllegalArgumentException("keystore " + parameterName + " can't be mixed with keyfile");
256        }
257
258        if (keystore == null && keyfile == null && certfile == null && storetype == null) {
259            throw new IllegalArgumentException("Either keystore, or keyfile and certfile, or storetype " + parameterName + "s must be set");
260        }
261
262        storetype().validate(this);
263    }
264
265    /**
266     * Returns the provider used to sign with the keystore.
267     */
268    public Provider provider() {
269        if (provider == null) {
270            provider = storetype().getProvider(this);
271        }
272        return provider;
273    }
274
275    /**
276     * Builds the keystore.
277     *
278     * @throws IllegalArgumentException if the parameters are invalid
279     * @throws KeyStoreException if the keystore can't be loaded
280     */
281    public KeyStore build() throws KeyStoreException {
282        validate();
283        return storetype().getKeystore(this, provider());
284    }
285}