KeyParameters.java
/*
* Copyright (C) 2016 Hobrasoft s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cz.hobrasoft.pdfmu.operation.signature;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_ALIASES;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_ALIAS_EXCEPTION;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_ALIAS_KEY_EXCEPTION;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_ALIAS_MISSING;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_ALIAS_NOT_KEY;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_CERTIFICATE_CHAIN;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_EMPTY;
import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_PRIVATE_KEY;
import cz.hobrasoft.pdfmu.operation.OperationException;
import cz.hobrasoft.pdfmu.operation.args.ArgsConfiguration;
import cz.hobrasoft.pdfmu.operation.args.PasswordArgs;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.util.AbstractMap.SimpleEntry;
import java.util.Enumeration;
import java.util.logging.Logger;
import net.sourceforge.argparse4j.inf.Argument;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.Namespace;
/**
*
* @author <a href="mailto:filip.bartek@hobrasoft.cz">Filip Bartek</a>
*/
class KeyParameters implements ArgsConfiguration {
public String alias = null;
public char[] password = null;
// TODO: Replace with Console
private static final Logger logger = Logger.getLogger(KeyParameters.class.getName());
public PasswordArgs passwordArgs = new PasswordArgs("key password");
private Argument keyAliasArgument;
@Override
public void addArguments(ArgumentParser parser) {
keyAliasArgument = parser.addArgument("--key-alias")
.help("key keystore entry alias (default: <first entry in the keystore>)")
.type(String.class);
passwordArgs.passwordArgument = parser.addArgument("--key-password")
.help("key password (default: <empty>)");
passwordArgs.environmentVariableArgument = parser.addArgument("--key-password-envvar")
.help("key password environment variable")
.setDefault("PDFMU_KEYPASS");
passwordArgs.finalizeArguments();
}
@Override
public void setFromNamespace(Namespace namespace) {
assert keyAliasArgument != null;
alias = namespace.getString(keyAliasArgument.getDest());
// Set password
assert passwordArgs != null;
passwordArgs.setFromNamespace(namespace);
password = passwordArgs.getPasswordCharArray();
// TODO?: Use keystore password by default
}
/**
* Encodes the alias using ISO-8859-1 and replaces it with the longest
* prefix of itself that is an actual alias in the keystore. The shortening
* is a necessary magic necessary for strings with some non-ASCII
* characters.
*/
private void fixAliasWcs(KeyStore ks) throws OperationException {
assert "Windows-MY".equals(ks.getType());
if (alias != null) {
logger.info(String.format("WCS alias correction will be applied. Original alias: %s", alias));
alias = new String(alias.getBytes(), StandardCharsets.ISO_8859_1);
Enumeration<String> aliases = null;
try {
aliases = ks.aliases();
} catch (KeyStoreException ex) {
throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIASES, ex);
}
String aliasBest = null;
while (aliases.hasMoreElements()) {
String aliasCandidate = aliases.nextElement();
if (alias.startsWith(aliasCandidate)
&& (aliasBest == null
|| aliasCandidate.length() > aliasBest.length())) {
aliasBest = aliasCandidate;
}
}
if (aliasBest != null) {
alias = aliasBest;
}
}
}
public void fixAlias(KeyStore ks) throws OperationException {
if (alias == null) {
// Get the first alias in the keystore
logger.info("Keystore entry alias not set. Using the first entry in the keystore.");
Enumeration<String> aliases;
try {
aliases = ks.aliases();
} catch (KeyStoreException ex) {
throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIASES, ex);
}
if (!aliases.hasMoreElements()) {
throw new OperationException(SIGNATURE_ADD_KEYSTORE_EMPTY);
}
alias = aliases.nextElement();
assert alias != null;
} else if ("Windows-MY".equals(ks.getType())) {
fixAliasWcs(ks);
}
logger.info(String.format("Keystore entry alias: %s", alias));
// Make sure the entry `alias` is present in the keystore
try {
if (!ks.containsAlias(alias)) {
throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIAS_MISSING,
new SimpleEntry<String, Object>("alias", alias));
}
} catch (KeyStoreException ex) {
throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIAS_EXCEPTION, ex,
new SimpleEntry<String, Object>("alias", alias));
}
// Make sure `alias` is a key entry
try {
if (!ks.isKeyEntry(alias)) {
throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIAS_NOT_KEY,
new SimpleEntry<String, Object>("alias", alias));
}
} catch (KeyStoreException ex) {
throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIAS_KEY_EXCEPTION, ex,
new SimpleEntry<String, Object>("alias", alias));
}
}
public void fixPassword(KeyStore ks, String ksPassword) {
switch (ks.getType()) {
case "Windows-MY":
if (password != null) {
logger.info("The keystore Windows-MY does not accept key password.");
password = null;
}
break;
case "pkcs12":
if (ksPassword != null) {
if (password != null && password != ksPassword.toCharArray()) {
logger.warning("In PKCS12 keystores, key password should not differ from the keystore password.");
}
if (password == null) {
logger.info("Key password not set. Using the keystore password.");
password = ksPassword.toCharArray();
}
} else if (password == null) {
logger.info("Key password not set. Using an empty password.");
password = "".toCharArray();
}
break;
default:
// Set key password to empty string if not set from command line
if (password == null) {
logger.info("Key password not set. Using an empty password.");
password = "".toCharArray();
}
}
}
public void fix(KeyStore ks, String ksPassword) throws OperationException {
fixAlias(ks);
fixPassword(ks, ksPassword);
}
private int countAliasOccurrences(KeyStore ks, String alias) throws KeyStoreException {
if (!ks.containsAlias(alias)) {
return 0;
}
Enumeration<String> aliases = ks.aliases();
int count = 0;
while (aliases.hasMoreElements()) {
if (alias.equals(aliases.nextElement())) {
++count;
}
}
assert (count > 0);
return count;
}
private boolean isAliasDuplicit(KeyStore ks, String alias) throws KeyStoreException {
return countAliasOccurrences(ks, alias) > 1;
}
public PrivateKey getPrivateKey(KeyStore ks) throws OperationException {
// Get private key from keystore
PrivateKey pk;
try {
if (isAliasDuplicit(ks, alias)) {
logger.warning(String.format("The key alias \"%1$s\" occurs multiple times in the keystore.", alias));
}
pk = (PrivateKey) ks.getKey(alias, password);
} catch (KeyStoreException ex) {
throw new OperationException(SIGNATURE_ADD_KEYSTORE_PRIVATE_KEY, ex,
new SimpleEntry<String, Object>("alias", alias));
} catch (NoSuchAlgorithmException ex) {
throw new OperationException(SIGNATURE_ADD_KEYSTORE_PRIVATE_KEY, ex,
new SimpleEntry<String, Object>("alias", alias));
} catch (UnrecoverableKeyException ex) {
// Incorrect key password? Incorrect keystore type?
throw new OperationException(SIGNATURE_ADD_KEYSTORE_PRIVATE_KEY, ex,
new SimpleEntry<String, Object>("alias", alias));
}
if (pk == null) {
// Incorrect alias?
throw new OperationException(SIGNATURE_ADD_KEYSTORE_PRIVATE_KEY,
new SimpleEntry<String, Object>("alias", alias));
}
return pk;
}
public Certificate[] getCertificateChain(KeyStore ks) throws OperationException {
Certificate[] chain;
try {
chain = ks.getCertificateChain(alias);
} catch (KeyStoreException ex) {
throw new OperationException(SIGNATURE_ADD_KEYSTORE_CERTIFICATE_CHAIN, ex,
new SimpleEntry<String, Object>("alias", alias));
}
return chain;
}
}