diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteFeature.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteFeature.java index d908322e..2e86915d 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteFeature.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteFeature.java @@ -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; + } } diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteType.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteType.java index 318bdc91..1ea12ae6 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteType.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteType.java @@ -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.of(), MPSiteFeature.ExportContent ), + ImmutableList.of( "personal" ), // + ImmutableList.of(), // + MPSiteTypeClass.Stored, 0x0, MPSiteFeature.ExportContent ), StoredDevicePrivate( "AES-encrypted, not exported.", // - ImmutableList.of( "device" ), MPSiteTypeClass.Stored, // - ImmutableList.of(), MPSiteFeature.DevicePrivate ); + ImmutableList.of( "device" ), // + ImmutableList.of(), // + MPSiteTypeClass.Stored, 0x1, MPSiteFeature.DevicePrivate ); static final Logger logger = Logger.get( MPSiteType.class ); private final String description; private final List options; - private final MPSiteTypeClass typeClass; private final List templates; + private final MPSiteTypeClass typeClass; + private final int typeIndex; private final Set typeFeatures; - MPSiteType(final String description, final List options, final MPSiteTypeClass typeClass, final List templates, - final MPSiteFeature... typeFeatures) { + MPSiteType(final String description, final List options, final List 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 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 forMask(final int mask) { + + ImmutableList.Builder 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() ); } diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteTypeClass.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteTypeClass.java index 189557ed..80196e50 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteTypeClass.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MPSiteTypeClass.java @@ -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; + } } diff --git a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java index c4a5e64a..8c12d500 100644 --- a/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java +++ b/MasterPassword/Java/masterpassword-algorithm/src/main/java/com/lyndir/masterpassword/MasterKey.java @@ -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? "": 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 ) ); diff --git a/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java b/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java index 04fdbce2..6cdd2f58 100644 --- a/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java +++ b/MasterPassword/Java/masterpassword-android/src/main/java/com/lyndir/masterpassword/EmergencyActivity.java @@ -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( 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 diff --git a/MasterPassword/Java/masterpassword-gui/pom.xml b/MasterPassword/Java/masterpassword-gui/pom.xml index d233d8aa..2870ecc4 100644 --- a/MasterPassword/Java/masterpassword-gui/pom.xml +++ b/MasterPassword/Java/masterpassword-gui/pom.xml @@ -73,10 +73,11 @@ com.lyndir.masterpassword - masterpassword-algorithm + masterpassword-model GIT-SNAPSHOT + ch.qos.logback logback-classic diff --git a/MasterPassword/Java/masterpassword-model/pom.xml b/MasterPassword/Java/masterpassword-model/pom.xml new file mode 100644 index 00000000..6cad040f --- /dev/null +++ b/MasterPassword/Java/masterpassword-model/pom.xml @@ -0,0 +1,44 @@ + + + + 4.0.0 + + + + com.lyndir.masterpassword + masterpassword + GIT-SNAPSHOT + + + Master Password Site Model + A persistence model for Master Password sites. + + com.lyndir.masterpassword + masterpassword-model + jar + + + + + + + com.lyndir.masterpassword + masterpassword-algorithm + GIT-SNAPSHOT + + + + + org.testng + testng + test + + + ch.qos.logback + logback-classic + test + + + + + diff --git a/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSite.java b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSite.java new file mode 100644 index 00000000..3820f219 --- /dev/null +++ b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSite.java @@ -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; + } +} diff --git a/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteFileManager.java b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteFileManager.java new file mode 100644 index 00000000..611b304a --- /dev/null +++ b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteFileManager.java @@ -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; + } +} diff --git a/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteManager.java b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteManager.java new file mode 100644 index 00000000..5b4e1d60 --- /dev/null +++ b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteManager.java @@ -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 findSitesByName(String query) { + ImmutableList.Builder results = ImmutableList.builder(); + for (MPSite site : user.getSites()) + if (site.getSiteName().startsWith( query )) + results.add( new MPSiteResult( site ) ); + + return results.build(); + } +} diff --git a/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteMarshaller.java b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteMarshaller.java new file mode 100644 index 00000000..048167b3 --- /dev/null +++ b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteMarshaller.java @@ -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); + } +} diff --git a/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteResult.java b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteResult.java new file mode 100644 index 00000000..95628ede --- /dev/null +++ b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteResult.java @@ -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; + } +} diff --git a/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteUnmarshaller.java b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteUnmarshaller.java new file mode 100644 index 00000000..98939446 --- /dev/null +++ b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPSiteUnmarshaller.java @@ -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 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 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() { + @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; + } +} diff --git a/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPUser.java b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPUser.java new file mode 100644 index 00000000..3f436f00 --- /dev/null +++ b/MasterPassword/Java/masterpassword-model/src/main/java/com/lyndir/masterpassword/model/MPUser.java @@ -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 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 getSites() { + return sites; + } +} diff --git a/MasterPassword/Java/pom.xml b/MasterPassword/Java/pom.xml index e249ba6a..3a5e7fb6 100644 --- a/MasterPassword/Java/pom.xml +++ b/MasterPassword/Java/pom.xml @@ -20,6 +20,7 @@ masterpassword-algorithm + masterpassword-model masterpassword-cli masterpassword-gui