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
String getName();
void setName(String name);
// - Algorithm
@Nonnull

View File

@ -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<S extends MPSite<?>> extends Comparable<MPUser<?>> {
@Nonnull
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;
}
@Override
public void setName(final String name) {
this.name = name;
setChanged();
}
@Nonnull
@Override
public MPAlgorithm getAlgorithm() {

View File

@ -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<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 final String fullName;
@ -44,7 +46,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
@Nullable
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) {
this( 0, fullName, algorithm );
@ -128,6 +130,9 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> 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<S extends MPBasicSite<?>> 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<S extends MPBasicSite<?>> extends Changeable i
@Nonnull
@Override
public Collection<S> getSites() {
return Collections.unmodifiableCollection( sites );
return Collections.unmodifiableCollection( sites.values() );
}
@Nonnull
@ -176,6 +181,24 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> 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() );

View File

@ -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<MPFileSite> {
@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<MPFileSite> {
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<MPFileSite> {
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<MPFileSite> {
@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

View File

@ -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<MPFileUser> {
public class MPFileUserManager {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPFileUserManager.class );
@ -46,13 +43,17 @@ public class MPFileUserManager extends MPUserManager<MPFileUser> {
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<String, MPFileUser> userByName = new HashMap<>();
private final File path;
public static MPFileUserManager get() {
return instance;
@ -63,86 +64,53 @@ public class MPFileUserManager extends MPUserManager<MPFileUser> {
}
protected MPFileUserManager(final File path) {
super( unmarshallUsers( path ) );
this.path = path;
}
private static Iterable<MPFileUser> 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<String, MPFileUser> 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<File> 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<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.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() );
}
}

View File

@ -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;
}
}

View File

@ -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<String, Site> 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<String, Site> sites;
Export export = new Export();
User user = new User();
Map<String, Site> 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

View File

@ -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 );

View File

@ -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 );
}
}
}

View File

@ -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 ),

View File

@ -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;
}