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}