From 928b617ed0682306a7afad89083410e9f4d5a8d5 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sun, 29 Jul 2018 14:01:07 -0400 Subject: [PATCH] Import & export users + improved user state tracking. --- .../lyndir/masterpassword/MPMasterKey.java | 3 +- platform-independent/java/gui/build.gradle | 2 +- .../com/lyndir/masterpassword/gui/GUI.java | 26 +++ .../gui/{Main.java => MasterPassword.java} | 72 ++++++--- .../masterpassword/gui/platform/AppleGUI.java | 62 -------- .../masterpassword/gui/platform/BaseGUI.java | 58 ------- .../masterpassword/gui/platform/JDK9GUI.java | 60 ------- .../masterpassword/gui/util/Components.java | 24 +++ .../masterpassword/gui/util/Platform.java | 52 ++++++ .../lyndir/masterpassword/gui/util/Res.java | 8 + .../gui/util/platform/ApplePlatform.java | 49 ++++++ .../gui/util/platform/BasePlatform.java | 25 +++ .../gui/util/platform/IPlatform.java | 16 ++ .../gui/util/platform/JDK9Platform.java | 43 +++++ .../gui/{ => util}/platform/package-info.java | 2 +- .../masterpassword/gui/view/FilesPanel.java | 26 +-- .../gui/view/MasterPasswordFrame.java | 4 +- .../gui/view/UserContentPanel.java | 150 +++++++++++++++--- .../src/main/resources/media/icon_export.png | Bin 0 -> 1944 bytes .../main/resources/media/icon_export@2x.png | Bin 0 -> 3146 bytes .../src/main/resources/media/icon_import.png | Bin 0 -> 1951 bytes .../main/resources/media/icon_import@2x.png | Bin 0 -> 3196 bytes .../lyndir/masterpassword/model/MPUser.java | 8 +- .../model/impl/MPBasicUser.java | 9 +- .../masterpassword/model/impl/MPFileUser.java | 55 +++++-- .../model/impl/MPFileUserManager.java | 37 +++-- 26 files changed, 510 insertions(+), 281 deletions(-) create mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java rename platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/{Main.java => MasterPassword.java} (71%) delete mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/AppleGUI.java delete mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/BaseGUI.java delete mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/JDK9GUI.java create mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Platform.java create mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java create mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java create mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java create mode 100644 platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java rename platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/{ => util}/platform/package-info.java (95%) create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_export.png create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_export@2x.png create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_import.png create mode 100644 platform-independent/java/gui/src/main/resources/media/icon_import@2x.png diff --git a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java index 15c6d458..bd55e387 100644 --- a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java +++ b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java @@ -44,7 +44,8 @@ public class MPMasterKey { /** * @param masterPassword The characters of the user's master password. - * Note: this method destroys the contents of the array. + * + * @apiNote This method destroys the contents of the {@code masterPassword} array. */ @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter") public MPMasterKey(final String fullName, final char[] masterPassword) { diff --git a/platform-independent/java/gui/build.gradle b/platform-independent/java/gui/build.gradle index 8bfb918a..04c2134e 100644 --- a/platform-independent/java/gui/build.gradle +++ b/platform-independent/java/gui/build.gradle @@ -5,7 +5,7 @@ plugins { } description = 'Master Password GUI' -mainClassName = 'com.lyndir.masterpassword.gui.Main' +mainClassName = 'com.lyndir.masterpassword.gui.MasterPassword' dependencies { implementation group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.7-p2' diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java new file mode 100644 index 00000000..3b5b3d3a --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java @@ -0,0 +1,26 @@ +package com.lyndir.masterpassword.gui; + +import com.lyndir.lhunath.opal.system.logging.Logger; +import com.lyndir.masterpassword.gui.util.Platform; +import com.lyndir.masterpassword.gui.util.Res; +import com.lyndir.masterpassword.gui.view.MasterPasswordFrame; + + +/** + * @author lhunath, 2018-07-28 + */ +public class GUI { + + private static final Logger logger = Logger.get( GUI.class ); + + private final MasterPasswordFrame frame = new MasterPasswordFrame(); + + public GUI() { + Platform.get().installAppForegroundHandler( this::open ); + Platform.get().installAppReopenHandler( this::open ); + } + + public void open() { + Res.ui( () -> frame.setVisible( true ) ); + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Main.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/MasterPassword.java similarity index 71% rename from platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Main.java rename to platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/MasterPassword.java index 9fa1fbd9..aced49f7 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Main.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/MasterPassword.java @@ -23,10 +23,14 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import com.google.common.base.Charsets; import com.google.common.io.ByteSource; import com.lyndir.lhunath.opal.system.logging.Logger; -import com.lyndir.masterpassword.gui.platform.BaseGUI; +import com.lyndir.lhunath.opal.system.util.ObjectUtils; +import com.lyndir.masterpassword.model.MPUser; import java.io.IOException; import java.io.InputStream; import java.net.*; +import java.util.Collection; +import java.util.concurrent.CopyOnWriteArraySet; +import javax.annotation.Nullable; import javax.swing.*; @@ -35,33 +39,42 @@ import javax.swing.*; * * @author mbillemo */ -public final class Main { +public final class MasterPassword { @SuppressWarnings("UnusedDeclaration") - private static final Logger logger = Logger.get( Main.class ); + private static final Logger logger = Logger.get( MasterPassword.class ); - public static void main(final String... args) { -// Thread.setDefaultUncaughtExceptionHandler( -// (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) ); + private static final MasterPassword instance = new MasterPassword(); - // Try and set the system look & feel, if available. - try { - UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); - } - catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) { - } + private final Collection listeners = new CopyOnWriteArraySet<>(); - // Check online to see if this version has been superseded. - if (Config.get().checkForUpdates()) - checkUpdate(); + @Nullable + private MPUser activeUser; - // Create a platform-specific GUI and open it. - BaseGUI.createPlatformGUI().open(); + public static MasterPassword get() { + return instance; + } + + public boolean addListener(final Listener listener) { + return listeners.add( listener ); + } + + public boolean removeListener(final Listener listener) { + return listeners.remove( listener ); + } + + public void activateUser(final MPUser user) { + if (ObjectUtils.equals( activeUser, user )) + return; + + activeUser = user; + for (final Listener listener : listeners) + listener.onUserSelected( activeUser ); } private static void checkUpdate() { try { - String implementationVersion = Main.class.getPackage().getImplementationVersion(); + String implementationVersion = MasterPassword.class.getPackage().getImplementationVersion(); String latestVersion = new ByteSource() { @Override public InputStream openStream() @@ -90,4 +103,27 @@ public final class Main { logger.wrn( e, "Couldn't check for version update." ); } } + + public static void main(final String... args) { + // Thread.setDefaultUncaughtExceptionHandler( + // (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) ); + + // Try and set the system look & feel, if available. + try { + UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); + } + catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) { + } + + // Check online to see if this version has been superseded. + if (Config.get().checkForUpdates()) + checkUpdate(); + + // Create a platform-specific GUI and open it. + new GUI().open(); + } + + public interface Listener { + void onUserSelected(@Nullable MPUser user); + } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/AppleGUI.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/AppleGUI.java deleted file mode 100644 index 4a5731bf..00000000 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/AppleGUI.java +++ /dev/null @@ -1,62 +0,0 @@ -//============================================================================== -// This file is part of Master Password. -// Copyright (c) 2011-2017, Maarten Billemont. -// -// Master Password is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Master Password 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 General Public License for more details. -// -// You can find a copy of the GNU General Public License in the -// LICENSE file. Alternatively, see . -//============================================================================== - -package com.lyndir.masterpassword.gui.platform; - -import com.apple.eawt.*; -import com.google.common.base.Preconditions; -import com.lyndir.masterpassword.gui.view.MasterPasswordFrame; -import javax.swing.*; - - -/** - * @author lhunath, 2014-06-10 - */ -public class AppleGUI extends BaseGUI { - - static Application application = Preconditions.checkNotNull( - Application.getApplication(), "Not an Apple Java application." ); - - public AppleGUI() { - application.addAppEventListener( new AppEventHandler() ); - } - - @Override - protected MasterPasswordFrame createFrame() { - MasterPasswordFrame frame = super.createFrame(); - frame.setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); - return frame; - } - - private class AppEventHandler implements AppForegroundListener, AppReOpenedListener { - - @Override - public void appMovedToBackground(final AppEvent.AppForegroundEvent arg0) { - } - - @Override - public void appRaisedToForeground(final AppEvent.AppForegroundEvent arg0) { - open(); - } - - @Override - public void appReOpened(final AppEvent.AppReOpenedEvent arg0) { - open(); - } - } -} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/BaseGUI.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/BaseGUI.java deleted file mode 100644 index f0750534..00000000 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/BaseGUI.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.lyndir.masterpassword.gui.platform; - -import com.lyndir.lhunath.opal.system.logging.Logger; -import com.lyndir.lhunath.opal.system.util.TypeUtils; -import com.lyndir.masterpassword.gui.util.Res; -import com.lyndir.masterpassword.gui.view.MasterPasswordFrame; -import java.lang.reflect.InvocationTargetException; -import java.util.Optional; -import javax.annotation.Nullable; - - -/** - * @author lhunath, 2018-07-28 - */ -public class BaseGUI { - - private static final Logger logger = Logger.get( BaseGUI.class ); - - private final MasterPasswordFrame frame = createFrame(); - - public static BaseGUI createPlatformGUI() { - BaseGUI jdk9GUI = construct( "com.lyndir.masterpassword.gui.platform.JDK9GUI" ); - if (jdk9GUI != null) - return jdk9GUI; - - BaseGUI appleGUI = construct( "com.lyndir.masterpassword.gui.platform.AppleGUI" ); - if (appleGUI != null) - return appleGUI; - - // Use platform-independent GUI. - return new BaseGUI(); - } - - @Nullable - private static BaseGUI construct(final String typeName) { - try { - // AppleGUI adds support for macOS features. - Optional> gui = TypeUtils.loadClass( typeName ); - if (gui.isPresent()) - return gui.get().getConstructor().newInstance(); - } - catch (@SuppressWarnings("ErrorNotRethrown") final LinkageError ignored) { - } - catch (final IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) { - throw logger.bug( e ); - } - - return null; - } - - protected MasterPasswordFrame createFrame() { - return new MasterPasswordFrame(); - } - - public void open() { - Res.ui( () -> frame.setVisible( true ) ); - } -} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/JDK9GUI.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/JDK9GUI.java deleted file mode 100644 index 7970f32b..00000000 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/JDK9GUI.java +++ /dev/null @@ -1,60 +0,0 @@ -//============================================================================== -// This file is part of Master Password. -// Copyright (c) 2011-2017, Maarten Billemont. -// -// Master Password is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Master Password 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 General Public License for more details. -// -// You can find a copy of the GNU General Public License in the -// LICENSE file. Alternatively, see . -//============================================================================== - -package com.lyndir.masterpassword.gui.platform; - -import com.lyndir.masterpassword.gui.view.MasterPasswordFrame; -import java.awt.*; -import java.awt.desktop.*; -import javax.swing.*; - - -/** - * @author lhunath, 2014-06-10 - */ -@SuppressWarnings("Since15") -public class JDK9GUI extends BaseGUI { - - public JDK9GUI() { - Desktop.getDesktop().addAppEventListener( new AppEventHandler() ); - } - - @Override - protected MasterPasswordFrame createFrame() { - MasterPasswordFrame frame = super.createFrame(); - frame.setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); - return frame; - } - - private class AppEventHandler implements AppForegroundListener, AppReopenedListener { - - @Override - public void appRaisedToForeground(final AppForegroundEvent e) { - open(); - } - - @Override - public void appMovedToBackground(final AppForegroundEvent e) { - } - - @Override - public void appReopened(final AppReopenedEvent e) { - open(); - } - } -} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java index 229bd22b..99107ae9 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java @@ -21,6 +21,7 @@ package com.lyndir.masterpassword.gui.util; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.function.Consumer; @@ -110,6 +111,29 @@ public abstract class Components { return (option < 0)? JOptionPane.CLOSED_OPTION: option; } + @Nullable + public static File showLoadDialog(@Nullable final Component owner, final String title) { + return showFileDialog( owner, title, FileDialog.LOAD, null ); + } + + @Nullable + public static File showSaveDialog(@Nullable final Component owner, final String title, final String fileName) { + return showFileDialog( owner, title, FileDialog.SAVE, fileName ); + } + + @Nullable + private static File showFileDialog(@Nullable final Component owner, final String title, + final int mode, @Nullable final String fileName) { + FileDialog fileDialog = new FileDialog( JOptionPane.getFrameForComponent( owner ), title, mode ); + fileDialog.setFile( fileName ); + fileDialog.setLocationRelativeTo( owner ); + fileDialog.setLocationByPlatform( true ); + fileDialog.setVisible( true ); + + File[] selectedFiles = fileDialog.getFiles(); + return ((selectedFiles != null) && (selectedFiles.length > 0))? selectedFiles[0]: null; + } + public static JDialog showDialog(@Nullable final Component owner, @Nullable final String title, final Container content) { JDialog dialog = new JDialog( (owner != null)? SwingUtilities.windowForComponent( owner ): null, title, Dialog.ModalityType.DOCUMENT_MODAL ); diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Platform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Platform.java new file mode 100644 index 00000000..5a7b8029 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Platform.java @@ -0,0 +1,52 @@ +package com.lyndir.masterpassword.gui.util; + +import com.lyndir.lhunath.opal.system.logging.Logger; +import com.lyndir.lhunath.opal.system.util.TypeUtils; +import com.lyndir.masterpassword.gui.util.platform.BasePlatform; +import com.lyndir.masterpassword.gui.util.platform.IPlatform; +import java.lang.reflect.InvocationTargetException; +import java.util.Optional; +import javax.annotation.Nullable; + + +/** + * @author lhunath, 2018-07-29 + */ +public final class Platform { + + private static final Logger logger = Logger.get( Platform.class ); + private static final IPlatform activePlatform; + + static { + IPlatform tryPlatform; + if (null != (tryPlatform = construct( "com.lyndir.masterpassword.gui.util.platform.JDK9Platform" ))) + activePlatform = tryPlatform; + + else if (null != (tryPlatform = construct( "com.lyndir.masterpassword.gui.util.platform.ApplePlatform" ))) + activePlatform = tryPlatform; + + else + activePlatform = new BasePlatform(); + } + + @Nullable + private static T construct(final String typeName) { + try { + // AppleGUI adds support for macOS features. + Optional> gui = TypeUtils.loadClass( typeName ); + if (gui.isPresent()) + return gui.get().getConstructor().newInstance(); + } + catch (@SuppressWarnings("ErrorNotRethrown") final LinkageError ignored) { + } + catch (final IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) { + throw logger.bug( e ); + } + + return null; + } + + public static IPlatform get() { + return activePlatform; + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java index e4bba494..c29896e1 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java @@ -136,6 +136,14 @@ public abstract class Res { return icon( "media/icon_reset.png" ); } + public Icon import_() { + return icon( "media/icon_import.png" ); + } + + public Icon export() { + return icon( "media/icon_export.png" ); + } + public Icon settings() { return icon( "media/icon_settings.png" ); } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java new file mode 100644 index 00000000..961bcc89 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java @@ -0,0 +1,49 @@ +package com.lyndir.masterpassword.gui.util.platform; + +import com.apple.eawt.*; +import com.apple.eio.FileManager; +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import java.io.File; +import java.io.FileNotFoundException; + + +/** + * @author lhunath, 2018-07-29 + */ +public class ApplePlatform implements IPlatform { + + static Application application = Preconditions.checkNotNull( + Application.getApplication(), "Not an Apple Java application." ); + + @Override + public boolean installAppForegroundHandler(final Runnable handler) { + application.addAppEventListener( new AppForegroundListener() { + @Override + public void appMovedToBackground(final AppEvent.AppForegroundEvent e) { + } + + @Override + public void appRaisedToForeground(final AppEvent.AppForegroundEvent e) { + handler.run(); + } + } ); + return true; + } + + @Override + public boolean installAppReopenHandler(final Runnable handler) { + application.addAppEventListener( (AppReOpenedListener) e -> handler.run() ); + return true; + } + + @Override + public boolean show(final File file) { + try { + return FileManager.revealInFinder( file ); + } + catch (final FileNotFoundException ignored) { + return false; + } + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java new file mode 100644 index 00000000..b6b41ea3 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java @@ -0,0 +1,25 @@ +package com.lyndir.masterpassword.gui.util.platform; + +import java.io.File; + + +/** + * @author lhunath, 2018-07-29 + */ +public class BasePlatform implements IPlatform { + + @Override + public boolean installAppForegroundHandler(final Runnable handler) { + return false; + } + + @Override + public boolean installAppReopenHandler(final Runnable handler) { + return false; + } + + @Override + public boolean show(final File file) { + return false; + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java new file mode 100644 index 00000000..a6f3d48e --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java @@ -0,0 +1,16 @@ +package com.lyndir.masterpassword.gui.util.platform; + +import java.io.File; + + +/** + * @author lhunath, 2018-07-29 + */ +public interface IPlatform { + + boolean installAppForegroundHandler(Runnable handler); + + boolean installAppReopenHandler(Runnable handler); + + boolean show(File file); +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java new file mode 100644 index 00000000..1bc94782 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java @@ -0,0 +1,43 @@ +package com.lyndir.masterpassword.gui.util.platform; + +import java.awt.*; +import java.awt.desktop.*; +import java.io.File; + + +/** + * @author lhunath, 2018-07-29 + */ +@SuppressWarnings("Since15") +public class JDK9Platform implements IPlatform { + + @Override + public boolean installAppForegroundHandler(final Runnable handler) { + Desktop.getDesktop().addAppEventListener( new AppForegroundListener() { + @Override + public void appRaisedToForeground(final AppForegroundEvent e) { + handler.run(); + } + + @Override + public void appMovedToBackground(final AppForegroundEvent e) { + } + } ); + return true; + } + + @Override + public boolean installAppReopenHandler(final Runnable handler) { + Desktop.getDesktop().addAppEventListener( (AppReopenedListener) e -> handler.run() ); + return true; + } + + @Override + public boolean show(final File file) { + if (!file.exists()) + return false; + + Desktop.getDesktop().browseFileDirectory( file ); + return true; + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/package-info.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/package-info.java similarity index 95% rename from platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/package-info.java rename to platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/package-info.java index 0168e33e..f4786cc2 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/package-info.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/package-info.java @@ -20,6 +20,6 @@ * @author lhunath, 2018-04-26 */ @ParametersAreNonnullByDefault -package com.lyndir.masterpassword.gui.platform; +package com.lyndir.masterpassword.gui.util.platform; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java index 8aa0778c..c42a102c 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java @@ -3,6 +3,7 @@ package com.lyndir.masterpassword.gui.view; import static com.lyndir.masterpassword.util.Utilities.*; import com.google.common.collect.ImmutableSortedSet; +import com.lyndir.masterpassword.gui.MasterPassword; import com.lyndir.masterpassword.gui.util.*; import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.impl.MPFileUser; @@ -18,15 +19,13 @@ import javax.swing.*; * @author lhunath, 2018-07-14 */ @SuppressWarnings("serial") -public class FilesPanel extends JPanel implements MPFileUserManager.Listener { - - private final Collection listeners = new CopyOnWriteArraySet<>(); +public class FilesPanel extends JPanel implements MPFileUserManager.Listener, MasterPassword.Listener { private final JButton avatarButton = Components.button( Res.icons().avatar( 0 ), event -> setAvatar(), "Click to change the user's avatar." ); private final CollectionListModel> usersModel = - CollectionListModel.>copy( MPFileUserManager.get().getFiles() ).selection( this::setUser ); + CollectionListModel.>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser ); private final JComboBox> userField = Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) ); @@ -50,10 +49,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener { add( userField ); MPFileUserManager.get().addListener( this ); - } - - public boolean addListener(final Listener listener) { - return listeners.add( listener ); + MasterPassword.get().addListener( this ); } private void setAvatar() { @@ -65,20 +61,14 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener { avatarButton.setIcon( Res.icons().avatar( selectedUser.getAvatar() ) ); } - public void setUser(@Nullable final MPUser user) { - avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) ); - - for (final Listener listener : listeners) - listener.onUserSelected( user ); - } - @Override public void onFilesUpdated(final ImmutableSortedSet files) { usersModel.set( files ); } - public interface Listener { - - void onUserSelected(@Nullable MPUser user); + @Override + public void onUserSelected(@Nullable final MPUser user) { + usersModel.setSelectedItem( user ); + avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) ); } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java index 2ac3698f..baf3a66e 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java @@ -3,10 +3,12 @@ package com.lyndir.masterpassword.gui.view; import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Res; +import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.impl.MPFileUserManager; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; +import javax.annotation.Nullable; import javax.swing.*; import javax.swing.border.BevelBorder; @@ -40,8 +42,6 @@ public class MasterPasswordFrame extends JFrame { BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ), Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER ); - filesPanel.addListener( userContent ); - addComponentListener( new ComponentHandler() ); setPreferredSize( new Dimension( 800, 560 ) ); setDefaultCloseOperation( DISPOSE_ON_CLOSE ); diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java index bc7e1307..aa064d7e 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java @@ -8,15 +8,22 @@ import com.google.common.primitives.UnsignedInteger; import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.util.ObjectUtils; import com.lyndir.masterpassword.*; +import com.lyndir.masterpassword.gui.MasterPassword; import com.lyndir.masterpassword.gui.model.MPNewSite; import com.lyndir.masterpassword.gui.util.*; +import com.lyndir.masterpassword.gui.util.Platform; import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.impl.*; import java.awt.*; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.*; +import java.util.Optional; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -31,20 +38,22 @@ import javax.swing.event.DocumentListener; * @author lhunath, 2018-07-14 */ @SuppressWarnings("SerializableStoresNonSerializable") -public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPUser.Listener { +public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener { private static final Random random = new Random(); private static final Logger logger = Logger.get( UserContentPanel.class ); private static final JButton iconButton = Components.button( Res.icons().user(), null, null ); - private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(), - "Add a new user to Master Password." ); + private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(), + "Add a new user to Master Password." ); + private final JButton importButton = Components.button( Res.icons().import_(), event -> importUser(), + "Import a user from a backup file into Master Password." ); private final JPanel userToolbar = Components.panel( BoxLayout.PAGE_AXIS ); private final JPanel siteToolbar = Components.panel( BoxLayout.PAGE_AXIS ); @Nullable - private MPUser activeUser; + private MPUser showingUser; private ContentMode contentMode; public UserContentPanel() { @@ -53,7 +62,9 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); setBorder( Components.marginBorder() ); - setUser( null ); + showUser( null ); + + MasterPassword.get().addListener( this ); } protected JComponent getUserToolbar() { @@ -66,52 +77,52 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU @Override public void onUserSelected(@Nullable final MPUser user) { - setUser( user ); + showUser( user ); } @Override public void onUserUpdated(final MPUser user) { - setUser( user ); + showUser( user ); } @Override public void onUserAuthenticated(final MPUser user) { - setUser( user ); + showUser( user ); } @Override public void onUserInvalidated(final MPUser user) { - setUser( user ); + showUser( user ); } - private void setUser(@Nullable final MPUser user) { + private void showUser(@Nullable final MPUser user) { Res.ui( () -> { - if (activeUser != null) - activeUser.removeListener( this ); + if (showingUser != null) + showingUser.removeListener( this ); ContentMode newContentMode = ContentMode.getContentMode( user ); - if ((newContentMode != contentMode) || !ObjectUtils.equals( activeUser, user )) { + if ((newContentMode != contentMode) || !ObjectUtils.equals( showingUser, user )) { userToolbar.removeAll(); siteToolbar.removeAll(); removeAll(); - activeUser = user; + showingUser = user; switch (contentMode = newContentMode) { case NO_USER: add( new NoUserPanel() ); break; case AUTHENTICATE: - add( new AuthenticateUserPanel( Preconditions.checkNotNull( activeUser ) ) ); + add( new AuthenticateUserPanel( Preconditions.checkNotNull( showingUser ) ) ); break; case AUTHENTICATED: - add( new AuthenticatedUserPanel( Preconditions.checkNotNull( activeUser ) ) ); + add( new AuthenticatedUserPanel( Preconditions.checkNotNull( showingUser ) ) ); break; } revalidate(); transferFocus(); } - if (activeUser != null) - activeUser.addListener( this ); + if (showingUser != null) + showingUser.addListener( this ); } ); } @@ -122,7 +133,70 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU if (fullName == null) return; - setUser( MPFileUserManager.get().add( fullName.toString() ) ); + MasterPassword.get().activateUser( MPFileUserManager.get().add( fullName.toString() ) ); + } + + private void importUser() { + File importFile = Components.showLoadDialog( this, "Import User File" ); + if (importFile == null) + return; + + try { + MPFileUser importUser = MPFileUser.load( importFile ); + if (importUser == null) { + JOptionPane.showMessageDialog( + this, "Not a Master Password file.", + "Import Failed", JOptionPane.ERROR_MESSAGE ); + return; + } + + JPasswordField passwordField = Components.passwordField(); + if (JOptionPane.OK_OPTION == Components.showDialog( this, "Import User", new JOptionPane( Components.panel( + BoxLayout.PAGE_AXIS, + Components.label( strf( "Enter the master password to import %s:", + importUser.getFullName() ) ), + Components.strut(), + passwordField ), JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) { + @Override + public void selectInitialValue() { + passwordField.requestFocusInWindow(); + } + } )) { + try { + importUser.authenticate( passwordField.getPassword() ); + Optional existingUser = MPFileUserManager.get().getFiles().stream().filter( + user -> user.getFullName().equalsIgnoreCase( importUser.getFullName() ) ).findFirst(); + if (existingUser.isPresent() && (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog( + this, + strf( "Importing user %s from this file will replace the existing user with the imported one.
" + + "Are you sure?

" + + "Existing user last modified: %s
Imported user last modified: %s
", + importUser.getFullName(), + Res.format( existingUser.get().getLastUsed() ), + Res.format( importUser.getLastUsed() ) ) ))) + return; + + MasterPassword.get().activateUser( MPFileUserManager.get().add( importUser ) ); + } + catch (final MPIncorrectMasterPasswordException | MPAlgorithmException e) { + JOptionPane.showMessageDialog( + this, e.getLocalizedMessage(), + "Import Failed", JOptionPane.ERROR_MESSAGE ); + } + } + } + catch (final IOException e) { + logger.err( e, "While reading user import file." ); + JOptionPane.showMessageDialog( + this, strf( "Couldn't read import file:
%s
.", e.getLocalizedMessage() ), + "Import Failed", JOptionPane.ERROR_MESSAGE ); + } + catch (MPMarshalException e) { + logger.err( e, "While parsing user import file." ); + JOptionPane.showMessageDialog( + this, strf( "Couldn't parse import file:
%s
.", e.getLocalizedMessage() ), + "Import Failed", JOptionPane.ERROR_MESSAGE ); + } } private enum ContentMode { @@ -147,6 +221,7 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); userToolbar.add( addButton ); + userToolbar.add( importButton ); add( Box.createGlue() ); add( Components.heading( "Select a user to proceed." ) ); @@ -160,6 +235,8 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU @Nonnull private final MPUser user; + private final JButton exportButton = Components.button( Res.icons().export(), event -> exportUser(), + "Export this user to a backup file." ); private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteUser(), "Delete this user from Master Password." ); private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(), @@ -177,6 +254,8 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU this.user = user; userToolbar.add( addButton ); + userToolbar.add( importButton ); + userToolbar.add( exportButton ); userToolbar.add( deleteButton ); userToolbar.add( resetButton ); @@ -197,6 +276,27 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU add( Box.createGlue() ); } + private void exportUser() { + MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null; + if (fileUser == null) + return; + + File exportFile = Components.showSaveDialog( this, "Export User File", fileUser.getFile().getName() ); + if (exportFile == null) + return; + + try { + Platform.get().show( + Files.copy( fileUser.getFile().toPath(), exportFile.toPath(), + StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES ).toFile() ); + } + catch (final IOException e) { + JOptionPane.showMessageDialog( + this, e.getLocalizedMessage(), + "Export Failed", JOptionPane.ERROR_MESSAGE ); + } + } + private void deleteUser() { MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null; if (fileUser == null) @@ -372,7 +472,17 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU sitesModel.registerList( sitesList ); add( Box.createGlue() ); - user.addListener( this ); + addComponentListener( new ComponentAdapter() { + @Override + public void componentShown(final ComponentEvent e) { + user.addListener( AuthenticatedUserPanel.this ); + } + + @Override + public void componentHidden(final ComponentEvent e) { + user.removeListener( AuthenticatedUserPanel.this ); + } + } ); } public void showUserPreferences() { diff --git a/platform-independent/java/gui/src/main/resources/media/icon_export.png b/platform-independent/java/gui/src/main/resources/media/icon_export.png new file mode 100644 index 0000000000000000000000000000000000000000..d1d97fb266f228a44fb1612f80522415b65bc81e GIT binary patch literal 1944 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFsEgPM3hAM`dB6B=jtV<?;Zqle1Gx6p~WYGxKbf-tXS8q>!0ns}yePYv5bpoSKp8QB{;0T;&&% zT$P<{nWAKGr(jcI1vDTxwIorYA~z?m*s8)-32d$vkPQ;nS5g2gDap1~as*kZ5aAo3 z;GAESs$i;Tpqp%9W}skZsAp(wVs37(qhMrUXrOOkq;F`XYiMp|Y-D9%pa2C*K--E^ z(yW49+@N*=dA3R!B_#z``ugSN<$C4Ddih1^`i7R4mLM~XjC6r2bc-wVN)jt{^NN*W zCb*;)Cl_TFlw{`TDS%8&Ov*1Uu~h=P6yk;40$*Ra!Fk2dfC2`Yennz|zM-Cher_(v zUtrb6B|)hOXJA!b98y`3svneEoL^d$oC;K~4ATq@JNy=b6armime z%DE^tu_V7JBtJg~mI4AY@=NlIGx7@*oP$jjd=ry1^FVx1^gw*;l3J8mmYU*Ll%J~r z4qvNGEcUxuqWasy(agow$j#8g$jQXi&CpT_syBt4Fw+M*Mjw7u7M@JC0cOh+ zo-U3d5r^MSkM{@;lsT@uc8!VeL660XM+1~Jbx*Z>9BPkKsxr!IbCLSXywp%9qV>xI zZ#y2ZHwV=sJz4{PrSLR|HKv(v>g;&8e)qYTckkZ$zjJP#y`_2H>$vSN`t{|09N4@6 z{`Y_9zR#&Xw=-HwQ6*Q^Gum`R)`R0m*Xtfm+kADheU7uUGyB48w~jnoDRH83XN>a4 zia8yLX0z2pwL-Ozu_08ehG7<@Um+&$O+*{k>C8>?t-aZS8KR#s+n%-bwG?y?ZWji+4%z z-C6(s{p%{QcyrW7h^zJ4?p?c{%vd~&HzO%0Cr9YB&goeX>atg{8L_dmOCO%d@$u*1 zi=Vo>j0H|Vx6aSb*8cRfM(oVz10J2nUz=XJdiCiiD~D?rFJ9a^gf|@bnTCdjM#>!D%ipTZ^4V#Fa$92DgBO{5V|K>Wo!b4NJLz7E z(f+oGFe7H~ZhZ-^qf7@CcCHKA=CG!tFT-|C`|*VZGZOrGOc*cMF{e-5_j%Lys=pl1 z7Nmu&44L+^JMfglGxNoPuRpV&m~zsF`@8!E)3b|zGasm2z-sEGP{tm>P`!cg?TUcf z*J0Z04JCM_WIC2G&h*)K_iSO_46{!!CYlv~eAvutQYK^SWWe^I?DTQT2fohMY4gh} z0uOI`H}lb>N4Y_4mrAYfn(}kRe$LslXOE0{{Ja+*Iiq_&>j@}I2w!}-NW1dqr>T6V zar5W%@bJVuVV|0j(0w&aI?0m1a_wbaZt?ju7_V?nG-z16VB5BB6R$_+ZsQj?EoQud zIp)~22=mOGtV2h`PrT1IWL_b>si(2GVoQ(IafvIH3;1JhB(Xg)_-gUj`*+wHUmu@2 z3ob{U%C3CDIpbl8)zzf)yAr=Jt&KV3u=290`?SMZTVK7%o4}aPn*Kjh>U8nhv$rQ- z6+Cy%pKV5@cqCs%yW!TTz4BXTXSV;j*?;|JZEfxIw8WN^uRNq?%qyIfav*Ns`T1p> z@2#_ovR?UWnX2&Km?=4l=g#?{+GCAgO%9|HdheLWfsOG`+p- ztC~FzWeUj;a=4?=f>gnp|vd$@? F2>={|5k~+3 literal 0 HcmV?d00001 diff --git a/platform-independent/java/gui/src/main/resources/media/icon_export@2x.png b/platform-independent/java/gui/src/main/resources/media/icon_export@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..37c9e4f62945d4ce6fa12d1c5b4574abe9fd143e GIT binary patch literal 3146 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 n#O7;a(s9ye_aCp^`?Y#`f~72;cK>i5Mtd literal 0 HcmV?d00001 diff --git a/platform-independent/java/gui/src/main/resources/media/icon_import.png b/platform-independent/java/gui/src/main/resources/media/icon_import.png new file mode 100644 index 0000000000000000000000000000000000000000..5cdfd6e0397cfacf960bec6ca22231dc0dcfa45a GIT binary patch literal 1951 zcmbVNYfuwc6pnx(QHqFFM1eK5MJY*k10*Dp#3Y+kT7nQ$4WP)9ED*?MW3oWV1O!@9 ziYX{X(V;b>fJ}X0Mbx$;sHlh)tlEbyh0#K*^6-J9Vy)dM*mlP0kM7Lw-p6;o@0@eb znOhPY^Nt7Amr9{fJffDs3i1y)-YKr+*JQDbQYg-;TBV9m$)kB{9Ah9FJOyQ#F$1|z zp+pMJ21K2S5k zM-pHtfdFPqr#JD;eA;tf9yxX_Giktc2$9LBi5!A}N*)VHa3czYGeEi;WP<=3VnCr` z91bT0V1W?G1X)aoMTa=NP!(CH9VP^BZDAL7|3CP><9?rLLgTp7Ky+xNXn5(ITIYO zvgNQjP^d^I<%%L;2$DiviHIEm!<%O-52^dOuAXyd;$Pu+x@0dG|=S8$IYSd<<8krHtfam$;X%hD$&FPjI|lO1CuTO)Cs zUlG|>a|o%5Scaz(IfxOZyy$=^3`rx)0MX>G=|CgNw(N?6MM~AFp{??0Z_&)Nanprf zSKwNo;7iF+evNAhB9aK9WZ`Lw+o;{$>zK%QW08;G zicBk+PJMbU{~-1_eP^v-@8$SItFpB=r`c~ku3a>8Jnw$*HqN1+T*OOGDmFKUB*(Yz zC_P5+(6Qg{5H=4F539>FYr)(|C&enpiq&t-m@jT!F>U3H*pOL^c)T&H_4c33Td6jg ziR%5~DMX=A+;h@p^!dlSR(lH;UfbiT_P=uVYS#7RZOLG)QrR@s^rx*L+}YWgnU
X(^)<0 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 literal 0 HcmV?d00001 diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java index e9be467c..f8242940 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java @@ -55,12 +55,11 @@ public interface MPUser> extends Comparable> { /** * Performs an authentication attempt against the keyID for this user. * - * Note: If a keyID is not set, authentication will always succeed and the keyID will be set to match the given master password. - * * @param masterPassword The password to authenticate with. - * You cannot re-use this array after passing it in, authentication will destroy its contents. * * @throws MPIncorrectMasterPasswordException If authentication fails due to the given master password not matching the user's keyID. + * @apiNote If a keyID is not set, authentication will always succeed and the keyID will be set to match the given master password. + * This method destroys the contents of the {@code masterPassword} array. */ void authenticate(char[] masterPassword) throws MPIncorrectMasterPasswordException, MPAlgorithmException; @@ -68,11 +67,10 @@ public interface MPUser> extends Comparable> { /** * Performs an authentication attempt against the keyID for this user. * - * Note: If a keyID is not set, authentication will always succeed and the keyID will be set to match the given key. - * * @param masterKey The master key to authenticate with. * * @throws MPIncorrectMasterPasswordException If authentication fails due to the given master password not matching the user's keyID. + * @apiNote If a keyID is not set, authentication will always succeed and the keyID will be set to match the given key. */ void authenticate(MPMasterKey masterKey) throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java index eef907c2..6821eeaf 100755 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java @@ -37,8 +37,9 @@ import javax.annotation.Nullable; */ public abstract class MPBasicUser> extends Changeable implements MPUser { - protected final Logger logger = Logger.get( getClass() ); - private final Set listeners = new CopyOnWriteArraySet<>(); + private static final Logger logger = Logger.get( MPBasicUser.class ); + + private final Set listeners = new CopyOnWriteArraySet<>(); private int avatar; private final String fullName; @@ -65,7 +66,7 @@ public abstract class MPBasicUser> extends Changeabl @Override public void setAvatar(final int avatar) { - if (Objects.equals(this.avatar, avatar)) + if (Objects.equals( this.avatar, avatar )) return; this.avatar = avatar; @@ -86,7 +87,7 @@ public abstract class MPBasicUser> extends Changeabl @Override public void setAlgorithm(final MPAlgorithm algorithm) { - if (Objects.equals(this.algorithm, algorithm)) + if (Objects.equals( this.algorithm, algorithm )) return; this.algorithm = algorithm; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java index acbdf2bc..f8fd88a2 100755 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java @@ -18,6 +18,7 @@ package com.lyndir.masterpassword.model.impl; +import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPUser; @@ -36,6 +37,8 @@ import org.joda.time.ReadableInstant; @SuppressWarnings("ComparableImplementedButEqualsNotOverridden") public class MPFileUser extends MPBasicUser { + private static final Logger logger = Logger.get( MPFileUser.class ); + @Nullable private byte[] keyID; private File path; @@ -46,13 +49,23 @@ public class MPFileUser extends MPBasicUser { private ReadableInstant lastUsed; private boolean complete; - public MPFileUser(final String fullName) { - this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm() ); + @Nullable + public static MPFileUser load(final File file) + throws IOException, MPMarshalException { + for (final MPMarshalFormat format : MPMarshalFormat.values()) + if (file.getName().endsWith( format.fileSuffix() )) + return format.unmarshaller().readUser( file ); + + return null; } - public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm) { + public MPFileUser(final String fullName, final File path) { + this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), path ); + } + + public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File path) { this( fullName, keyID, algorithm, 0, null, new Instant(), - MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, MPFileUserManager.get().getPath() ); + MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, path ); } public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, @@ -74,6 +87,10 @@ public class MPFileUser extends MPBasicUser { return (keyID == null)? null: keyID.clone(); } + public void setPath(final File path) { + this.path = path; + } + @Override public void setAlgorithm(final MPAlgorithm algorithm) { if (!algorithm.equals( getAlgorithm() ) && (keyID != null)) { @@ -99,7 +116,7 @@ public class MPFileUser extends MPBasicUser { } public void setFormat(final MPMarshalFormat format) { - if (Objects.equals(this.format, format)) + if (Objects.equals( this.format, format )) return; this.format = format; @@ -111,7 +128,7 @@ public class MPFileUser extends MPBasicUser { } public void setContentMode(final MPMarshaller.ContentMode contentMode) { - if (Objects.equals(this.contentMode, contentMode)) + if (Objects.equals( this.contentMode, contentMode )) return; this.contentMode = contentMode; @@ -123,7 +140,7 @@ public class MPFileUser extends MPBasicUser { } public void setDefaultType(final MPResultType defaultType) { - if (Objects.equals(this.defaultType, defaultType)) + if (Objects.equals( this.defaultType, defaultType )) return; this.defaultType = defaultType; @@ -169,6 +186,19 @@ public class MPFileUser extends MPBasicUser { } } + public void save() { + try { + if (isComplete()) + getFormat().marshaller().marshall( this ); + } + catch (final MPKeyUnavailableException e) { + logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this ); + } + catch (final IOException | MPMarshalException | MPAlgorithmException e) { + logger.err( e, "Unable to write out changes for user: %s", this ); + } + } + @Override public void reset() { keyID = null; @@ -183,16 +213,7 @@ public class MPFileUser extends MPBasicUser { @Override protected void onChanged() { - try { - if (isComplete()) - getFormat().marshaller().marshall( this ); - } - catch (final MPKeyUnavailableException e) { - logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this ); - } - catch (final IOException | MPMarshalException | MPAlgorithmException e) { - logger.err( e, "Unable to write out changes for user: %s", this ); - } + save(); super.onChanged(); } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java index d90eff13..b3d260dd 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java @@ -78,31 +78,40 @@ public class MPFileUserManager { } for (final File file : pathFiles) - for (final MPMarshalFormat format : MPMarshalFormat.values()) - if (file.getName().endsWith( format.fileSuffix() )) - try { - MPFileUser user = format.unmarshaller().readUser( file ); - MPFileUser previousUser = userByName.put( user.getFullName(), user ); - if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal())) - userByName.put( previousUser.getFullName(), previousUser ); - break; - } - catch (final IOException | MPMarshalException e) { - logger.err( e, "Couldn't read user from: %s", file ); - } + try { + MPFileUser user = MPFileUser.load( file ); + if (user != null) { + MPFileUser previousUser = userByName.put( user.getFullName(), user ); + if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal())) + userByName.put( previousUser.getFullName(), previousUser ); + } + } + catch (final IOException | MPMarshalException e) { + logger.err( e, "Couldn't read user from: %s", file ); + } fireUpdated(); } public MPFileUser add(final String fullName) { - MPFileUser user = new MPFileUser( fullName ); - userByName.put( user.getFullName(), user ); + return add( new MPFileUser( fullName, getPath() ) ); + } + + public MPFileUser add(final MPFileUser user) { + user.setPath( getPath() ); + user.save(); + + MPFileUser oldUser = userByName.put( user.getFullName(), user ); + if (oldUser != null) + oldUser.invalidate(); fireUpdated(); return user; } public void delete(final MPFileUser user) { + user.invalidate(); + // Remove deleted users. File userFile = user.getFile(); if (userFile.exists() && !userFile.delete())