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.toUpperCase()) : 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                File file = createFile(keystore);
140                if (!file.isFile()) {
141                    throw new IllegalArgumentException("Keystore file '" + keystore + "' not found");
142                }
143                storetype = KeyStoreType.of(file);
144                if (storetype == null) {
145                    throw new IllegalArgumentException("Keystore type of '" + keystore + "' not recognized");
146                }
147            }
148        }
149        return storetype;
150    }
151
152    /**
153     * Sets the password to access the private key. The password can be loaded from a file by using the <code>file:</code>
154     * prefix followed by the path of the file, or from an environment variable by using the <code>env:</code> prefix
155     * followed by the name of the variable.
156     */
157    public KeyStoreBuilder keypass(String keypass) {
158        this.keypass = keypass;
159        return this;
160    }
161
162    String keypass() {
163        keypass = readPassword("keypass", keypass);
164        return keypass;
165    }
166
167    /**
168     * Sets the file containing the private key.
169     */
170    public KeyStoreBuilder keyfile(String keyfile) {
171        return keyfile(createFile(keyfile));
172    }
173
174    /**
175     * Sets the file containing the private key.
176     */
177    public KeyStoreBuilder keyfile(File keyfile) {
178        this.keyfile = keyfile;
179        return this;
180    }
181
182    File keyfile() {
183        return keyfile;
184    }
185
186    /**
187     * Sets the file containing the certificate chain.
188     */
189    public KeyStoreBuilder certfile(String certfile) {
190        return certfile(createFile(certfile));
191    }
192
193    /**
194     * Sets the file containing the certificate chain.
195     */
196    public KeyStoreBuilder certfile(File certfile) {
197        this.certfile = certfile;
198        return this;
199    }
200
201    File certfile() {
202        return certfile;
203    }
204
205    void setBaseDir(File basedir) {
206        this.basedir = basedir;
207    }
208
209    File createFile(String file) {
210        if (file == null) {
211            return null;
212        }
213
214        if (new File(file).isAbsolute()) {
215            return new File(file);
216        } else {
217            return new File(basedir, file);
218        }
219    }
220
221    /**
222     * Read the password from the specified value. If the value is prefixed with <code>file:</code>
223     * the password is loaded from a file. If the value is prefixed with <code>env:</code> the password
224     * is loaded from an environment variable. Otherwise the value is returned as is.
225     *
226     * @param name  the name of the parameter
227     * @param value the value to parse
228     */
229    private String readPassword(String name, String value) {
230        if (value != null) {
231            if (value.startsWith("file:")) {
232                String filename = value.substring("file:".length());
233                Path path = createFile(filename).toPath();
234                try {
235                    value = String.join("\n", Files.readAllLines(path, StandardCharsets.UTF_8)).trim();
236                } catch (IOException e) {
237                    throw new IllegalArgumentException("Failed to read the " + name + " " + parameterName + " from the file '" + filename + "'", e);
238                }
239            } else if (value.startsWith("env:")) {
240                String variable = value.substring("env:".length());
241                if (!System.getenv().containsKey(variable)) {
242                    throw new IllegalArgumentException("Failed to read the " + name + " " + parameterName + ", the '" + variable + "' environment variable is not defined");
243                }
244                value = System.getenv(variable);
245            }
246        }
247
248        return value;
249    }
250
251    /**
252     * Validates the parameters.
253     */
254    void validate() throws IllegalArgumentException {
255        // keystore or keyfile, but not both
256        if (keystore != null && keyfile != null) {
257            throw new IllegalArgumentException("keystore " + parameterName + " can't be mixed with keyfile");
258        }
259
260        if (keystore == null && keyfile == null && certfile == null && storetype == null) {
261            throw new IllegalArgumentException("Either keystore, or keyfile and certfile, or storetype " + parameterName + "s must be set");
262        }
263
264        storetype().validate(this);
265    }
266
267    /**
268     * Returns the provider used to sign with the keystore.
269     */
270    public Provider provider() {
271        if (provider == null) {
272            provider = storetype().getProvider(this);
273        }
274        return provider;
275    }
276
277    /**
278     * Builds the keystore.
279     *
280     * @throws IllegalArgumentException if the parameters are invalid
281     * @throws KeyStoreException if the keystore can't be loaded
282     */
283    public KeyStore build() throws KeyStoreException {
284        validate();
285        return storetype().getKeystore(this, provider());
286    }
287
288    /**
289     * Returns a java.security.KeyStore.Builder using the parameters of this builder.
290     */
291    public KeyStore.Builder builder() {
292        return new KeyStore.Builder() {
293            @Override
294            public KeyStore getKeyStore() throws KeyStoreException {
295                return build();
296            }
297
298            @Override
299            public KeyStore.ProtectionParameter getProtectionParameter(String alias) {
300                String storepass = storepass();
301                return new KeyStore.PasswordProtection(storepass != null ? storepass.toCharArray() : null);
302            }
303        };
304    }
305}