de.unkrig.commons.lang.crypto.SecretKeys Maven / Gradle / Ivy
/*
* de.unkrig.commons - A general-purpose Java class library
*
* Copyright (c) 2017, Arno Unkrig
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package de.unkrig.commons.lang.crypto;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.UnrecoverableKeyException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPasswordField;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import de.unkrig.commons.lang.AssertionUtil;
import de.unkrig.commons.nullanalysis.Nullable;
/**
* Utility methods related to {@link SecretKey}s.
*/
public final
class SecretKeys {
static { AssertionUtil.enableAssertionsForThisClass(); }
private SecretKeys() {}
/**
* If the keyStoreFile does not exist, then this method generates a new {@link SecretKey} and stores it
* in the keyStoreFile).
*
* Otherwise, if the keyStoreFile does exist, then this method loads the {@link SecretKey} from the
* keyStoreFile.
*
*
* @param keyStorePassword See {@link KeyStore#load(InputStream, char[])}
* @param keyProtectionPassword See {@link KeyStore#getKey(String, char[])}; {@code null} to prompt the user
* @see #adHocSecretKey(File, char[], String, String, String, String)
*/
public static SecretKey
adHocSecretKey(
File keyStoreFile,
@Nullable char[] keyStorePassword,
String keyAlias,
char[] keyProtectionPassword
) throws GeneralSecurityException, IOException {
// Notice: The "KeyStore.getDefault()" cannot store SecretKeys, so we must use "JCEKS".
KeyStore ks = KeyStore.getInstance("JCEKS");
SecretKey secretKey;
boolean keystoreDirty;
if (keyStoreFile.exists()) {
// Load existing keystore file.
SecretKeys.loadKeyStoreFromFile(keyStoreFile, keyStorePassword, ks);
secretKey = (SecretKey) ks.getKey(keyAlias, keyProtectionPassword);
keystoreDirty = false;
} else {
// The keystore file does not yet exist; create an empty keystore.
ks.load(null, keyStorePassword);
secretKey = null;
keystoreDirty = true;
}
if (secretKey == null) {
// Key does not exist in keystore; generate a new one and put it into the keystore.
secretKey = KeyGenerator.getInstance("AES").generateKey();
ks.setKeyEntry(keyAlias, secretKey, keyProtectionPassword, null);
keystoreDirty = true;
}
// Store the keystore in the file, if necessary.
if (keystoreDirty) {
SecretKeys.saveKeyStoreToFile(ks, keyStoreFile, keyStorePassword);
}
return secretKey;
}
/**
* If the keyStoreFile does not exist, then this method asks the user interactively to (optionally)
* choose a "key protection password", generates a new {@link SecretKey} and stores it in the
* keyStoreFile).
*
* Otherwise, if the keyStoreFile does exist, then this method asks the user interactively for the
* "key protection password" (if one was given when the key was previously generated), and loads the {@link
* SecretKey} from the keyStoreFile.
*
*
* @param keyStorePassword See {@link KeyStore#load(InputStream, char[])}
* @param dialogTitle E.g. {@code "Authentication store"}
* @param messageCreateKey E.g. {@code "Do you want to create an authentication store for user names and
* passwords?"}
* @param messageUseExistingKey E.g. {@code "Do you want to use the existing authentication store for user names
* and passwords?"}
* @return {@code null} indicates that the user cancelled the operation
* @see #adHocSecretKey(File, char[], String, char[])
*/
@Nullable public static SecretKey
adHocSecretKey(
File keyStoreFile,
@Nullable char[] keyStorePassword,
String keyAlias,
String dialogTitle,
String messageCreateKey,
String messageUseExistingKey
) throws GeneralSecurityException, IOException {
// Notice: The "KeyStore.getDefault()" cannot store SecretKeys, so we must use "JCEKS".
KeyStore ks = KeyStore.getInstance("JCEKS");
SecretKey secretKey;
boolean keystoreDirty;
if (keyStoreFile.exists()) {
// Load existing keystore file.
SecretKeys.loadKeyStoreFromFile(keyStoreFile, keyStorePassword, ks);
try {
// Try to get the key with an EMPTY protection password first.
secretKey = (SecretKey) ks.getKey(keyAlias, new char[0]);
} catch (UnrecoverableKeyException uke) {
// The user now needs to enter the correct master password.
String label1 = (
""
+ ""
+ messageUseExistingKey + "
"
+ "
"
+ "If yes, then enter the correct master password:"
+ ""
);
for (;;) {
char[] keyProtectionPassword = SecretKeys.askForPassword(
dialogTitle,
label1,
(
""
+ ""
+ ""
+ "If you lost the password, you can always delete the keystore file
"
+ "\"" + keyStoreFile + "\" and start
"
+ "over with a new password."
+ ""
+ ""
)
);
if (keyProtectionPassword == null) return null;
try {
secretKey = (SecretKey) ks.getKey(keyAlias, keyProtectionPassword);
break;
} catch (UnrecoverableKeyException uke2) {
label1 = "The password you entered is wrong; please try again:";
}
}
}
keystoreDirty = false;
} else {
// The keystore file does not yet exist; create an empty keystore.
ks.load(null, keyStorePassword);
secretKey = null;
keystoreDirty = true;
}
if (secretKey == null) {
// Key does not exist in keystore; generate a new one and put it into the keystore.
secretKey = KeyGenerator.getInstance("AES").generateKey();
char[] keyProtectionPassword = SecretKeys.askForPassword(
dialogTitle,
(
""
+ ""
+ messageCreateKey + "
"
+ "
"
+ "If yes, then define a password for it:"
+ ""
),
(
""
+ ""
+ ""
+ "Choosing an empty or trivial master password makes it possible for an
"
+ "attacker to compromise the stored data."
+ ""
+ ""
)
);
if (keyProtectionPassword == null) return null;
ks.setKeyEntry(keyAlias, secretKey, keyProtectionPassword, null);
keystoreDirty = true;
}
// Store the keystore in the file, if necessary.
if (keystoreDirty) {
SecretKeys.saveKeyStoreToFile(ks, keyStoreFile, keyStorePassword);
}
return secretKey;
}
private static void
loadKeyStoreFromFile(File keyStoreFile, @Nullable char[] keyStorePassword, KeyStore keyStore)
throws FileNotFoundException, GeneralSecurityException {
InputStream is = new FileInputStream(keyStoreFile);
try {
keyStore.load(is, keyStorePassword);
is.close();
} catch (IOException ioe) {
if (ioe.getCause() instanceof UnrecoverableKeyException) {
// Wrong key store password.
throw new AssertionError(ioe); // TODO Better handling required - query password interactively!?
}
} finally {
try { is.close(); } catch (Exception e) {}
}
}
private static void
saveKeyStoreToFile(KeyStore keyStore, File keyStoreFile, @Nullable char[] keyStorePassword)
throws GeneralSecurityException, IOException {
OutputStream os = new FileOutputStream(keyStoreFile);
try {
keyStore.store(os, keyStorePassword);
os.close();
} finally {
try { os.close(); } catch (Exception e) {}
}
}
@Nullable private static char[]
askForPassword(String title, String label1, @Nullable String label2) {
JPasswordField passwordField = new JPasswordField();
SecretKeys.focussify(passwordField);
if (JOptionPane.showOptionDialog(
null, // parentComponent
new Object[] { // message
new JLabel(label1),
passwordField,
label2 == null ? null : new JLabel(label2)
},
title, // title
JOptionPane.YES_NO_OPTION, // optionType
JOptionPane.QUESTION_MESSAGE, // messageType
null, // icon
null, // options
null // initialValue
) != JOptionPane.YES_OPTION) return null;
return passwordField.getPassword();
}
private static void
focussify(JComponent component) {
// One terrible hack: One this particular machine, "focussify()" makes any dialog unusable, and I have no
// reasonable way to debug the problem.
try {
if (InetAddress.getLocalHost().getHostName().endsWith("DRMF")) return;
} catch (UnknownHostException e) {
;
}
// This is tricky... see
// http://tips4java.wordpress.com/2010/03/14/dialog-focus/
component.addAncestorListener(new AncestorListener() {
@Override public void
ancestorAdded(@Nullable AncestorEvent event) {
assert event != null;
JComponent component = event.getComponent();
component.requestFocusInWindow();
component.removeAncestorListener(this);
}
@Override public void ancestorRemoved(@Nullable AncestorEvent event) {}
@Override public void ancestorMoved(@Nullable AncestorEvent event) {}
});
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy