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

View File

@ -6,6 +6,16 @@ package com.lyndir.masterpassword;
* @author lhunath * @author lhunath
*/ */
public enum MPSiteTypeClass { public enum MPSiteTypeClass {
Generated, Generated( 1 << 4 ),
Stored 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 class MasterKey {
public static final int ALGORITHM = 1;
public static final String VERSION = "2.1";
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MasterKey.class ); private static final Logger logger = Logger.get( MasterKey.class );
private static final int MP_N = 32768; private static final int MP_N = 32768;
@ -103,9 +106,9 @@ public class MasterKey {
byte[] siteContextBytes = siteContext == null? null: siteContext.getBytes( MP_charset ); byte[] siteContextBytes = siteContext == null? null: siteContext.getBytes( MP_charset );
byte[] siteContextLengthBytes = bytesForInt( siteContextBytes == null? 0: siteContextBytes.length ); byte[] siteContextLengthBytes = bytesForInt( siteContextBytes == null? 0: siteContextBytes.length );
logger.trc( "site scope: %s, context: %s", siteScope, siteContext == null? "<empty>": siteContext ); 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, logger.trc( "seed from: hmac-sha256(masterKey, %s | %s | %s | %s | %s | %s)", siteScope, CodeUtils.encodeHex( siteNameLengthBytes ),
CodeUtils.encodeHex( siteNameLengthBytes ), siteName, CodeUtils.encodeHex( siteCounterBytes ), siteName, CodeUtils.encodeHex( siteCounterBytes ), CodeUtils.encodeHex( siteContextLengthBytes ),
CodeUtils.encodeHex( siteContextLengthBytes ), siteContext == null? "(null)": siteContext ); siteContext == null? "(null)": siteContext );
byte[] sitePasswordInfo = Bytes.concat( siteScope.getBytes( MP_charset ), siteNameLengthBytes, siteNameBytes, siteCounterBytes ); byte[] sitePasswordInfo = Bytes.concat( siteScope.getBytes( MP_charset ), siteNameLengthBytes, siteNameBytes, siteCounterBytes );
logger.trc( "sitePasswordInfo ID: %s", idForBytes( sitePasswordInfo ) ); logger.trc( "sitePasswordInfo ID: %s", idForBytes( sitePasswordInfo ) );

View File

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

View File

@ -73,10 +73,11 @@
<!-- PROJECT REFERENCES --> <!-- PROJECT REFERENCES -->
<dependency> <dependency>
<groupId>com.lyndir.masterpassword</groupId> <groupId>com.lyndir.masterpassword</groupId>
<artifactId>masterpassword-algorithm</artifactId> <artifactId>masterpassword-model</artifactId>
<version>GIT-SNAPSHOT</version> <version>GIT-SNAPSHOT</version>
</dependency> </dependency>
<!-- EXTERNAL DEPENDENCIES -->
<dependency> <dependency>
<groupId>ch.qos.logback</groupId> <groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId> <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> <modules>
<module>masterpassword-algorithm</module> <module>masterpassword-algorithm</module>
<module>masterpassword-model</module>
<module>masterpassword-cli</module> <module>masterpassword-cli</module>
<module>masterpassword-gui</module> <module>masterpassword-gui</module>
</modules> </modules>