2
0

A persistence model for the Java implementation and ability to parse in and write out export files.

This commit is contained in:
Maarten Billemont 2014-12-08 11:11:29 -05:00
parent 3f4558da2b
commit 4ff8cd6d90
15 changed files with 714 additions and 38 deletions

View File

@ -7,8 +7,23 @@ package com.lyndir.masterpassword;
*/
public enum MPSiteFeature {
/** Export the key-protected content data. */
ExportContent,
/** Never export content. */
DevicePrivate,
/**
* Export the key-protected content data.
*/
ExportContent( 1 << 10 ),
/**
* Never export content.
*/
DevicePrivate( 1 << 11 );
MPSiteFeature(final int mask) {
this.mask = mask;
}
private final int mask;
public int getMask() {
return mask;
}
}

View File

@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableSet;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
/**
@ -15,11 +16,12 @@ import java.util.Set;
public enum MPSiteType {
GeneratedMaximum( "20 characters, contains symbols.", //
ImmutableList.of( "x", "max", "maximum" ), MPSiteTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ), new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ) ),
ImmutableList.of( "x", "max", "maximum" ), //
ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ), new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), //
MPSiteTypeClass.Generated, 0x0 ),
GeneratedLong( "Copy-friendly, 14 characters, contains symbols.", //
ImmutableList.of( "l", "long" ), MPSiteTypeClass.Generated, //
ImmutableList.of( "l", "long" ), //
ImmutableList.of( new MPTemplate( "CvcvnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvnoCvcv" ),
new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ),
new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ),
@ -30,56 +32,67 @@ public enum MPSiteType {
new MPTemplate( "CvccCvccCvcvno" ), new MPTemplate( "CvcvnoCvccCvcc" ),
new MPTemplate( "CvcvCvccnoCvcc" ), new MPTemplate( "CvcvCvccCvccno" ),
new MPTemplate( "CvccnoCvcvCvcc" ), new MPTemplate( "CvccCvcvnoCvcc" ),
new MPTemplate( "CvccCvcvCvccno" ) ) ),
new MPTemplate( "CvccCvcvCvccno" ) ), //
MPSiteTypeClass.Generated, 0x1 ),
GeneratedMedium( "Copy-friendly, 8 characters, contains symbols.", //
ImmutableList.of( "m", "med", "medium" ), MPSiteTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "CvcnoCvc" ), new MPTemplate( "CvcCvcno" ) ) ),
ImmutableList.of( "m", "med", "medium" ), //
ImmutableList.of( new MPTemplate( "CvcnoCvc" ), new MPTemplate( "CvcCvcno" ) ), //
MPSiteTypeClass.Generated, 0x2 ),
GeneratedBasic( "8 characters, no symbols.", //
ImmutableList.of( "b", "basic" ), MPSiteTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "aaanaaan" ), new MPTemplate( "aannaaan" ), new MPTemplate( "aaannaaa" ) ) ),
ImmutableList.of( "b", "basic" ), //
ImmutableList.of( new MPTemplate( "aaanaaan" ), new MPTemplate( "aannaaan" ), new MPTemplate( "aaannaaa" ) ), //
MPSiteTypeClass.Generated, 0x3 ),
GeneratedShort( "Copy-friendly, 4 characters, no symbols.", //
ImmutableList.of( "s", "short" ), MPSiteTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "Cvcn" ) ) ),
ImmutableList.of( "s", "short" ), //
ImmutableList.of( new MPTemplate( "Cvcn" ) ), //
MPSiteTypeClass.Generated, 0x4 ),
GeneratedPIN( "4 numbers.", //
ImmutableList.of( "i", "pin" ), MPSiteTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "nnnn" ) ) ),
ImmutableList.of( "i", "pin" ), //
ImmutableList.of( new MPTemplate( "nnnn" ) ), //
MPSiteTypeClass.Generated, 0x5 ),
GeneratedName( "9 letter name.", //
ImmutableList.of( "n", "name" ), MPSiteTypeClass.Generated, //
ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ) ),
ImmutableList.of( "n", "name" ), //
ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), //
MPSiteTypeClass.Generated, 0xE ),
GeneratedPhrase( "20 character sentence.", //
ImmutableList.of( "p", "phrase" ), MPSiteTypeClass.Generated, //
ImmutableList.of( "p", "phrase" ), //
ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ), new MPTemplate( "cvc cvccvcvcv cvcv" ),
new MPTemplate( "cv cvccv cvc cvcvccv" ) ) ),
new MPTemplate( "cv cvccv cvc cvcvccv" ) ), //
MPSiteTypeClass.Generated, 0xF ),
StoredPersonal( "AES-encrypted, exportable.", //
ImmutableList.of( "personal" ), MPSiteTypeClass.Stored, //
ImmutableList.<MPTemplate>of(), MPSiteFeature.ExportContent ),
ImmutableList.of( "personal" ), //
ImmutableList.<MPTemplate>of(), //
MPSiteTypeClass.Stored, 0x0, MPSiteFeature.ExportContent ),
StoredDevicePrivate( "AES-encrypted, not exported.", //
ImmutableList.of( "device" ), MPSiteTypeClass.Stored, //
ImmutableList.<MPTemplate>of(), MPSiteFeature.DevicePrivate );
ImmutableList.of( "device" ), //
ImmutableList.<MPTemplate>of(), //
MPSiteTypeClass.Stored, 0x1, MPSiteFeature.DevicePrivate );
static final Logger logger = Logger.get( MPSiteType.class );
private final String description;
private final List<String> options;
private final MPSiteTypeClass typeClass;
private final List<MPTemplate> templates;
private final MPSiteTypeClass typeClass;
private final int typeIndex;
private final Set<MPSiteFeature> typeFeatures;
MPSiteType(final String description, final List<String> options, final MPSiteTypeClass typeClass, final List<MPTemplate> templates,
final MPSiteFeature... typeFeatures) {
MPSiteType(final String description, final List<String> options, final List<MPTemplate> templates, final MPSiteTypeClass typeClass,
final int typeIndex, final MPSiteFeature... typeFeatures) {
this.description = description;
this.options = options;
this.typeClass = typeClass;
this.templates = templates;
this.typeClass = typeClass;
this.typeIndex = typeIndex;
ImmutableSet.Builder<MPSiteFeature> typeFeaturesBuilder = ImmutableSet.builder();
for (final MPSiteFeature typeFeature : typeFeatures) {
@ -107,6 +120,14 @@ public enum MPSiteType {
return typeFeatures;
}
public int getMask() {
int mask = typeIndex | typeClass.getMask();
for (MPSiteFeature typeFeature : typeFeatures)
mask |= typeFeature.getMask();
return mask;
}
/**
* @param option The option to select a type with. It is matched case insensitively.
*
@ -153,6 +174,21 @@ public enum MPSiteType {
return types.build();
}
/**
* @param mask The mask for which we look up types.
*
* @return All types that support the given mask.
*/
public static ImmutableList<MPSiteType> forMask(final int mask) {
ImmutableList.Builder<MPSiteType> types = ImmutableList.builder();
for (MPSiteType siteType : values())
if ((siteType.getMask() & mask) != 0)
types.add( siteType );
return types.build();
}
public MPTemplate getTemplateAtRollingIndex(final int templateIndex) {
return templates.get( templateIndex % templates.size() );
}

View File

@ -6,6 +6,16 @@ package com.lyndir.masterpassword;
* @author lhunath
*/
public enum MPSiteTypeClass {
Generated,
Stored
Generated( 1 << 4 ),
Stored( 1 << 5 );
private final int mask;
MPSiteTypeClass(final int mask) {
this.mask = mask;
}
public int getMask() {
return mask;
}
}

View File

@ -19,6 +19,9 @@ import javax.annotation.Nullable;
*/
public class MasterKey {
public static final int ALGORITHM = 1;
public static final String VERSION = "2.1";
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MasterKey.class );
private static final int MP_N = 32768;
@ -103,9 +106,9 @@ public class MasterKey {
byte[] siteContextBytes = siteContext == null? null: siteContext.getBytes( MP_charset );
byte[] siteContextLengthBytes = bytesForInt( siteContextBytes == null? 0: siteContextBytes.length );
logger.trc( "site scope: %s, context: %s", siteScope, siteContext == null? "<empty>": siteContext );
logger.trc( "seed from: hmac-sha256(masterKey, %s | %s | %s | %s | %s | %s)", siteScope,
CodeUtils.encodeHex( siteNameLengthBytes ), siteName, CodeUtils.encodeHex( siteCounterBytes ),
CodeUtils.encodeHex( siteContextLengthBytes ), siteContext == null? "(null)": siteContext );
logger.trc( "seed from: hmac-sha256(masterKey, %s | %s | %s | %s | %s | %s)", siteScope, CodeUtils.encodeHex( siteNameLengthBytes ),
siteName, CodeUtils.encodeHex( siteCounterBytes ), CodeUtils.encodeHex( siteContextLengthBytes ),
siteContext == null? "(null)": siteContext );
byte[] sitePasswordInfo = Bytes.concat( siteScope.getBytes( MP_charset ), siteNameLengthBytes, siteNameBytes, siteCounterBytes );
logger.trc( "sitePasswordInfo ID: %s", idForBytes( sitePasswordInfo ) );

View File

@ -86,8 +86,7 @@ public class EmergencyActivity extends Activity {
sitePasswordField.setTypeface( Res.sourceCodePro_Black );
sitePasswordField.setPaintFlags( userNameField.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG );
typeField.setAdapter(
new ArrayAdapter<MPSiteType>( this, R.layout.type_item, MPSiteType.forClass( MPSiteTypeClass.Generated ) ) );
typeField.setAdapter( new ArrayAdapter<>( this, R.layout.type_item, MPSiteType.forClass( MPSiteTypeClass.Generated ) ) );
typeField.setSelection( MPSiteType.GeneratedLong.ordinal() );
counterField.setMinValue( 1 );
@ -129,7 +128,7 @@ public class EmergencyActivity extends Activity {
SharedPreferences.Editor pref = getPreferences( MODE_PRIVATE ).edit();
pref.putString( "userName", userName );
pref.commit();
pref.apply();
if (masterKeyFuture != null)
masterKeyFuture.cancel( true );
@ -184,7 +183,7 @@ public class EmergencyActivity extends Activity {
@Override
public void run() {
try {
final String sitePassword = masterKeyFuture.get().encode( siteName, type, counter, variant, context );
final String sitePassword = masterKeyFuture.get().encode( siteName, type, counter, MPSiteVariant.Password, null );
runOnUiThread( new Runnable() {
@Override

View File

@ -73,10 +73,11 @@
<!-- PROJECT REFERENCES -->
<dependency>
<groupId>com.lyndir.masterpassword</groupId>
<artifactId>masterpassword-algorithm</artifactId>
<artifactId>masterpassword-model</artifactId>
<version>GIT-SNAPSHOT</version>
</dependency>
<!-- EXTERNAL DEPENDENCIES -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- PROJECT METADATA -->
<parent>
<groupId>com.lyndir.masterpassword</groupId>
<artifactId>masterpassword</artifactId>
<version>GIT-SNAPSHOT</version>
</parent>
<name>Master Password Site Model</name>
<description>A persistence model for Master Password sites.</description>
<groupId>com.lyndir.masterpassword</groupId>
<artifactId>masterpassword-model</artifactId>
<packaging>jar</packaging>
<!-- DEPENDENCY MANAGEMENT -->
<dependencies>
<!-- PROJECT REFERENCES -->
<dependency>
<groupId>com.lyndir.masterpassword</groupId>
<artifactId>masterpassword-algorithm</artifactId>
<version>GIT-SNAPSHOT</version>
</dependency>
<!-- TESTING -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,115 @@
package com.lyndir.masterpassword.model;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse;
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
import com.lyndir.masterpassword.*;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/**
* @author lhunath, 14-12-05
*/
public class MPSite {
public static final MPSiteType DEFAULT_TYPE = MPSiteType.GeneratedLong;
public static final int DEFAULT_COUNTER = 1;
private int mpVersion;
private DateTime lastUsed;
private String siteName;
private MPSiteType siteType;
private int siteCounter;
private int uses;
private String loginName;
public MPSite(final String siteName) {
this( siteName, DEFAULT_TYPE, DEFAULT_COUNTER );
}
public MPSite(final String siteName, final MPSiteType siteType, final int siteCounter) {
this.siteName = siteName;
this.siteType = siteType;
this.siteCounter = siteCounter;
}
protected MPSite(final int mpVersion, final DateTime lastUsed, final String siteName, final MPSiteType siteType, final int siteCounter,
final int uses, final String loginName, final String importContent) {
this.mpVersion = mpVersion;
this.lastUsed = lastUsed;
this.siteName = siteName;
this.siteType = siteType;
this.siteCounter = siteCounter;
this.uses = uses;
this.loginName = loginName;
}
public String resultFor(final MasterKey masterKey) {
return resultFor( masterKey, MPSiteVariant.Password, null );
}
public String resultFor(final MasterKey masterKey, final MPSiteVariant variant, final String context) {
return masterKey.encode( siteName, siteType, siteCounter, variant, context );
}
@Nullable
protected String exportContent() {
return null;
}
public int getMPVersion() {
return mpVersion;
}
public void setMPVersion(final int mpVersion) {
this.mpVersion = mpVersion;
}
public DateTime getLastUsed() {
return lastUsed;
}
public void setLastUsed(final DateTime lastUsed) {
this.lastUsed = lastUsed;
}
public String getSiteName() {
return siteName;
}
public void setSiteName(final String siteName) {
this.siteName = siteName;
}
public MPSiteType getSiteType() {
return siteType;
}
public void setSiteType(final MPSiteType siteType) {
this.siteType = siteType;
}
public int getSiteCounter() {
return siteCounter;
}
public void setSiteCounter(final int siteCounter) {
this.siteCounter = siteCounter;
}
public int getUses() {
return uses;
}
public void setUses(final int uses) {
this.uses = uses;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(final String loginName) {
this.loginName = loginName;
}
}

View File

@ -0,0 +1,52 @@
package com.lyndir.masterpassword.model;
import com.google.common.io.CharSink;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.io.*;
/**
* @author lhunath, 14-12-07
*/
public class MPSiteFileManager extends MPSiteManager {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPSiteFileManager.class );
private final File file;
public static MPSiteFileManager create(final File file) {
try {
return new MPSiteFileManager( file );
}
catch (IOException e) {
throw logger.bug( e, "Unable to open sites from file: %s", file );
}
}
protected MPSiteFileManager(final File file)
throws IOException {
super( MPSiteUnmarshaller.unmarshall( file ).getUser() );
this.file = file;
}
public void save() {
try {
new CharSink() {
@Override
public Writer openStream()
throws IOException {
return new FileWriter( file );
}
}.write( MPSiteMarshaller.marshallSafe( getUser() ).getExport() );
}
catch (IOException e) {
logger.err( e, "Unable to save sites to file: %s", file );
}
}
public File getFile() {
return file;
}
}

View File

@ -0,0 +1,30 @@
package com.lyndir.masterpassword.model;
import com.google.common.collect.ImmutableList;
import java.util.Collection;
/**
* @author lhunath, 14-12-05
*/
public abstract class MPSiteManager {
private final MPUser user;
public MPSiteManager(final MPUser user) {
this.user = user;
}
public MPUser getUser() {
return user;
}
public Collection<MPSiteResult> findSitesByName(String query) {
ImmutableList.Builder<MPSiteResult> results = ImmutableList.builder();
for (MPSite site : user.getSites())
if (site.getSiteName().startsWith( query ))
results.add( new MPSiteResult( site ) );
return results.build();
}
}

View File

@ -0,0 +1,130 @@
package com.lyndir.masterpassword.model;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.ifNotNullElse;
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
import com.google.common.base.Preconditions;
import com.lyndir.masterpassword.MasterKey;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.joda.time.Instant;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
/**
* @author lhunath, 14-12-07
*/
public class MPSiteMarshaller {
private static final DateTimeFormatter rfc3339 = ISODateTimeFormat.dateTime();
private final StringBuilder export = new StringBuilder();
private ContentMode contentMode = ContentMode.PROTECTED;
private MasterKey masterKey;
public static MPSiteMarshaller marshallSafe(final MPUser user) {
MPSiteMarshaller marshaller = new MPSiteMarshaller();
marshaller.marshallHeaderForSafeContent( user );
for (MPSite site : user.getSites())
marshaller.marshallSite( site );
return marshaller;
}
public static MPSiteMarshaller marshallVisible(final MPUser user, final MasterKey masterKey) {
MPSiteMarshaller marshaller = new MPSiteMarshaller();
marshaller.marshallHeaderForVisibleContentWithKey( user, masterKey );
for (MPSite site : user.getSites())
marshaller.marshallSite( site );
return marshaller;
}
private String marshallHeaderForSafeContent(final MPUser user) {
return marshallHeader( ContentMode.PROTECTED, user, null );
}
private String marshallHeaderForVisibleContentWithKey(final MPUser user, final MasterKey masterKey) {
return marshallHeader( ContentMode.VISIBLE, user, masterKey );
}
private String marshallHeader(final ContentMode contentMode, final MPUser user, @Nullable final MasterKey masterKey) {
this.masterKey = masterKey;
StringBuilder header = new StringBuilder();
header.append( "# Master Password site export\n" );
header.append( "# " ).append( contentMode.description() ).append( '\n' );
header.append( "# \n" );
header.append( "##\n" );
header.append( "# Format: 1\n" );
header.append( "# Date: " ).append( rfc3339.print( new Instant() ) ).append( '\n' );
header.append( "# User Name: " ).append( user.getFullName() ).append( '\n' );
header.append( "# Full Name: " ).append( user.getFullName() ).append( '\n' );
header.append( "# Avatar: " ).append( user.getAvatar() ).append( '\n' );
header.append( "# Key ID: " ).append( user.exportKeyID() ).append( '\n' );
header.append( "# Version: " ).append( MasterKey.VERSION ).append( '\n' );
header.append( "# Algorithm: " ).append( MasterKey.ALGORITHM ).append( '\n' );
header.append( "# Default Type: " ).append( user.getDefaultType().getMask() ).append( '\n' );
header.append( "# Passwords: " ).append( contentMode.name() ).append( '\n' );
header.append( "##\n" );
header.append( "#\n" );
header.append( "# Last Times Password Login\t Site\tSite\n" );
header.append( "# used used type name\t name\tpassword\n" );
export.append( header );
return header.toString();
}
public String marshallSite(MPSite site) {
String exportLine = strf( "%s %8ld %8s %25s\t%25s\t%s", //
rfc3339.print( site.getLastUsed() ), // lastUsed
site.getUses(), // uses
strf( "%lu:%lu:%lu", //
site.getSiteType().getMask(), // type
site.getMPVersion(), // algorithm
site.getSiteCounter() ), // counter
ifNotNullElse( site.getLoginName(), "" ), // loginName
site.getSiteName(), // siteName
ifNotNullElse( contentMode.contentForSite( site, masterKey ), "" ) // password
);
export.append( exportLine ).append( '\n' );
return exportLine;
}
public String getExport() {
return export.toString();
}
public ContentMode getContentMode() {
return contentMode;
}
public enum ContentMode {
PROTECTED( "Export of site names and stored passwords (unless device-private) encrypted with the master key." ) {
@Override
public String contentForSite(final MPSite site, @Nullable final MasterKey masterKey) {
return site.exportContent();
}
},
VISIBLE( "Export of site names and passwords in clear-text." ) {
@Override
public String contentForSite(final MPSite site, @Nonnull final MasterKey masterKey) {
return site.resultFor( Preconditions.checkNotNull( masterKey, "Master key is required when content mode is VISIBLE." ) );
}
};
private final String description;
ContentMode(final String description) {
this.description = description;
}
public String description() {
return description;
}
public abstract String contentForSite(final MPSite site, final MasterKey masterKey);
}
}

View File

@ -0,0 +1,17 @@
package com.lyndir.masterpassword.model;
/**
* @author lhunath, 14-12-07
*/
public class MPSiteResult {
private final MPSite site;
public MPSiteResult(final MPSite site) {
this.site = site;
}
public MPSite getSite() {
return site;
}
}

View File

@ -0,0 +1,158 @@
package com.lyndir.masterpassword.model;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.io.CharStreams;
import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import com.lyndir.lhunath.opal.system.util.NNOperation;
import com.lyndir.masterpassword.MPSiteType;
import java.io.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
/**
* @author lhunath, 14-12-07
*/
public class MPSiteUnmarshaller {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPSite.class );
private static final DateTimeFormatter rfc3339 = ISODateTimeFormat.dateTime();
private static final Pattern[] unmarshallFormats = new Pattern[]{
Pattern.compile( "^([^ ]+) +([[:digit:]]+) +([[:digit:]]+)(:[[:digit:]]+)? +([^\t]+)\t(.*)" ),
Pattern.compile( "^([^ ]+) +([[:digit:]]+) +([[:digit:]]+)(:[[:digit:]]+)?(:[[:digit:]]+)? +([^\t]*)\t *([^\t]+)\t(.*)" ) };
private static final Pattern headerFormat = Pattern.compile( "^#[[:space:]]*([^:]+): (.*)" );
private final int importFormat;
private final int mpVersion;
private final boolean clearContent;
private final MPUser user;
@Nonnull
public static MPSiteUnmarshaller unmarshall(@Nonnull File file)
throws IOException {
try (Reader reader = new FileReader( file )) {
return unmarshall( CharStreams.readLines( reader ) );
}
}
@Nonnull
public static MPSiteUnmarshaller unmarshall(@Nonnull List<String> lines) {
byte[] keyID = null;
String fullName = null;
int mpVersion = 0, importFormat = 0, avatar = 0;
boolean clearContent = false, headerStarted = false;
MPSiteType defaultType = MPSiteType.GeneratedLong;
MPSiteUnmarshaller marshaller = null;
final ImmutableList.Builder<MPSite> sites = ImmutableList.builder();
for (String line : lines)
// Header delimitor.
if (line.startsWith( "##" ))
if (!headerStarted)
// Starts the header.
headerStarted = true;
else
// Ends the header.
marshaller = new MPSiteUnmarshaller( importFormat, mpVersion, fullName, keyID, avatar, defaultType, clearContent );
// Comment.
else if (line.startsWith( "#" )) {
if (headerStarted && marshaller == 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 = value.equalsIgnoreCase( "visible" );
else if ("Default Type".equalsIgnoreCase( name ))
defaultType = Iterables.getOnlyElement( MPSiteType.forMask( ConversionUtils.toIntegerNN( value ) ) );
}
}
}
// No comment.
else if (marshaller != null)
ifNotNull( marshaller.unmarshallSite( line ), new NNOperation<MPSite>() {
@Override
public void apply(@Nonnull final MPSite site) {
sites.add( site );
}
} );
return Preconditions.checkNotNull( marshaller, "No full header found in import file." );
}
protected MPSiteUnmarshaller(final int importFormat, final int mpVersion, final String fullName, final byte[] keyID, final int avatar,
final MPSiteType defaultType, final boolean clearContent) {
this.importFormat = importFormat;
this.mpVersion = mpVersion;
this.clearContent = clearContent;
user = new MPUser( fullName, keyID, avatar, defaultType, new DateTime( 0 ) );
}
@Nullable
public MPSite unmarshallSite(@Nonnull String siteLine) {
Matcher siteMatcher = unmarshallFormats[importFormat].matcher( siteLine );
if (!siteMatcher.matches())
return null;
MPSite site;
switch (importFormat) {
case 0:
site = new MPSite( ConversionUtils.toIntegerNN( siteMatcher.group( 4 ).replace( ":", "" ) ), //
rfc3339.parseDateTime( siteMatcher.group( 1 ) ), //
siteMatcher.group( 5 ), //
Iterables.getOnlyElement( MPSiteType.forMask( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ) ),
MPSite.DEFAULT_COUNTER, //
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), //
null, //
siteMatcher.group( 6 ) );
break;
case 1:
site = new MPSite( ConversionUtils.toIntegerNN( siteMatcher.group( 4 ).replace( ":", "" ) ), //
rfc3339.parseDateTime( siteMatcher.group( 1 ) ), //
siteMatcher.group( 7 ), //
Iterables.getOnlyElement( MPSiteType.forMask( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ) ),
ConversionUtils.toIntegerNN( siteMatcher.group( 5 ).replace( ":", "" ) ), //
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), //
siteMatcher.group( 6 ), //
siteMatcher.group( 8 ) );
break;
default:
throw logger.bug( "Unexpected format: %d", importFormat );
}
user.addSite( site );
return site;
}
public MPUser getUser() {
return user;
}
}

View File

@ -0,0 +1,65 @@
package com.lyndir.masterpassword.model;
import com.google.common.collect.Sets;
import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.masterpassword.MPSiteType;
import java.util.*;
import org.joda.time.DateTime;
/**
* @author lhunath, 14-12-07
*/
public class MPUser {
private final String fullName;
private final byte[] keyID;
private final int avatar;
private final MPSiteType defaultType;
private final DateTime lastUsed;
private final Collection<MPSite> sites = Sets.newHashSet();
public MPUser(final String fullName, final byte[] keyID) {
this( fullName, keyID, 0, MPSiteType.GeneratedLong, new DateTime() );
}
public MPUser(final String fullName, final byte[] keyID, final int avatar, final MPSiteType defaultType, final DateTime lastUsed) {
this.fullName = fullName;
this.keyID = keyID;
this.avatar = avatar;
this.defaultType = defaultType;
this.lastUsed = lastUsed;
}
public void addSite(final MPSite site) {
sites.add( site );
}
public String getFullName() {
return fullName;
}
public boolean hasKeyID(final byte[] keyID) {
return Arrays.equals( this.keyID, keyID );
}
public String exportKeyID() {
return CodeUtils.encodeHex( keyID );
}
public int getAvatar() {
return avatar;
}
public MPSiteType getDefaultType() {
return defaultType;
}
public DateTime getLastUsed() {
return lastUsed;
}
public Iterable<MPSite> getSites() {
return sites;
}
}

View File

@ -20,6 +20,7 @@
<modules>
<module>masterpassword-algorithm</module>
<module>masterpassword-model</module>
<module>masterpassword-cli</module>
<module>masterpassword-gui</module>
</modules>