2
0

Refactor model, improved isolation & access unauthenticated file metadata.

This commit is contained in:
Maarten Billemont 2018-07-18 12:23:53 -04:00
parent a16bc9a318
commit 80b5fcd785
14 changed files with 305 additions and 333 deletions

View File

@ -35,8 +35,6 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
@Nonnull @Nonnull
String getName(); String getName();
void setName(String name);
// - Algorithm // - Algorithm
@Nonnull @Nonnull

View File

@ -19,6 +19,8 @@
package com.lyndir.masterpassword.model; package com.lyndir.masterpassword.model;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.impl.MPBasicSite;
import com.lyndir.masterpassword.model.impl.MPBasicUser;
import java.util.Collection; import java.util.Collection;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -93,4 +95,15 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
@Nonnull @Nonnull
Collection<S> findSites(String query); Collection<S> findSites(String query);
boolean addListener(Listener listener);
boolean removeListener(Listener listener);
interface Listener {
void onUserUpdated(MPUser<?> user);
void onUserAuthenticated(MPUser<?> user);
}
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
//==============================================================================
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<U extends MPUser<?>> {
private final Map<String, U> usersByName = Maps.newHashMap();
protected MPUserManager(final Iterable<U> users) {
for (final U user : users)
usersByName.put( user.getFullName(), user );
}
public Collection<U> 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() );
}
}

View File

@ -63,13 +63,6 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
return name; return name;
} }
@Override
public void setName(final String name) {
this.name = name;
setChanged();
}
@Nonnull @Nonnull
@Override @Override
public MPAlgorithm getAlgorithm() { public MPAlgorithm getAlgorithm() {

View File

@ -27,6 +27,7 @@ import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.MPUser;
import java.util.*; import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -36,7 +37,8 @@ import javax.annotation.Nullable;
*/ */
public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable implements MPUser<S> { public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable implements MPUser<S> {
protected final Logger logger = Logger.get( getClass() ); protected final Logger logger = Logger.get( getClass() );
private final Set<Listener> listeners = new CopyOnWriteArraySet<>();
private int avatar; private int avatar;
private final String fullName; private final String fullName;
@ -44,7 +46,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
@Nullable @Nullable
protected MPMasterKey masterKey; protected MPMasterKey masterKey;
private final Collection<S> sites = new LinkedHashSet<>(); private final Map<String, S> sites = new LinkedHashMap<>();
protected MPBasicUser(final String fullName, final MPAlgorithm algorithm) { protected MPBasicUser(final String fullName, final MPAlgorithm algorithm) {
this( 0, fullName, algorithm ); this( 0, fullName, algorithm );
@ -128,6 +130,9 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
throw new MPIncorrectMasterPasswordException( this ); throw new MPIncorrectMasterPasswordException( this );
this.masterKey = masterKey; this.masterKey = masterKey;
for (final Listener listener : listeners)
listener.onUserAuthenticated( this );
} }
@Override @Override
@ -147,14 +152,14 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
@Override @Override
public void addSite(final S site) { public void addSite(final S site) {
sites.add( site ); sites.put( site.getName(), site );
setChanged(); setChanged();
} }
@Override @Override
public void deleteSite(final S site) { public void deleteSite(final S site) {
sites.remove( site ); sites.values().remove( site );
setChanged(); setChanged();
} }
@ -162,7 +167,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
@Nonnull @Nonnull
@Override @Override
public Collection<S> getSites() { public Collection<S> getSites() {
return Collections.unmodifiableCollection( sites ); return Collections.unmodifiableCollection( sites.values() );
} }
@Nonnull @Nonnull
@ -176,6 +181,24 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
return results.build(); 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 @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode( getFullName() ); return Objects.hashCode( getFullName() );

View File

@ -21,7 +21,8 @@ package com.lyndir.masterpassword.model.impl;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.MPUser;
import javax.annotation.Nonnull; import java.io.File;
import java.io.IOException;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.joda.time.Instant; import org.joda.time.Instant;
import org.joda.time.ReadableInstant; import org.joda.time.ReadableInstant;
@ -35,32 +36,31 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
@Nullable @Nullable
private byte[] keyID; private byte[] keyID;
private File path;
private MPMarshalFormat format; private MPMarshalFormat format;
private MPMarshaller.ContentMode contentMode; private MPMarshaller.ContentMode contentMode;
private MPResultType defaultType; private MPResultType defaultType;
private ReadableInstant lastUsed; private ReadableInstant lastUsed;
@Nullable
private MPJSONFile json;
public MPFileUser(final String fullName) { public MPFileUser(final String fullName) {
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm() ); this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm() );
} }
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm) { public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm) {
this( fullName, keyID, algorithm, 0, algorithm.mpw_default_result_type(), new Instant(), this( fullName, keyID, algorithm, 0, null, new Instant(),
MPMarshalFormat.DEFAULT, MPMarshaller.ContentMode.PROTECTED ); MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, MPFileUserManager.get().getPath() );
} }
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm,
final int avatar, final MPResultType defaultType, final ReadableInstant lastUsed, final int avatar, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed,
final MPMarshalFormat format, final MPMarshaller.ContentMode contentMode) { final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File path) {
super( avatar, fullName, algorithm ); super( avatar, fullName, algorithm );
this.keyID = (keyID == null)? null: keyID.clone(); this.keyID = (keyID != null)? keyID.clone(): null;
this.defaultType = defaultType; this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type();
this.lastUsed = lastUsed; this.lastUsed = lastUsed;
this.path = path;
this.format = format; this.format = format;
this.contentMode = contentMode; this.contentMode = contentMode;
} }
@ -131,15 +131,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
setChanged(); setChanged();
} }
public void setJSON(final MPJSONFile json) { public File getFile() {
this.json = json; return new File( path, getFullName() + getFormat().fileSuffix() );
setChanged();
}
@Nonnull
public MPJSONFile getJSON() {
return (json == null)? json = new MPJSONFile(): json;
} }
@Override @Override
@ -147,6 +140,13 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
super.authenticate( masterKey ); super.authenticate( masterKey );
try {
getFormat().unmarshaller().readSites( this );
}
catch (final IOException | MPMarshalException e) {
logger.err( e, "While reading sites on authentication." );
}
if (keyID == null) { if (keyID == null) {
keyID = masterKey.getKeyID( getAlgorithm() ); keyID = masterKey.getKeyID( getAlgorithm() );
@ -156,19 +156,17 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
@Override @Override
protected void onChanged() { protected void onChanged() {
super.onChanged();
try { try {
save(); getFormat().marshaller().marshall( this );
} }
catch (final MPKeyUnavailableException | MPAlgorithmException e) { catch (final MPKeyUnavailableException e) {
logger.wrn( e, "Couldn't save change." ); 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() super.onChanged();
throws MPKeyUnavailableException, MPAlgorithmException {
MPFileUserManager.get().save( this, getMasterKey() );
} }
@Override @Override

View File

@ -20,16 +20,13 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import com.google.common.base.Charsets; import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharSink;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.model.MPConstants;
import com.lyndir.masterpassword.model.*; import java.io.File;
import java.io.*; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.annotation.Nonnull;
/** /**
@ -38,7 +35,7 @@ import javax.annotation.Nonnull;
* @author lhunath, 14-12-07 * @author lhunath, 14-12-07
*/ */
@SuppressWarnings("CallToSystemGetenv") @SuppressWarnings("CallToSystemGetenv")
public class MPFileUserManager extends MPUserManager<MPFileUser> { public class MPFileUserManager {
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPFileUserManager.class ); private static final Logger logger = Logger.get( MPFileUserManager.class );
@ -46,13 +43,17 @@ public class MPFileUserManager extends MPUserManager<MPFileUser> {
static { static {
String rcDir = System.getenv( MPConstants.env_rcDir ); String rcDir = System.getenv( MPConstants.env_rcDir );
if (rcDir != null) if (rcDir != null)
instance = create( new File( rcDir ) ); instance = create( new File( rcDir ) );
else else {
instance = create( new File( ifNotNullElseNullable( System.getProperty( "user.home" ), System.getenv( "HOME" ) ), ".mpw.d" ) ); String home = ifNotNullElseNullable( System.getProperty( "user.home" ), System.getenv( "HOME" ) );
instance = create( new File( home, ".mpw.d" ) );
}
} }
private final File path; private final Map<String, MPFileUser> userByName = new HashMap<>();
private final File path;
public static MPFileUserManager get() { public static MPFileUserManager get() {
return instance; return instance;
@ -63,86 +64,53 @@ public class MPFileUserManager extends MPUserManager<MPFileUser> {
} }
protected MPFileUserManager(final File path) { protected MPFileUserManager(final File path) {
super( unmarshallUsers( path ) );
this.path = path; this.path = path;
} }
private static Iterable<MPFileUser> unmarshallUsers(final File userFilesDirectory) { public void reload() {
if (!userFilesDirectory.mkdirs() && !userFilesDirectory.isDirectory()) { userByName.clear();
logger.err( "Couldn't create directory for user files: %s", userFilesDirectory );
return ImmutableList.of(); File[] pathFiles;
if ((!path.exists() && !path.mkdirs()) || ((pathFiles = path.listFiles()) == null)) {
logger.err( "Couldn't create directory for user files: %s", path );
return;
} }
Map<String, MPFileUser> users = new HashMap<>(); for (final File file : pathFiles)
for (final File userFile : listUserFiles( userFilesDirectory ))
for (final MPMarshalFormat format : MPMarshalFormat.values()) for (final MPMarshalFormat format : MPMarshalFormat.values())
if (userFile.getName().endsWith( format.fileSuffix() )) if (file.getName().endsWith( format.fileSuffix() ))
try { try {
MPFileUser user = format.unmarshaller().unmarshall( userFile, null ); MPFileUser user = format.unmarshaller().readUser( file );
MPFileUser previousUser = users.put( user.getFullName(), user ); MPFileUser previousUser = userByName.put( user.getFullName(), user );
if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal())) 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) { 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<File> listUserFiles(final File userFilesDirectory) { public MPFileUser add(final String fullName) {
return ImmutableList.copyOf( ifNotNullElse( userFilesDirectory.listFiles( (dir, name) -> { MPFileUser user = new MPFileUser( fullName );
for (final MPMarshalFormat format : MPMarshalFormat.values()) userByName.put( user.getFullName(), user );
if (name.endsWith( format.fileSuffix() )) return user;
return true;
return false;
} ), new File[0] ) );
} }
@Override public void delete(final MPFileUser user) {
public void deleteUser(final MPFileUser user) {
super.deleteUser( user );
// Remove deleted users. // Remove deleted users.
File userFile = getUserFile( user, user.getFormat() ); File userFile = user.getFile();
if (userFile.exists() && !userFile.delete()) if (userFile.exists() && !userFile.delete())
logger.err( "Couldn't delete file: %s", userFile ); 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() { public File getPath() {
return path; return path;
} }
public ImmutableSortedSet<MPFileUser> getFiles() {
return ImmutableSortedSet.copyOf( userByName.values() );
}
} }

View File

@ -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.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; 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.MPAlgorithmException;
import com.lyndir.masterpassword.MPKeyUnavailableException; import com.lyndir.masterpassword.MPKeyUnavailableException;
import com.lyndir.masterpassword.model.MPConstants; import com.lyndir.masterpassword.model.MPConstants;
import javax.annotation.Nonnull; import java.io.*;
import org.joda.time.Instant; import org.joda.time.Instant;
@ -36,10 +38,9 @@ public class MPFlatMarshaller implements MPMarshaller {
private static final int FORMAT = 1; private static final int FORMAT = 1;
@Nonnull
@Override @Override
public String marshall(final MPFileUser user) public void marshall(final MPFileUser user)
throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException { throws IOException, MPKeyUnavailableException, MPMarshalException, MPAlgorithmException {
StringBuilder content = new StringBuilder(); StringBuilder content = new StringBuilder();
content.append( "# Master Password site export\n" ); content.append( "# Master Password site export\n" );
content.append( "# " ).append( user.getContentMode().description() ).append( '\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() );
} }
} }

View File

@ -18,7 +18,7 @@
package com.lyndir.masterpassword.model.impl; 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.io.CharStreams;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.CodeUtils;
@ -31,7 +31,6 @@ import java.io.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.joda.time.Instant; import org.joda.time.Instant;
@ -49,114 +48,146 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
@Nonnull @Nonnull
@Override @Override
public MPFileUser unmarshall(@Nonnull final File file, @Nullable final char[] masterPassword) public MPFileUser readUser(@Nonnull final File file)
throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { throws IOException, MPMarshalException {
try (Reader reader = new InputStreamReader( new FileInputStream( file ), Charsets.UTF_8 )) { 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 //noinspection HardcodedLineSeparator
@Override for (final String line : CharStreams.readLines( reader ))
public MPFileUser unmarshall(@Nonnull final String content, @Nullable final char[] masterPassword) // Header delimitor.
throws MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { if (line.startsWith( "##" )) {
MPFileUser user = null; if (!headerStarted)
byte[] keyID = null; // Starts the header.
String fullName = null; headerStarted = true;
int mpVersion = 0, importFormat = 0, avatar = 0; else if ((fullName != null) && (keyID != null))
boolean clearContent = false, headerStarted = false; // Ends the header.
MPResultType defaultType = null; return new MPFileUser( fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(),
avatar, defaultType, new Instant( 0 ),
//noinspection HardcodedLineSeparator clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED,
for (final String line : Splitter.on( CharMatcher.anyOf( "\r\n" ) ).omitEmptyStrings().split( content )) MPMarshalFormat.Flat, file.getParentFile() );
// 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();
} }
// Comment. // Comment.
else if (line.startsWith( "#" )) { else if (line.startsWith( "#" )) {
if (headerStarted && (user == null)) { if (headerStarted) {
// In header. // In header.
Matcher headerMatcher = headerFormat.matcher( line ); Matcher headerMatcher = headerFormat.matcher( line );
if (headerMatcher.matches()) { if (headerMatcher.matches()) {
String name = headerMatcher.group( 1 ), value = headerMatcher.group( 2 ); String name = headerMatcher.group( 1 ), value = headerMatcher.group( 2 );
if ("Full Name".equalsIgnoreCase( name ) || "User Name".equalsIgnoreCase( name )) if ("Full Name".equalsIgnoreCase( name ) || "User Name".equalsIgnoreCase( name ))
fullName = value; fullName = value;
else if ("Key ID".equalsIgnoreCase( name )) else if ("Key ID".equalsIgnoreCase( name ))
keyID = CodeUtils.decodeHex( value ); keyID = CodeUtils.decodeHex( value );
else if ("Algorithm".equalsIgnoreCase( name )) else if ("Algorithm".equalsIgnoreCase( name ))
mpVersion = ConversionUtils.toIntegerNN( value ); mpVersion = ConversionUtils.toIntegerNN( value );
else if ("Format".equalsIgnoreCase( name )) else if ("Avatar".equalsIgnoreCase( name ))
importFormat = ConversionUtils.toIntegerNN( value ); avatar = ConversionUtils.toIntegerNN( value );
else if ("Avatar".equalsIgnoreCase( name )) else if ("Passwords".equalsIgnoreCase( name ))
avatar = ConversionUtils.toIntegerNN( value ); clearContent = "visible".equalsIgnoreCase( value );
else if ("Passwords".equalsIgnoreCase( name )) else if ("Default Type".equalsIgnoreCase( name ))
clearContent = "visible".equalsIgnoreCase( value ); defaultType = MPResultType.forType( ConversionUtils.toIntegerNN( 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." ); 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(); user.endChanges();
return user;
} }
} }

View File

@ -31,6 +31,7 @@ import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPConstants; import com.lyndir.masterpassword.model.MPConstants;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -59,19 +60,20 @@ public class MPJSONFile extends MPJSONAnyObject {
objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE ); objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE );
} }
public MPJSONFile write(final MPFileUser modelUser) public MPJSONFile() {
throws MPKeyUnavailableException, MPAlgorithmException { }
public MPJSONFile(final MPFileUser modelUser)
throws MPAlgorithmException, MPKeyUnavailableException {
// Section: "export" // Section: "export"
if (export == null) export = new Export();
export = new Export();
export.format = 1; export.format = 1;
export.redacted = modelUser.getContentMode().isRedacted(); export.redacted = modelUser.getContentMode().isRedacted();
export.date = MPConstants.dateTimeFormatter.print( new Instant() ); export.date = MPConstants.dateTimeFormatter.print( new Instant() );
// Section: "user" // Section: "user"
if (user == null) user = new User();
user = new User();
user.avatar = modelUser.getAvatar(); user.avatar = modelUser.getAvatar();
user.full_name = modelUser.getFullName(); user.full_name = modelUser.getFullName();
user.last_used = MPConstants.dateTimeFormatter.print( modelUser.getLastUsed() ); user.last_used = MPConstants.dateTimeFormatter.print( modelUser.getLastUsed() );
@ -84,6 +86,7 @@ public class MPJSONFile extends MPJSONAnyObject {
sites = new LinkedHashMap<>(); sites = new LinkedHashMap<>();
for (final MPFileSite modelSite : modelUser.getSites()) { for (final MPFileSite modelSite : modelUser.getSites()) {
String content = null, loginContent = null; String content = null, loginContent = null;
if (!export.redacted) { if (!export.redacted) {
// Clear Text // Clear Text
content = modelSite.getResult(); content = modelSite.getResult();
@ -111,8 +114,7 @@ public class MPJSONFile extends MPJSONAnyObject {
site.uses = modelSite.getUses(); site.uses = modelSite.getUses();
site.last_used = MPConstants.dateTimeFormatter.print( modelSite.getLastUsed() ); 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()) for (final MPFileQuestion question : modelSite.getQuestions())
site.questions.put( question.getKeyword(), new Site.Question() { 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(); site._ext_mpw.url = modelSite.getUrl();
} }
return this;
} }
public MPFileUser read(@Nullable final char[] masterPassword) public MPFileUser readUser(final File file) {
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
MPAlgorithm algorithm = ifNotNullElse( user.algorithm, MPAlgorithm.Version.CURRENT ).getAlgorithm(); 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.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar,
(user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(), (user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(),
(user.last_used != null)? MPConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(), (user.last_used != null)? MPConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
MPMarshalFormat.JSON, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE ); export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE,
model.ignoreChanges(); MPMarshalFormat.JSON, file.getParentFile()
model.setJSON( this ); );
if (masterPassword != null) }
model.authenticate( masterPassword );
public void readSites(final MPFileUser user)
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
user.ignoreChanges();
for (final Map.Entry<String, Site> siteEntry : sites.entrySet()) { for (final Map.Entry<String, Site> siteEntry : sites.entrySet()) {
String siteName = siteEntry.getKey(); String siteName = siteEntry.getKey();
Site fileSite = siteEntry.getValue(); Site fileSite = siteEntry.getValue();
MPFileSite site = new MPFileSite( 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, export.redacted? fileSite.password: null,
fileSite.login_type, export.redacted? fileSite.login_name: null, fileSite.login_type, export.redacted? fileSite.login_name: null,
(fileSite._ext_mpw != null)? fileSite._ext_mpw.url: null, fileSite.uses, (fileSite._ext_mpw != null)? fileSite._ext_mpw.url: null, fileSite.uses,
@ -168,18 +170,17 @@ public class MPJSONFile extends MPJSONAnyObject {
fileSite.login_name ); fileSite.login_name );
} }
model.addSite( site ); user.addSite( site );
} }
model.endChanges();
return model; user.endChanges();
} }
// -- Data // -- Data
Export export; Export export = new Export();
User user; User user = new User();
Map<String, Site> sites; Map<String, Site> sites = new LinkedHashMap<>();
public static class Export extends MPJSONAnyObject { public static class Export extends MPJSONAnyObject {
@ -210,7 +211,7 @@ public class MPJSONFile extends MPJSONAnyObject {
@Nullable @Nullable
MPResultType type; MPResultType type;
long counter; long counter;
MPAlgorithm.Version algorithm; MPAlgorithm.Version algorithm = MPAlgorithm.Version.CURRENT;
@Nullable @Nullable
String password; String password;
@Nullable @Nullable

View File

@ -23,6 +23,7 @@ import static com.lyndir.masterpassword.model.impl.MPJSONFile.*;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.lyndir.masterpassword.MPAlgorithmException; import com.lyndir.masterpassword.MPAlgorithmException;
import com.lyndir.masterpassword.MPKeyUnavailableException; import com.lyndir.masterpassword.MPKeyUnavailableException;
import java.io.IOException;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -33,11 +34,11 @@ public class MPJSONMarshaller implements MPMarshaller {
@Nonnull @Nonnull
@Override @Override
public String marshall(final MPFileUser user) public void marshall(final MPFileUser user)
throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException { throws IOException, MPKeyUnavailableException, MPMarshalException, MPAlgorithmException {
try { try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString( user.getJSON().write( user ) ); objectMapper.writerWithDefaultPrettyPrinter().writeValue( user.getFile(), new MPJSONFile( user ) );
} }
catch (final JsonProcessingException e) { catch (final JsonProcessingException e) {
throw new MPMarshalException( "Couldn't compose JSON for: " + user, e ); throw new MPMarshalException( "Couldn't compose JSON for: " + user, e );

View File

@ -28,7 +28,6 @@ import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/** /**
@ -38,27 +37,11 @@ public class MPJSONUnmarshaller implements MPUnmarshaller {
@Nonnull @Nonnull
@Override @Override
public MPFileUser unmarshall(@Nonnull final File file, @Nullable final char[] masterPassword) public MPFileUser readUser(@Nonnull final File file)
throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { throws IOException, MPMarshalException {
try { try {
return objectMapper.readValue( file, MPJSONFile.class ).read( masterPassword ); return objectMapper.readValue( file, MPJSONFile.class ).readUser( file );
}
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 );
} }
catch (final JsonParseException e) { catch (final JsonParseException e) {
throw new MPMarshalException( "Couldn't parse JSON.", e ); throw new MPMarshalException( "Couldn't parse JSON.", e );
@ -66,8 +49,20 @@ public class MPJSONUnmarshaller implements MPUnmarshaller {
catch (final JsonMappingException e) { catch (final JsonMappingException e) {
throw new MPMarshalException( "Couldn't map JSON.", 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 );
} }
} }
} }

View File

@ -20,7 +20,7 @@ package com.lyndir.masterpassword.model.impl;
import com.lyndir.masterpassword.MPAlgorithmException; import com.lyndir.masterpassword.MPAlgorithmException;
import com.lyndir.masterpassword.MPKeyUnavailableException; import com.lyndir.masterpassword.MPKeyUnavailableException;
import javax.annotation.Nonnull; import java.io.IOException;
/** /**
@ -29,9 +29,8 @@ import javax.annotation.Nonnull;
@FunctionalInterface @FunctionalInterface
public interface MPMarshaller { public interface MPMarshaller {
@Nonnull void marshall(MPFileUser user)
String marshall(MPFileUser user) throws IOException, MPKeyUnavailableException, MPMarshalException, MPAlgorithmException;
throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException;
enum ContentMode { enum ContentMode {
PROTECTED( "Export of site names and stored passwords (unless device-private) encrypted with the master key.", true ), PROTECTED( "Export of site names and stored passwords (unless device-private) encrypted with the master key.", true ),

View File

@ -24,7 +24,6 @@ import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/** /**
@ -33,10 +32,9 @@ import javax.annotation.Nullable;
public interface MPUnmarshaller { public interface MPUnmarshaller {
@Nonnull @Nonnull
MPFileUser unmarshall(@Nonnull File file, @Nullable char[] masterPassword) MPFileUser readUser(File file)
throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException; throws IOException, MPMarshalException;
@Nonnull void readSites(MPFileUser user)
MPFileUser unmarshall(@Nonnull String content, @Nullable char[] masterPassword) throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException;
throws MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException;
} }