From 80b5fcd785a489ebf673609b228a5c3560ac4d60 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Wed, 18 Jul 2018 12:23:53 -0400 Subject: [PATCH] Refactor model, improved isolation & access unauthenticated file metadata. --- .../lyndir/masterpassword/model/MPSite.java | 2 - .../lyndir/masterpassword/model/MPUser.java | 13 + .../masterpassword/model/MPUserManager.java | 53 ---- .../model/impl/MPBasicSite.java | 7 - .../model/impl/MPBasicUser.java | 33 ++- .../masterpassword/model/impl/MPFileUser.java | 54 ++-- .../model/impl/MPFileUserManager.java | 108 +++----- .../model/impl/MPFlatMarshaller.java | 17 +- .../model/impl/MPFlatUnmarshaller.java | 231 ++++++++++-------- .../masterpassword/model/impl/MPJSONFile.java | 57 ++--- .../model/impl/MPJSONMarshaller.java | 7 +- .../model/impl/MPJSONUnmarshaller.java | 39 ++- .../model/impl/MPMarshaller.java | 7 +- .../model/impl/MPUnmarshaller.java | 10 +- 14 files changed, 305 insertions(+), 333 deletions(-) delete mode 100644 platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUserManager.java 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 d5571eb1..521821e8 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 @@ -35,8 +35,6 @@ public interface MPSite extends Comparable> { @Nonnull String getName(); - void setName(String name); - // - Algorithm @Nonnull 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 5c9f2f31..dbce1fef 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 @@ -19,6 +19,8 @@ package com.lyndir.masterpassword.model; import com.lyndir.masterpassword.*; +import com.lyndir.masterpassword.model.impl.MPBasicSite; +import com.lyndir.masterpassword.model.impl.MPBasicUser; import java.util.Collection; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -93,4 +95,15 @@ public interface MPUser> extends Comparable> { @Nonnull Collection findSites(String query); + + boolean addListener(Listener listener); + + boolean removeListener(Listener listener); + + interface Listener { + + void onUserUpdated(MPUser user); + + void onUserAuthenticated(MPUser user); + } } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUserManager.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUserManager.java deleted file mode 100644 index 60cdfb51..00000000 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUserManager.java +++ /dev/null @@ -1,53 +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.model; - -import com.google.common.collect.*; -import java.util.Collection; -import java.util.Map; - - -/** - * @author lhunath, 14-12-05 - */ -public abstract class MPUserManager> { - - private final Map usersByName = Maps.newHashMap(); - - protected MPUserManager(final Iterable users) { - for (final U user : users) - usersByName.put( user.getFullName(), user ); - } - - public Collection getUsers() { - return ImmutableSortedSet.copyOf( usersByName.values() ); - } - - public U getUserNamed(final String fullName) { - return usersByName.get( fullName ); - } - - public void addUser(final U user) { - usersByName.put( user.getFullName(), user ); - } - - public void deleteUser(final U user) { - usersByName.remove( user.getFullName() ); - } -} 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 b5a63d38..e0163fc9 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 @@ -63,13 +63,6 @@ public abstract class MPBasicSite extends Changeable imple return name; } - @Override - public void setName(final String name) { - this.name = name; - - setChanged(); - } - @Nonnull @Override public MPAlgorithm getAlgorithm() { 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 45d45016..c27ad308 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 @@ -27,6 +27,7 @@ import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPUser; import java.util.*; +import java.util.concurrent.CopyOnWriteArraySet; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -36,7 +37,8 @@ import javax.annotation.Nullable; */ public abstract class MPBasicUser> extends Changeable implements MPUser { - protected final Logger logger = Logger.get( getClass() ); + protected final Logger logger = Logger.get( getClass() ); + private final Set listeners = new CopyOnWriteArraySet<>(); private int avatar; private final String fullName; @@ -44,7 +46,7 @@ public abstract class MPBasicUser> extends Changeable i @Nullable protected MPMasterKey masterKey; - private final Collection sites = new LinkedHashSet<>(); + private final Map sites = new LinkedHashMap<>(); protected MPBasicUser(final String fullName, final MPAlgorithm algorithm) { this( 0, fullName, algorithm ); @@ -128,6 +130,9 @@ public abstract class MPBasicUser> extends Changeable i throw new MPIncorrectMasterPasswordException( this ); this.masterKey = masterKey; + + for (final Listener listener : listeners) + listener.onUserAuthenticated( this ); } @Override @@ -147,14 +152,14 @@ public abstract class MPBasicUser> extends Changeable i @Override public void addSite(final S site) { - sites.add( site ); + sites.put( site.getName(), site ); setChanged(); } @Override public void deleteSite(final S site) { - sites.remove( site ); + sites.values().remove( site ); setChanged(); } @@ -162,7 +167,7 @@ public abstract class MPBasicUser> extends Changeable i @Nonnull @Override public Collection getSites() { - return Collections.unmodifiableCollection( sites ); + return Collections.unmodifiableCollection( sites.values() ); } @Nonnull @@ -176,6 +181,24 @@ public abstract class MPBasicUser> extends Changeable i return results.build(); } + @Override + public boolean addListener(final Listener listener) { + return listeners.add( listener ); + } + + @Override + public boolean removeListener(final Listener listener) { + return listeners.remove( listener ); + } + + @Override + protected void onChanged() { + super.onChanged(); + + for (final Listener listener : listeners) + listener.onUserUpdated( this ); + } + @Override public int hashCode() { return Objects.hashCode( getFullName() ); 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 6d095005..158768ce 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 @@ -21,7 +21,8 @@ package com.lyndir.masterpassword.model.impl; import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPUser; -import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; import javax.annotation.Nullable; import org.joda.time.Instant; import org.joda.time.ReadableInstant; @@ -35,32 +36,31 @@ public class MPFileUser extends MPBasicUser { @Nullable private byte[] keyID; + private File path; private MPMarshalFormat format; private MPMarshaller.ContentMode contentMode; private MPResultType defaultType; private ReadableInstant lastUsed; - @Nullable - private MPJSONFile json; - public MPFileUser(final String fullName) { this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm() ); } public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm) { - this( fullName, keyID, algorithm, 0, algorithm.mpw_default_result_type(), new Instant(), - MPMarshalFormat.DEFAULT, MPMarshaller.ContentMode.PROTECTED ); + this( fullName, keyID, algorithm, 0, null, new Instant(), + MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, MPFileUserManager.get().getPath() ); } public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, - final int avatar, final MPResultType defaultType, final ReadableInstant lastUsed, - final MPMarshalFormat format, final MPMarshaller.ContentMode contentMode) { + final int avatar, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed, + final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File path) { super( avatar, fullName, algorithm ); - this.keyID = (keyID == null)? null: keyID.clone(); - this.defaultType = defaultType; + this.keyID = (keyID != null)? keyID.clone(): null; + this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type(); this.lastUsed = lastUsed; + this.path = path; this.format = format; this.contentMode = contentMode; } @@ -131,15 +131,8 @@ public class MPFileUser extends MPBasicUser { setChanged(); } - public void setJSON(final MPJSONFile json) { - this.json = json; - - setChanged(); - } - - @Nonnull - public MPJSONFile getJSON() { - return (json == null)? json = new MPJSONFile(): json; + public File getFile() { + return new File( path, getFullName() + getFormat().fileSuffix() ); } @Override @@ -147,6 +140,13 @@ public class MPFileUser extends MPBasicUser { throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { super.authenticate( masterKey ); + try { + getFormat().unmarshaller().readSites( this ); + } + catch (final IOException | MPMarshalException e) { + logger.err( e, "While reading sites on authentication." ); + } + if (keyID == null) { keyID = masterKey.getKeyID( getAlgorithm() ); @@ -156,19 +156,17 @@ public class MPFileUser extends MPBasicUser { @Override protected void onChanged() { - super.onChanged(); - try { - save(); + getFormat().marshaller().marshall( this ); } - catch (final MPKeyUnavailableException | MPAlgorithmException e) { - logger.wrn( e, "Couldn't save change." ); + 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 ); } - } - void save() - throws MPKeyUnavailableException, MPAlgorithmException { - MPFileUserManager.get().save( this, getMasterKey() ); + super.onChanged(); } @Override 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 09f0e470..fe7956c8 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 @@ -20,16 +20,13 @@ package com.lyndir.masterpassword.model.impl; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; -import com.google.common.base.Charsets; -import com.google.common.collect.ImmutableList; -import com.google.common.io.CharSink; +import com.google.common.collect.ImmutableSortedSet; import com.lyndir.lhunath.opal.system.logging.Logger; -import com.lyndir.masterpassword.*; -import com.lyndir.masterpassword.model.*; -import java.io.*; +import com.lyndir.masterpassword.model.MPConstants; +import java.io.File; +import java.io.IOException; import java.util.HashMap; import java.util.Map; -import javax.annotation.Nonnull; /** @@ -38,7 +35,7 @@ import javax.annotation.Nonnull; * @author lhunath, 14-12-07 */ @SuppressWarnings("CallToSystemGetenv") -public class MPFileUserManager extends MPUserManager { +public class MPFileUserManager { @SuppressWarnings("UnusedDeclaration") private static final Logger logger = Logger.get( MPFileUserManager.class ); @@ -46,13 +43,17 @@ public class MPFileUserManager extends MPUserManager { static { String rcDir = System.getenv( MPConstants.env_rcDir ); + if (rcDir != null) instance = create( new File( rcDir ) ); - else - instance = create( new File( ifNotNullElseNullable( System.getProperty( "user.home" ), System.getenv( "HOME" ) ), ".mpw.d" ) ); + else { + String home = ifNotNullElseNullable( System.getProperty( "user.home" ), System.getenv( "HOME" ) ); + instance = create( new File( home, ".mpw.d" ) ); + } } - private final File path; + private final Map userByName = new HashMap<>(); + private final File path; public static MPFileUserManager get() { return instance; @@ -63,86 +64,53 @@ public class MPFileUserManager extends MPUserManager { } protected MPFileUserManager(final File path) { - - super( unmarshallUsers( path ) ); this.path = path; } - private static Iterable unmarshallUsers(final File userFilesDirectory) { - if (!userFilesDirectory.mkdirs() && !userFilesDirectory.isDirectory()) { - logger.err( "Couldn't create directory for user files: %s", userFilesDirectory ); - return ImmutableList.of(); + public void reload() { + userByName.clear(); + + File[] pathFiles; + if ((!path.exists() && !path.mkdirs()) || ((pathFiles = path.listFiles()) == null)) { + logger.err( "Couldn't create directory for user files: %s", path ); + return; } - Map users = new HashMap<>(); - for (final File userFile : listUserFiles( userFilesDirectory )) + for (final File file : pathFiles) for (final MPMarshalFormat format : MPMarshalFormat.values()) - if (userFile.getName().endsWith( format.fileSuffix() )) + if (file.getName().endsWith( format.fileSuffix() )) try { - MPFileUser user = format.unmarshaller().unmarshall( userFile, null ); - MPFileUser previousUser = users.put( user.getFullName(), user ); + MPFileUser user = format.unmarshaller().readUser( file ); + MPFileUser previousUser = userByName.put( user.getFullName(), user ); if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal())) - users.put( previousUser.getFullName(), previousUser ); + userByName.put( previousUser.getFullName(), previousUser ); + break; } catch (final IOException | MPMarshalException e) { - logger.err( e, "Couldn't read user from: %s", userFile ); + logger.err( e, "Couldn't read user from: %s", file ); } - catch (final MPKeyUnavailableException | MPIncorrectMasterPasswordException | MPAlgorithmException e) { - logger.err( e, "Couldn't authenticate user for: %s", userFile ); - } - - return users.values(); } - private static ImmutableList listUserFiles(final File userFilesDirectory) { - return ImmutableList.copyOf( ifNotNullElse( userFilesDirectory.listFiles( (dir, name) -> { - for (final MPMarshalFormat format : MPMarshalFormat.values()) - if (name.endsWith( format.fileSuffix() )) - return true; - - return false; - } ), new File[0] ) ); + public MPFileUser add(final String fullName) { + MPFileUser user = new MPFileUser( fullName ); + userByName.put( user.getFullName(), user ); + return user; } - @Override - public void deleteUser(final MPFileUser user) { - super.deleteUser( user ); - + public void delete(final MPFileUser user) { // Remove deleted users. - File userFile = getUserFile( user, user.getFormat() ); + File userFile = user.getFile(); if (userFile.exists() && !userFile.delete()) logger.err( "Couldn't delete file: %s", userFile ); + else + userByName.values().remove( user ); } - /** - * Write the current user state to disk. - */ - public void save(final MPFileUser user, final MPMasterKey masterKey) - throws MPKeyUnavailableException, MPAlgorithmException { - try { - MPMarshalFormat format = user.getFormat(); - new CharSink() { - @Override - public Writer openStream() - throws IOException { - return new OutputStreamWriter( new FileOutputStream( getUserFile( user, format ) ), Charsets.UTF_8 ); - } - }.write( format.marshaller().marshall( user ) ); - } - catch (final MPMarshalException | IOException e) { - logger.err( e, "Unable to save sites for user: %s", user ); - } - } - - @Nonnull - private File getUserFile(final MPUser user, final MPMarshalFormat format) { - return new File( path, user.getFullName() + format.fileSuffix() ); - } - - /** - * @return The location on the file system where the user models are stored. - */ public File getPath() { return path; } + + public ImmutableSortedSet getFiles() { + return ImmutableSortedSet.copyOf( userByName.values() ); + } } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatMarshaller.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatMarshaller.java index 520f94c2..552c5590 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatMarshaller.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatMarshaller.java @@ -21,10 +21,12 @@ package com.lyndir.masterpassword.model.impl; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*; +import com.google.common.base.Charsets; +import com.google.common.io.CharSink; import com.lyndir.masterpassword.MPAlgorithmException; import com.lyndir.masterpassword.MPKeyUnavailableException; import com.lyndir.masterpassword.model.MPConstants; -import javax.annotation.Nonnull; +import java.io.*; import org.joda.time.Instant; @@ -36,10 +38,9 @@ public class MPFlatMarshaller implements MPMarshaller { private static final int FORMAT = 1; - @Nonnull @Override - public String marshall(final MPFileUser user) - throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException { + public void marshall(final MPFileUser user) + throws IOException, MPKeyUnavailableException, MPMarshalException, MPAlgorithmException { StringBuilder content = new StringBuilder(); content.append( "# Master Password site export\n" ); content.append( "# " ).append( user.getContentMode().description() ).append( '\n' ); @@ -80,6 +81,12 @@ public class MPFlatMarshaller implements MPMarshaller { ) ); } - return content.toString(); + new CharSink() { + @Override + public Writer openStream() + throws IOException { + return new OutputStreamWriter( new FileOutputStream( user.getFile() ), Charsets.UTF_8 ); + } + }.write( content.toString() ); } } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatUnmarshaller.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatUnmarshaller.java index e3b00dbd..6aa8be48 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatUnmarshaller.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFlatUnmarshaller.java @@ -18,7 +18,7 @@ package com.lyndir.masterpassword.model.impl; -import com.google.common.base.*; +import com.google.common.base.Charsets; import com.google.common.io.CharStreams; import com.google.common.primitives.UnsignedInteger; import com.lyndir.lhunath.opal.system.CodeUtils; @@ -31,7 +31,6 @@ import java.io.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import org.joda.time.Instant; @@ -49,114 +48,146 @@ public class MPFlatUnmarshaller implements MPUnmarshaller { @Nonnull @Override - public MPFileUser unmarshall(@Nonnull final File file, @Nullable final char[] masterPassword) - throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { + public MPFileUser readUser(@Nonnull final File file) + throws IOException, MPMarshalException { try (Reader reader = new InputStreamReader( new FileInputStream( file ), Charsets.UTF_8 )) { - return unmarshall( CharStreams.toString( reader ), masterPassword ); - } - } + byte[] keyID = null; + String fullName = null; + int mpVersion = 0, avatar = 0; + boolean clearContent = false, headerStarted = false; + MPResultType defaultType = null; - @Nonnull - @Override - public MPFileUser unmarshall(@Nonnull final String content, @Nullable final char[] masterPassword) - throws MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { - MPFileUser user = null; - byte[] keyID = null; - String fullName = null; - int mpVersion = 0, importFormat = 0, avatar = 0; - boolean clearContent = false, headerStarted = false; - MPResultType defaultType = null; - - //noinspection HardcodedLineSeparator - for (final String line : Splitter.on( CharMatcher.anyOf( "\r\n" ) ).omitEmptyStrings().split( content )) - // Header delimitor. - if (line.startsWith( "##" )) - if (!headerStarted) - // Starts the header. - headerStarted = true; - else { - // Ends the header. - user = new MPFileUser( fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(), - avatar, defaultType, new Instant( 0 ), MPMarshalFormat.Flat, - clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED ); - user.ignoreChanges(); + //noinspection HardcodedLineSeparator + for (final String line : CharStreams.readLines( reader )) + // Header delimitor. + if (line.startsWith( "##" )) { + if (!headerStarted) + // Starts the header. + headerStarted = true; + else if ((fullName != null) && (keyID != null)) + // Ends the header. + return new MPFileUser( fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(), + avatar, defaultType, new Instant( 0 ), + clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED, + MPMarshalFormat.Flat, file.getParentFile() ); } // Comment. - else if (line.startsWith( "#" )) { - if (headerStarted && (user == null)) { - // In header. - Matcher headerMatcher = headerFormat.matcher( line ); - if (headerMatcher.matches()) { - String name = headerMatcher.group( 1 ), value = headerMatcher.group( 2 ); - if ("Full Name".equalsIgnoreCase( name ) || "User Name".equalsIgnoreCase( name )) - fullName = value; - else if ("Key ID".equalsIgnoreCase( name )) - keyID = CodeUtils.decodeHex( value ); - else if ("Algorithm".equalsIgnoreCase( name )) - mpVersion = ConversionUtils.toIntegerNN( value ); - else if ("Format".equalsIgnoreCase( name )) - importFormat = ConversionUtils.toIntegerNN( value ); - else if ("Avatar".equalsIgnoreCase( name )) - avatar = ConversionUtils.toIntegerNN( value ); - else if ("Passwords".equalsIgnoreCase( name )) - clearContent = "visible".equalsIgnoreCase( value ); - else if ("Default Type".equalsIgnoreCase( name )) - defaultType = MPResultType.forType( ConversionUtils.toIntegerNN( value ) ); + else if (line.startsWith( "#" )) { + if (headerStarted) { + // In header. + Matcher headerMatcher = headerFormat.matcher( line ); + if (headerMatcher.matches()) { + String name = headerMatcher.group( 1 ), value = headerMatcher.group( 2 ); + if ("Full Name".equalsIgnoreCase( name ) || "User Name".equalsIgnoreCase( name )) + fullName = value; + else if ("Key ID".equalsIgnoreCase( name )) + keyID = CodeUtils.decodeHex( value ); + else if ("Algorithm".equalsIgnoreCase( name )) + mpVersion = ConversionUtils.toIntegerNN( value ); + else if ("Avatar".equalsIgnoreCase( name )) + avatar = ConversionUtils.toIntegerNN( value ); + else if ("Passwords".equalsIgnoreCase( name )) + clearContent = "visible".equalsIgnoreCase( value ); + else if ("Default Type".equalsIgnoreCase( name )) + defaultType = MPResultType.forType( ConversionUtils.toIntegerNN( value ) ); + } } } - } - // No comment. - else if (user != null) { - Matcher siteMatcher = unmarshallFormats[importFormat].matcher( line ); - if (!siteMatcher.matches()) { - logger.wrn( "Couldn't parse line: %s, skipping.", line ); - continue; - } - - MPFileSite site; - switch (importFormat) { - case 0: - site = new MPFileSite( user, // - siteMatcher.group( 5 ), MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN( - colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(), - user.getAlgorithm().mpw_default_counter(), - MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ), - clearContent? null: siteMatcher.group( 6 ), - null, null, null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), - MPConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() ); - if (clearContent) - site.setSitePassword( site.getResultType(), siteMatcher.group( 6 ) ); - break; - - case 1: - site = new MPFileSite( user, // - siteMatcher.group( 7 ), MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN( - colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(), - UnsignedInteger.valueOf( colon.matcher( siteMatcher.group( 5 ) ).replaceAll( "" ) ), - MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ), - clearContent? null: siteMatcher.group( 8 ), - MPResultType.GeneratedName, clearContent? null: siteMatcher.group( 6 ), null, - ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), - MPConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() ); - if (clearContent) { - site.setSitePassword( site.getResultType(), siteMatcher.group( 8 ) ); - site.setLoginName( MPResultType.StoredPersonal, siteMatcher.group( 6 ) ); - } - break; - - default: - throw new MPMarshalException( "Unexpected format: " + importFormat ); - } - - user.addSite( site ); - } - - if (user == null) throw new MPMarshalException( "No full header found in import file." ); + } + } + + @Override + public void readSites(final MPFileUser user) + throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { + user.ignoreChanges(); + + try (Reader reader = new InputStreamReader( new FileInputStream( user.getFile() ), Charsets.UTF_8 )) { + byte[] keyID = null; + String fullName = null; + int mpVersion = 0, importFormat = 0, avatar = 0; + boolean clearContent = false, headerStarted = false, headerEnded = false; + MPResultType defaultType = null; + + //noinspection HardcodedLineSeparator + for (final String line : CharStreams.readLines( reader )) + // Header delimitor. + if (line.startsWith( "##" )) { + if (!headerStarted) + // Starts the header. + headerStarted = true; + else + // Ends the header. + headerEnded = true; + } + + // Comment. + else if (line.startsWith( "#" )) { + if (headerStarted && !headerEnded) { + // In header. + Matcher headerMatcher = headerFormat.matcher( line ); + if (headerMatcher.matches()) { + String name = headerMatcher.group( 1 ), value = headerMatcher.group( 2 ); + if ("Format".equalsIgnoreCase( name )) + importFormat = ConversionUtils.toIntegerNN( value ); + else if ("Passwords".equalsIgnoreCase( name )) + clearContent = "visible".equalsIgnoreCase( value ); + } + } + } + + // No comment. + else if (headerEnded) { + Matcher siteMatcher = unmarshallFormats[importFormat].matcher( line ); + if (!siteMatcher.matches()) { + logger.wrn( "Couldn't parse line: %s, skipping.", line ); + continue; + } + + MPFileSite site; + switch (importFormat) { + case 0: + site = new MPFileSite( user, // + siteMatcher.group( 5 ), MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN( + colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(), + user.getAlgorithm().mpw_default_counter(), + MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ), + clearContent? null: siteMatcher.group( 6 ), + null, null, null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), + MPConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() ); + if (clearContent) + site.setSitePassword( site.getResultType(), siteMatcher.group( 6 ) ); + break; + + case 1: + site = new MPFileSite( user, // + siteMatcher.group( 7 ), MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN( + colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(), + UnsignedInteger.valueOf( colon.matcher( siteMatcher.group( 5 ) ).replaceAll( "" ) ), + MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ), + clearContent? null: siteMatcher.group( 8 ), + MPResultType.GeneratedName, clearContent? null: siteMatcher.group( 6 ), null, + ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), + MPConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() ); + if (clearContent) { + site.setSitePassword( site.getResultType(), siteMatcher.group( 8 ) ); + site.setLoginName( MPResultType.StoredPersonal, siteMatcher.group( 6 ) ); + } + break; + + default: + throw new MPMarshalException( "Unexpected format: " + importFormat ); + } + + user.addSite( site ); + } + + if (user == null) + throw new MPMarshalException( "No full header found in import file." ); + } user.endChanges(); - return user; } } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java index 12521888..7e5959aa 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java @@ -31,6 +31,7 @@ import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.model.MPConstants; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.File; import java.util.LinkedHashMap; import java.util.Map; import javax.annotation.Nullable; @@ -59,19 +60,20 @@ public class MPJSONFile extends MPJSONAnyObject { objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE ); } - public MPJSONFile write(final MPFileUser modelUser) - throws MPKeyUnavailableException, MPAlgorithmException { + public MPJSONFile() { + } + + public MPJSONFile(final MPFileUser modelUser) + throws MPAlgorithmException, MPKeyUnavailableException { // Section: "export" - if (export == null) - export = new Export(); + export = new Export(); export.format = 1; export.redacted = modelUser.getContentMode().isRedacted(); export.date = MPConstants.dateTimeFormatter.print( new Instant() ); // Section: "user" - if (user == null) - user = new User(); + user = new User(); user.avatar = modelUser.getAvatar(); user.full_name = modelUser.getFullName(); user.last_used = MPConstants.dateTimeFormatter.print( modelUser.getLastUsed() ); @@ -84,6 +86,7 @@ public class MPJSONFile extends MPJSONAnyObject { sites = new LinkedHashMap<>(); for (final MPFileSite modelSite : modelUser.getSites()) { String content = null, loginContent = null; + if (!export.redacted) { // Clear Text content = modelSite.getResult(); @@ -111,8 +114,7 @@ public class MPJSONFile extends MPJSONAnyObject { site.uses = modelSite.getUses(); site.last_used = MPConstants.dateTimeFormatter.print( modelSite.getLastUsed() ); - if (site.questions == null) - site.questions = new LinkedHashMap<>(); + site.questions = new LinkedHashMap<>(); for (final MPFileQuestion question : modelSite.getQuestions()) site.questions.put( question.getKeyword(), new Site.Question() { { @@ -129,32 +131,32 @@ public class MPJSONFile extends MPJSONAnyObject { } } ); - if (site._ext_mpw == null) - site._ext_mpw = new Site.Ext(); + site._ext_mpw = new Site.Ext(); site._ext_mpw.url = modelSite.getUrl(); } - - return this; } - public MPFileUser read(@Nullable final char[] masterPassword) - throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { + public MPFileUser readUser(final File file) { MPAlgorithm algorithm = ifNotNullElse( user.algorithm, MPAlgorithm.Version.CURRENT ).getAlgorithm(); - MPFileUser model = new MPFileUser( + + return new MPFileUser( user.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar, (user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(), (user.last_used != null)? MPConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(), - MPMarshalFormat.JSON, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE ); - model.ignoreChanges(); - model.setJSON( this ); - if (masterPassword != null) - model.authenticate( masterPassword ); + export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE, + MPMarshalFormat.JSON, file.getParentFile() + ); + } + + public void readSites(final MPFileUser user) + throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { + user.ignoreChanges(); for (final Map.Entry siteEntry : sites.entrySet()) { String siteName = siteEntry.getKey(); Site fileSite = siteEntry.getValue(); MPFileSite site = new MPFileSite( - model, siteName, fileSite.algorithm.getAlgorithm(), UnsignedInteger.valueOf( fileSite.counter ), fileSite.type, + user, siteName, fileSite.algorithm.getAlgorithm(), UnsignedInteger.valueOf( fileSite.counter ), fileSite.type, export.redacted? fileSite.password: null, fileSite.login_type, export.redacted? fileSite.login_name: null, (fileSite._ext_mpw != null)? fileSite._ext_mpw.url: null, fileSite.uses, @@ -168,18 +170,17 @@ public class MPJSONFile extends MPJSONAnyObject { fileSite.login_name ); } - model.addSite( site ); + user.addSite( site ); } - model.endChanges(); - return model; + user.endChanges(); } // -- Data - Export export; - User user; - Map sites; + Export export = new Export(); + User user = new User(); + Map sites = new LinkedHashMap<>(); public static class Export extends MPJSONAnyObject { @@ -210,7 +211,7 @@ public class MPJSONFile extends MPJSONAnyObject { @Nullable MPResultType type; long counter; - MPAlgorithm.Version algorithm; + MPAlgorithm.Version algorithm = MPAlgorithm.Version.CURRENT; @Nullable String password; @Nullable diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONMarshaller.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONMarshaller.java index b8563ce7..2e39fc7d 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONMarshaller.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONMarshaller.java @@ -23,6 +23,7 @@ import static com.lyndir.masterpassword.model.impl.MPJSONFile.*; import com.fasterxml.jackson.core.JsonProcessingException; import com.lyndir.masterpassword.MPAlgorithmException; import com.lyndir.masterpassword.MPKeyUnavailableException; +import java.io.IOException; import javax.annotation.Nonnull; @@ -33,11 +34,11 @@ public class MPJSONMarshaller implements MPMarshaller { @Nonnull @Override - public String marshall(final MPFileUser user) - throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException { + public void marshall(final MPFileUser user) + throws IOException, MPKeyUnavailableException, MPMarshalException, MPAlgorithmException { try { - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString( user.getJSON().write( user ) ); + objectMapper.writerWithDefaultPrettyPrinter().writeValue( user.getFile(), new MPJSONFile( user ) ); } catch (final JsonProcessingException e) { throw new MPMarshalException( "Couldn't compose JSON for: " + user, e ); diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONUnmarshaller.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONUnmarshaller.java index 3aac982c..2d44cc02 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONUnmarshaller.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONUnmarshaller.java @@ -28,7 +28,6 @@ import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import java.io.File; import java.io.IOException; import javax.annotation.Nonnull; -import javax.annotation.Nullable; /** @@ -38,27 +37,11 @@ public class MPJSONUnmarshaller implements MPUnmarshaller { @Nonnull @Override - public MPFileUser unmarshall(@Nonnull final File file, @Nullable final char[] masterPassword) - throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { + public MPFileUser readUser(@Nonnull final File file) + throws IOException, MPMarshalException { try { - return objectMapper.readValue( file, MPJSONFile.class ).read( masterPassword ); - } - catch (final JsonParseException e) { - throw new MPMarshalException( "Couldn't parse JSON in: " + file, e ); - } - catch (final JsonMappingException e) { - throw new MPMarshalException( "Couldn't map JSON in: " + file, e ); - } - } - - @Nonnull - @Override - public MPFileUser unmarshall(@Nonnull final String content, @Nullable final char[] masterPassword) - throws MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { - - try { - return objectMapper.readValue( content, MPJSONFile.class ).read( masterPassword ); + return objectMapper.readValue( file, MPJSONFile.class ).readUser( file ); } catch (final JsonParseException e) { throw new MPMarshalException( "Couldn't parse JSON.", e ); @@ -66,8 +49,20 @@ public class MPJSONUnmarshaller implements MPUnmarshaller { catch (final JsonMappingException e) { throw new MPMarshalException( "Couldn't map JSON.", e ); } - catch (final IOException e) { - throw new MPMarshalException( "Couldn't read JSON.", e ); + } + + @Override + public void readSites(final MPFileUser user) + throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { + + try { + objectMapper.readValue( user.getFile(), MPJSONFile.class ).readSites( user ); + } + catch (final JsonParseException e) { + throw new MPMarshalException( "Couldn't parse JSON.", e ); + } + catch (final JsonMappingException e) { + throw new MPMarshalException( "Couldn't map JSON.", e ); } } } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPMarshaller.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPMarshaller.java index 5dc03b24..668d1576 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPMarshaller.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPMarshaller.java @@ -20,7 +20,7 @@ package com.lyndir.masterpassword.model.impl; import com.lyndir.masterpassword.MPAlgorithmException; import com.lyndir.masterpassword.MPKeyUnavailableException; -import javax.annotation.Nonnull; +import java.io.IOException; /** @@ -29,9 +29,8 @@ import javax.annotation.Nonnull; @FunctionalInterface public interface MPMarshaller { - @Nonnull - String marshall(MPFileUser user) - throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException; + void marshall(MPFileUser user) + throws IOException, MPKeyUnavailableException, MPMarshalException, MPAlgorithmException; enum ContentMode { PROTECTED( "Export of site names and stored passwords (unless device-private) encrypted with the master key.", true ), diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPUnmarshaller.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPUnmarshaller.java index 6fbfe593..d52949f7 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPUnmarshaller.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPUnmarshaller.java @@ -24,7 +24,6 @@ import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import java.io.File; import java.io.IOException; import javax.annotation.Nonnull; -import javax.annotation.Nullable; /** @@ -33,10 +32,9 @@ import javax.annotation.Nullable; public interface MPUnmarshaller { @Nonnull - MPFileUser unmarshall(@Nonnull File file, @Nullable char[] masterPassword) - throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException; + MPFileUser readUser(File file) + throws IOException, MPMarshalException; - @Nonnull - MPFileUser unmarshall(@Nonnull String content, @Nullable char[] masterPassword) - throws MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException; + void readSites(MPFileUser user) + throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException; }