All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.schlichtherle.key.passwd.swing.PromptingKeyProviderUI Maven / Gradle / Ivy

/*
 * Copyright (C) 2006-2010 Schlichtherle IT Services
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.schlichtherle.key.passwd.swing;

import de.schlichtherle.awt.EventDispatchTimeoutException;
import de.schlichtherle.awt.EventQueue;
import de.schlichtherle.awt.Windows;
import de.schlichtherle.key.KeyPromptingInterruptedException;
import de.schlichtherle.key.KeyPromptingTimeoutException;
import de.schlichtherle.key.PromptingKeyProvider;
import de.schlichtherle.util.ClassLoaders;
import java.awt.Window;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JComponent;
import javax.swing.JOptionPane;

/**
 * A Swing based user interface to prompt for passwords or key files.
 * This class is thread safe.
 *
 * @author Christian Schlichtherle
 * @version $Id: PromptingKeyProviderUI.java 5e709f50671d 2010/11/05 11:52:07 christian $
 * @since TrueZIP 6.0
 */
public class PromptingKeyProviderUI
        implements de.schlichtherle.key.PromptingKeyProviderUI {

    private static final String PACKAGE_NAME
            = "de.schlichtherle.key.passwd.swing";
    private static final String CLASS_NAME
            = PACKAGE_NAME + ".PromptingKeyProviderUI";
    private static final ResourceBundle resources = ResourceBundle.getBundle(CLASS_NAME);
    private static final Logger logger = Logger.getLogger(CLASS_NAME);

    /**
     * The timeout for the EDT to start prompting for a key in
     * milliseconds.
     */
    private static final long START_PROMPTING_TIMEOUT = 1000;

    /**
     * This is the number of bytes to load from the beginning of a key file.
     * A valid key file for encryption must contain at least this number of
     * bytes!
     */
    // Must be a multiple of 2 and must be long enough so that
    // GZIPOutputStream most likely produces more than 2 * 256 / 8 bytes
    // output.
    public static final int KEY_FILE_LEN = 512;

    private static final Map openKeyPanels = new WeakHashMap();

    /**
     * The last resource ID used when prompting.
     * Initialized to the empty string.
     */
    static String lastResourceID = "";

    /**
     * @deprecated This field is not used anymore and will be removed for the
     *             next major release number.
     */
    private CreateKeyPanel createKeyPanel;

    /**
     * @deprecated This field is not used anymore and will be removed for the
     *             next major release number.
     */
    private OpenKeyPanel openKeyPanel;
    
    private Feedback unknownCreateKeyFeedback;
    private Feedback invalidCreateKeyFeedback;
    private Feedback unknownOpenKeyFeedback;
    private Feedback invalidOpenKeyFeedback;

    /**
     * Reads the encryption key as a byte sequence from the given pathname
     * into a buffer of exactly {@code KEY_FILE_LEN} bytes and returns it.
     *
     * @throws EOFException If the file is not at least {@code KEY_FILE_LEN}
     *         bytes long.
     * @throws IOException on any other I/O related issue.
     */
    static byte[] readKeyFile(String pathname)
    throws IOException {
        final byte[] buf = new byte[KEY_FILE_LEN];

        final RandomAccessFile raf = new RandomAccessFile(pathname, "r");
        try {
            raf.readFully(buf);
        } finally {
            raf.close();
        }

        return buf;
    }

    /**
     * @deprecated This method is not used anymore and will be removed for the
     *             next major release number.
     *             It's use may dead lock the GUI.
     *             Use {@link #createCreateKeyPanel} instead.
     */
    protected CreateKeyPanel getCreateKeyPanel() {
        if (createKeyPanel == null)
            createKeyPanel = createCreateKeyPanel();
        return createKeyPanel;
    }

    /**
     * A factory method to create the Create Protected Resource Key Panel.
     */
    protected CreateKeyPanel createCreateKeyPanel() {
        return new CreateKeyPanel();
    }

    /**
     * @deprecated This method is not used anymore and will be removed for the
     *             next major release number.
     *             It's use may dead lock the GUI.
     *             Use {@link #createOpenKeyPanel} instead.
     */
    protected OpenKeyPanel getOpenKeyPanel() {
        if (openKeyPanel == null)
            openKeyPanel = createOpenKeyPanel();
        return openKeyPanel;
    }

    /**
     * A factory method to create the Open Protected Resource Key Panel.
     */
    protected OpenKeyPanel createOpenKeyPanel() {
        return new OpenKeyPanel();
    }

    protected Feedback getUnknownCreateKeyFeedback() {
        if (unknownCreateKeyFeedback == null)
            unknownCreateKeyFeedback = createFeedback("UnknownCreateKeyFeedback");
        return unknownCreateKeyFeedback;
    }

    protected void setUnkownCreateKeyFeedback(final Feedback uckf) {
        this.unknownCreateKeyFeedback = uckf;
    }

    protected Feedback getInvalidCreateKeyFeedback() {
        if (invalidCreateKeyFeedback == null)
            invalidCreateKeyFeedback = createFeedback("InvalidCreateKeyFeedback");
        return invalidCreateKeyFeedback;
    }

    protected void setInvalidCreateKeyFeedback(final Feedback ickf) {
        this.invalidCreateKeyFeedback = ickf;
    }

    protected Feedback getUnknownOpenKeyFeedback() {
        if (unknownOpenKeyFeedback == null)
            unknownOpenKeyFeedback = createFeedback("UnknownOpenKeyFeedback");
        return unknownOpenKeyFeedback;
    }

    protected void setUnknownOpenKeyFeedback(final Feedback uokf) {
        this.unknownOpenKeyFeedback = uokf;
    }

    protected Feedback getInvalidOpenKeyFeedback() {
        if (invalidOpenKeyFeedback == null)
            invalidOpenKeyFeedback = createFeedback("InvalidOpenKeyFeedback");
        return invalidOpenKeyFeedback;
    }

    protected void setInvalidOpenKeyFeedback(final Feedback iokf) {
        this.invalidOpenKeyFeedback = iokf;
    }

    private static Feedback createFeedback(final String type) {
        try {
            String n = System.getProperty(
                    PACKAGE_NAME + "." + type,
                    PACKAGE_NAME + ".Basic" + type);
            Class c = ClassLoaders.loadClass(n, PromptingKeyProviderUI.class);
            Feedback f = (Feedback) c.newInstance();
            return f;
        } catch (ClassNotFoundException ex) {
            logger.log(Level.WARNING, "", ex);
        } catch (IllegalAccessException ex) {
            logger.log(Level.WARNING, "", ex);
        } catch (InstantiationException ex) {
            logger.log(Level.WARNING, "", ex);
        }
        return null;
    }

    public /*synchronized*/ final void promptCreateKey(
            final PromptingKeyProvider provider) {
        final Runnable task = new Runnable() {
            public void run() {
                promptCreateKey(provider, null);
            }
        };
        multiplexOnEDT(task); // synchronized on class instance!
    }

    public /*synchronized*/ final boolean promptUnknownOpenKey(
            final PromptingKeyProvider provider) {
        final BooleanRunnable task = new BooleanRunnable() {
            public void run() {
                result = promptOpenKey(provider, false, null);
            }
        };
        multiplexOnEDT(task); // synchronized on class instance!
        return task.result;
    }

    public /*synchronized*/ final boolean promptInvalidOpenKey(
            final PromptingKeyProvider provider) {
        final BooleanRunnable task = new BooleanRunnable() {
            public void run() {
                result = promptOpenKey(provider, true, null);
            }
        };
        multiplexOnEDT(task); // synchronized on class instance!
        return task.result;
    }

    private abstract static class BooleanRunnable implements Runnable {
        public boolean result;
    }

    /**
     * This method is only called by the AWT Event Dispatch Thread,
     * so it doesn't need to be thread safe.
     */
    protected void promptCreateKey(
            final PromptingKeyProvider provider,
            final JComponent extraDataUI) {
        assert EventQueue.isDispatchThread();

        final CreateKeyPanel createKeyPanel = createCreateKeyPanel();
        createKeyPanel.setExtraDataUI(extraDataUI);

        final Window parent = Windows.getParentWindow();
        try {
            while (!Thread.interrupted()) { // test and clear status!
                // Setting this inside the loop has the side effect of
                // de-highlighting the resource ID in the panel if the
                // loop iteration has to be repeated due to an invalid
                // user input.
                createKeyPanel.setResourceID(provider.getResourceID());
                createKeyPanel.setFeedback(createKeyPanel.getError() != null
                        ? getInvalidCreateKeyFeedback()
                        : getUnknownCreateKeyFeedback());

                final int result;
                try {
                    result = JOptionPane.showConfirmDialog(
                            parent,
                            createKeyPanel,
                            resources.getString("newPasswdDialog.title"),
                            JOptionPane.OK_CANCEL_OPTION,
                            JOptionPane.QUESTION_MESSAGE);
                } catch (StackOverflowError failure) {
                    // Workaround for bug ID #6471418 - should be fixed in
                    // JSE 1.5.0_11
                    boolean interrupted = Thread.interrupted(); // test and clear status!
                    assert interrupted;
                    break;
                }
                if (Thread.interrupted()) // test and clear status!
                    break;

                if (result != JOptionPane.OK_OPTION)
                    break; // reuse old key

                final Object createKey = createKeyPanel.getCreateKey();
                if (createKey != null) { // valid input?
                    provider.setKey(createKey);
                    break;
                }

                // Continue looping until valid input.
                assert createKeyPanel.getError() != null;
            }
        } finally {
            // Do NOT do this within the loop - would close the next
            // JOptionPane on repeat.
            eventuallyDispose(parent);
        }
    }

    /**
     * This method is only called by the AWT Event Dispatch Thread,
     * so it doesn't need to be thread safe.
     */
    protected boolean promptOpenKey(
            final PromptingKeyProvider provider,
            final boolean invalid,
            final JComponent extraDataUI) {
        assert EventQueue.isDispatchThread();

        final OpenKeyPanel openKeyPanel;
        if (invalid) {
            OpenKeyPanel panel = (OpenKeyPanel) openKeyPanels.get(provider);
            openKeyPanel = panel != null ? panel : createOpenKeyPanel();
            openKeyPanel.setError(resources.getString("invalidKey"));
        } else {
            openKeyPanel = createOpenKeyPanel();
            openKeyPanel.setExtraDataUI(extraDataUI);
        }
        openKeyPanels.put(provider, openKeyPanel);

        final Window parent = Windows.getParentWindow();
        try {
            while (!Thread.interrupted()) { // test and clear status!
                // Setting this inside the loop has the side effect of
                // de-highlighting the resource ID in the panel if the
                // loop iteration has to be repeated due to an invalid
                // user input.
                openKeyPanel.setResourceID(provider.getResourceID());
                openKeyPanel.setFeedback(openKeyPanel.getError() != null
                        ? getInvalidOpenKeyFeedback()
                        : getUnknownOpenKeyFeedback());

                final int result;
                try {
                    result = JOptionPane.showConfirmDialog(
                            parent,
                            openKeyPanel,
                            resources.getString("passwdDialog.title"),
                            JOptionPane.OK_CANCEL_OPTION,
                            JOptionPane.QUESTION_MESSAGE);
                } catch (StackOverflowError failure) {
                    // Workaround for bug ID #6471418 - should be fixed in
                    // JSE 1.5.0_11
                    boolean interrupted = Thread.interrupted(); // test and clear status!
                    assert interrupted;
                    break;
                }
                if (Thread.interrupted()) // test and clear status!
                    break;

                if (result != JOptionPane.OK_OPTION) {
                    provider.setKey(null);
                    break;
                }

                final Object openKey = openKeyPanel.getOpenKey();
                if (openKey != null) { // valid input?
                    provider.setKey(openKey);
                    break;
                }

                // Continue looping until valid input.
                assert openKeyPanel.getError() != null;
            }
        } finally {
            // Do NOT do this within the loop - would close the next
            // JOptionPane on repeat.
            eventuallyDispose(parent);
        }

        return openKeyPanel.isKeyChangeRequested();
    }

    /**
     * The following is a double work around for Sun's J2SE 1.4.2
     * which has been tested with 1.4.2-b28 and 1.4.2_12-b03 on the
     * Windows platform.
     * The issue for which this work around is required is known
     * to be present in the Java code of the AWT package, so this
     * should pertain to all platforms.
     * This issue has been fixed with Sun's JSE 1.5.0-b64 or higher.
     * 

* The issue is that an application will not terminate until all * Window's have been dispose()d or System.exit() has been called - * it is not sufficient just to hide() all Window's. *

* The {@code JOptionPane} properly dispose()s its Dialog which displays * our password panels. * However, by default {@code JOptionPane} uses an internal {@code Frame} * as its parent window if the application does not specify a parent * window explicitly. * {@code JOptionPane} never dispose()s the parent window, so the * client application may eventually not terminate. *

* The workaround is to dispose the parent window if it's not showing. */ private static void eventuallyDispose(final Window window) { if (!window.isShowing()) window.dispose(); } /** * Invokes the given {@code task} on the AWT Event Dispatching Thread * (EDT) and waits until it's finished. *

* In multithreaded environments, although technically possible, * do not allow multiple threads to prompt for a key concurrently, * because this would only confuse users. * By explicitly locking the class object rather than the instance, * we enforce this even if multiple implementations and instances * are used. *

* If the current thread is interrupted, an * {@link UndeclaredThrowableException} is thrown with a * {@link KeyPromptingInterruptedException} as its cause. *

* If a {@link Throwable} is thrown by the EDT, then it's wrapped in an * {@link UndeclaredThrowableException} and re-thrown by this thread. */ private static void multiplexOnEDT(final Runnable task) { if (Thread.interrupted()) throw new UndeclaredThrowableException( new KeyPromptingInterruptedException()); if (EventQueue.isDispatchThread()) { task.run(); } else { synchronized (PromptingKeyProviderUI.class) { try { EventQueue.invokeAndWaitUninterruptibly( task, START_PROMPTING_TIMEOUT); } catch (EventDispatchTimeoutException failure) { // Timeout while waiting for the EDT to start the task. // Now wrap this in two exceptions: The outermost exception will // be catched by the PromptingKeyProvider class and its cause // will be unwrapped and passed on to the client application by // the PromptingKeyProvider class. throw new UndeclaredThrowableException( new KeyPromptingTimeoutException(failure)); /*} catch (InterruptedException failure) { // We've been interrupted while waiting for the EDT. // Now wrap this in two exceptions: The outermost exception will // be catched by the PromptingKeyProvider class and its cause // will be unwrapped and passed on to the client application by // the PromptingKeyProvider class. throw new UndeclaredThrowableException( new KeyPromptingInterruptedException(failure)); */} catch (InvocationTargetException failure) { throw new UndeclaredThrowableException(failure); } finally { Thread.interrupted(); } } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy