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}