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 bd55e387..32147cf6 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 @@ -66,6 +66,7 @@ public class MPMasterKey { * * @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object. */ + @Nonnull public byte[] getKeyID(final MPAlgorithm algorithm) throws MPKeyUnavailableException, MPAlgorithmException { @@ -87,6 +88,7 @@ public class MPMasterKey { return !invalidated; } + @Nonnull private byte[] masterKey(final MPAlgorithm algorithm) throws MPKeyUnavailableException, MPAlgorithmException { Preconditions.checkArgument( masterPassword.length > 0 ); @@ -109,6 +111,7 @@ public class MPMasterKey { return masterKey; } + @Nonnull private byte[] siteKey(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter, final MPKeyPurpose keyPurpose, @Nullable final String keyContext) throws MPKeyUnavailableException, MPAlgorithmException { @@ -141,13 +144,19 @@ public class MPMasterKey { * In the case of {@link MPResultTypeClass#Stateful} types, the result of * {@link #siteState(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)}. * + * @return {@code null} if the result type is missing a required parameter. + * * @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object. */ + @Nullable public String siteResult(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter, final MPKeyPurpose keyPurpose, @Nullable final String keyContext, final MPResultType resultType, @Nullable final String resultParam) throws MPKeyUnavailableException, MPAlgorithmException { + if ((resultType.getTypeClass() == MPResultTypeClass.Stateful) && (resultParam == null)) + return null; + byte[] masterKey = masterKey( algorithm ); byte[] siteKey = siteKey( siteName, algorithm, siteCounter, keyPurpose, keyContext ); @@ -176,6 +185,7 @@ public class MPMasterKey { * * @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object. */ + @Nonnull public String siteState(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter, final MPKeyPurpose keyPurpose, @Nullable final String keyContext, final MPResultType resultType, final String resultParam) diff --git a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPResultType.java b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPResultType.java index e13e358c..36af3936 100644 --- a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPResultType.java +++ b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPResultType.java @@ -41,7 +41,7 @@ public enum MPResultType { /** * 16: pg^VMAUBk5x3p%HP%i4= */ - GeneratedMaximum( "maximum", "Maximum Security", "pg^VMAUBk5x3p%HP%i4=", "20 characters, contains symbols.", // + GeneratedMaximum( "maximum", "Maximum Security", "pg^VMAUBk5x3p%HP%i4=", "20 characters, contains symbols", // ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ), new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), // MPResultTypeClass.Template, 0x0 ), @@ -49,7 +49,7 @@ public enum MPResultType { /** * 17: BiroYena8:Kixa */ - GeneratedLong( "long", "Long Password", "BiroYena8:Kixa", "Copy-friendly, 14 characters, contains symbols.", // + GeneratedLong( "long", "Long Password", "BiroYena8:Kixa", "Copy-friendly, 14 characters, contains symbols", // ImmutableList.of( new MPTemplate( "CvcvnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvnoCvcv" ), new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ), new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ), @@ -66,7 +66,7 @@ public enum MPResultType { /** * 18: BirSuj0- */ - GeneratedMedium( "medium", "Medium Password", "BirSuj0-", "Copy-friendly, 8 characters, contains symbols.", // + GeneratedMedium( "medium", "Medium Password", "BirSuj0-", "Copy-friendly, 8 characters, contains symbols", // ImmutableList.of( new MPTemplate( "CvcnoCvc" ), new MPTemplate( "CvcCvcno" ) ), // MPResultTypeClass.Template, 0x2 ), @@ -74,14 +74,14 @@ public enum MPResultType { /** * 19: Bir8 */ - GeneratedShort( "short", "Short Password", "Bir8", "Copy-friendly, 4 characters, no symbols.", // + GeneratedShort( "short", "Short Password", "Bir8", "Copy-friendly, 4 characters, no symbols", // ImmutableList.of( new MPTemplate( "Cvcn" ) ), // MPResultTypeClass.Template, 0x3 ), /** * 20: pO98MoD0 */ - GeneratedBasic( "basic", "Basic Password", "pO98MoD0", "8 characters, no symbols.", // + GeneratedBasic( "basic", "Basic Password", "pO98MoD0", "8 characters, no symbols", // ImmutableList.of( new MPTemplate( "aaanaaan" ), new MPTemplate( "aannaaan" ), new MPTemplate( "aaannaaa" ) ), // @@ -90,44 +90,44 @@ public enum MPResultType { /** * 21: 2798 */ - GeneratedPIN( "pin", "PIN Code", "2798", "4 numbers.", // + GeneratedPIN( "pin", "PIN Code", "2798", "4 numbers", // ImmutableList.of( new MPTemplate( "nnnn" ) ), // MPResultTypeClass.Template, 0x5 ), /** * 30: birsujano */ - GeneratedName( "name", "Name", "birsujano", "9 letter name.", // + GeneratedName( "name", "Name", "birsujano", "9 letter name", // ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), // MPResultTypeClass.Template, 0xE ), /** * 31: bir yennoquce fefi */ - GeneratedPhrase( "phrase", "Phrase", "bir yennoquce fefi", "20 character sentence.", // + GeneratedPhrase( "phrase", "Phrase", "bir yennoquce fefi", "20 character sentence", // ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ), new MPTemplate( "cvc cvccvcvcv cvcv" ), new MPTemplate( "cv cvccv cvc cvcvccv" ) ), // MPResultTypeClass.Template, 0xF ), /** - * 1056: Custom saved password. + * 1056: Custom saved value. */ - StoredPersonal( "personal", "Saved Password", null, "AES-encrypted, exportable.", // + StoredPersonal( "personal", "Saved", null, "AES-encrypted, exportable", // ImmutableList.of(), // MPResultTypeClass.Stateful, 0x0, MPSiteFeature.ExportContent ), /** - * 2081: Custom saved password that should not be exported from the device. + * 2081: Custom saved value that should not be exported from the device. */ - StoredDevicePrivate( "device", "Private Password", null, "AES-encrypted, not exported.", // + StoredDevicePrivate( "device", "Private", null, "AES-encrypted, not exported", // ImmutableList.of(), // MPResultTypeClass.Stateful, 0x1, MPSiteFeature.DevicePrivate ), /** * 4160: Derive a unique binary key. */ - DeriveKey( "key", "Binary Key", null, "Encryption key.", // + DeriveKey( "key", "Binary Key", null, "Encryption key", // ImmutableList.of(), // MPResultTypeClass.Derive, 0x0, MPSiteFeature.Alternative ); 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 837b7ab2..2a0241b7 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 @@ -160,6 +160,10 @@ public abstract class Res { return icon( "media/icon_settings.png" ); } + public Icon edit() { + return icon( "media/icon_edit.png" ); + } + public Icon avatar(final int index) { return icon( strf( "media/avatar-%d.png", index % avatars() ) ); } 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 af9b83a6..1bc494a0 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 @@ -470,6 +470,8 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, "Show site settings." ); private final JButton questionsButton = Components.button( Res.icons().question(), event -> showSiteQuestions(), "Show site recovery questions." ); + private final JButton editButton = Components.button( Res.icons().edit(), event -> showEditSite(), + "Set/save personal password/login." ); private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(), "Delete the site from the user." ); @@ -499,6 +501,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, siteToolbar.add( settingsButton ); siteToolbar.add( questionsButton ); + siteToolbar.add( editButton ); siteToolbar.add( deleteButton ); settingsButton.setEnabled( false ); questionsButton.setEnabled( false ); @@ -587,12 +590,14 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, Components.strut() ); components.add( Components.label( "Password Type:" ), - Components.comboBox( MPResultType.values(), MPResultType::getLongName, + Components.comboBox( MPResultType.values(), type -> getTypeDescription( + type, user.getDefaultType(), user.getAlgorithm().mpw_default_result_type() ), site.getResultType(), site::setResultType ), Components.strut() ); components.add( Components.label( "Login Type:" ), - Components.comboBox( MPResultType.values(), MPResultType::getLongName, + Components.comboBox( MPResultType.values(), type -> getTypeDescription( + type, user.getAlgorithm().mpw_default_login_type() ), site.getLoginType(), site::setLoginType ), Components.strut() ); @@ -606,6 +611,15 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) ); } + private String getTypeDescription(final MPResultType type, final MPResultType... defaults) { + boolean isDefault = false; + for (final MPResultType d : defaults) + if (isDefault = type == d) + break; + + return strf( "%s%s%s, %s", isDefault? "": "", type.getLongName(), isDefault? "": "", type.getDescription() ); + } + public void showSiteQuestions() { MPSite site = sitesModel.getSelectedItem(); if (site == null) @@ -651,6 +665,51 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener, } ); } + public void showEditSite() { + MPSite site = sitesModel.getSelectedItem(); + if (site == null) + return; + + try { + JTextField passwordField = Components.textField( site.getResult(), null ); + JTextField loginField = Components.textField( site.getLogin(), null ); + passwordField.setEditable( site.getResultType().getTypeClass() == MPResultTypeClass.Stateful ); + loginField.setEditable( site.getLoginType().getTypeClass() == MPResultTypeClass.Stateful ); + + if (JOptionPane.OK_OPTION == Components.showDialog( this, site.getSiteName(), new JOptionPane( + Components.panel( + BoxLayout.PAGE_AXIS, + Components.label( strf( "Site Login (currently set to: %s):", + getTypeDescription( site.getLoginType() ) ) ), + loginField, + Components.strut(), + Components.label( strf( "Site Password (currently set to: %s):", + getTypeDescription( site.getResultType() ) ) ), + passwordField, + Components.strut(), + Components.label( "To save a personal value in these fields,\n" + + "change the type to Saved in the site's settings." ) ), + JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) { + @Override + public void selectInitialValue() { + passwordField.requestFocusInWindow(); + } + } )) { + if (site instanceof MPFileSite) { + MPFileSite fileSite = (MPFileSite) site; + + if (site.getResultType().getTypeClass() == MPResultTypeClass.Stateful) + fileSite.setSitePassword( site.getResultType(), passwordField.getText() ); + if (site.getLoginType().getTypeClass() == MPResultTypeClass.Stateful) + fileSite.setLoginName( site.getLoginType(), loginField.getText() ); + } + } + } + catch (final MPKeyUnavailableException | MPAlgorithmException e) { + logger.err( e, "While computing site edit results." ); + } + } + public void deleteSite() { MPSite site = sitesModel.getSelectedItem(); if (site == null) diff --git a/platform-independent/java/gui/src/main/resources/media/icon_edit.png b/platform-independent/java/gui/src/main/resources/media/icon_edit.png new file mode 100644 index 00000000..d72e3ba0 Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_edit.png differ diff --git a/platform-independent/java/gui/src/main/resources/media/icon_edit@2x.png b/platform-independent/java/gui/src/main/resources/media/icon_edit@2x.png new file mode 100644 index 00000000..56dc750d Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_edit@2x.png differ diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java index 645e160c..f35a150a 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java @@ -58,30 +58,29 @@ public interface MPSite extends Comparable> { void setLoginType(@Nullable MPResultType loginType); - @Nonnull + @Nullable default String getResult() throws MPKeyUnavailableException, MPAlgorithmException { - return getResult( MPKeyPurpose.Authentication ); } - @Nonnull + @Nullable default String getResult(final MPKeyPurpose keyPurpose) throws MPKeyUnavailableException, MPAlgorithmException { return getResult( keyPurpose, null ); } - @Nonnull + @Nullable default String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext) throws MPKeyUnavailableException, MPAlgorithmException { return getResult( keyPurpose, keyContext, null ); } - @Nonnull + @Nullable String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state) throws MPKeyUnavailableException, MPAlgorithmException; - @Nonnull + @Nullable String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable UnsignedInteger counter, MPResultType type, @Nullable String state) throws MPKeyUnavailableException, MPAlgorithmException; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java index e7dce0c3..2d3b3a1c 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java @@ -129,7 +129,7 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext setChanged(); } - @Nonnull + @Nullable @Override public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final String state) throws MPKeyUnavailableException, MPAlgorithmException { @@ -137,7 +137,7 @@ public abstract class MPBasicSite, Q extends MPQuestion> ext return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state ); } - @Nonnull + @Nullable @Override public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state) diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java index 1de501f0..595216d1 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java @@ -93,7 +93,7 @@ public class MPFileSite extends MPBasicSite { setChanged(); } - @Nonnull + @Nullable @Override public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext) throws MPKeyUnavailableException, MPAlgorithmException {