diff --git a/core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPResultType.java b/core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPResultType.java
index 4688afe7..7b09a4d5 100644
--- a/core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPResultType.java
+++ b/core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPResultType.java
@@ -174,6 +174,11 @@ public enum MPResultType {
return typeFeatures;
}
+ public boolean supportsTypeFeature(final MPSiteFeature feature) {
+
+ return typeFeatures.contains( feature );
+ }
+
public int getType() {
int mask = typeIndex | typeClass.getMask();
for (final MPSiteFeature typeFeature : typeFeatures)
diff --git a/core/java/model/build.gradle b/core/java/model/build.gradle
index 1187b1ea..fc2e1569 100644
--- a/core/java/model/build.gradle
+++ b/core/java/model/build.gradle
@@ -9,6 +9,7 @@ dependencies {
compile project( ':masterpassword-algorithm' )
compile group: 'joda-time', name: 'joda-time', version: '2.4'
+ compile group: 'com.google.code.gson', name: 'gson', version: '2.8.2'
compileOnly group: 'com.google.auto.value', name: 'auto-value', version: '1.2'
apt group: 'com.google.auto.value', name: 'auto-value', version: '1.2'
diff --git a/core/java/model/src/main/java/com/lyndir/masterpassword/model/EnumOrdinalAdapter.java b/core/java/model/src/main/java/com/lyndir/masterpassword/model/EnumOrdinalAdapter.java
new file mode 100644
index 00000000..feb0961b
--- /dev/null
+++ b/core/java/model/src/main/java/com/lyndir/masterpassword/model/EnumOrdinalAdapter.java
@@ -0,0 +1,53 @@
+//==============================================================================
+// 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 .
+//==============================================================================
+
+package com.lyndir.masterpassword.model;
+
+import com.google.gson.*;
+import java.lang.reflect.Type;
+
+
+/**
+ * @author lhunath, 2018-04-27
+ */
+public class EnumOrdinalAdapter implements JsonSerializer>, JsonDeserializer> {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Enum> deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
+ throws JsonParseException {
+ Enum>[] enumConstants = ((Class>) typeOfT).getEnumConstants();
+ if (enumConstants == null)
+ throw new JsonParseException( "Not an enum: " + typeOfT );
+
+ try {
+ int ordinal = json.getAsInt();
+ if ((ordinal < 0) || (ordinal >= enumConstants.length))
+ throw new JsonParseException( "No ordinal " + ordinal + " in enum: " + typeOfT );
+
+ return enumConstants[ordinal];
+ } catch (final ClassCastException | IllegalStateException e) {
+ throw new JsonParseException( "Not an ordinal value: " + json, e );
+ }
+ }
+
+ @Override
+ public JsonElement serialize(final Enum> src, final Type typeOfSrc, final JsonSerializationContext context) {
+ return new JsonPrimitive( src.ordinal() );
+ }
+}
diff --git a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileSite.java b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileSite.java
index 3bc390c4..6e8acab0 100644
--- a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileSite.java
+++ b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileSite.java
@@ -39,7 +39,6 @@ public class MPFileSite extends MPSite {
@Nullable
private String loginContent;
- @Nullable
private MPResultType loginType;
@Nullable
@@ -48,35 +47,27 @@ public class MPFileSite extends MPSite {
private Instant lastUsed;
public MPFileSite(final MPFileUser user, final String siteName) {
- this( user, siteName,
- user.getAlgorithm().mpw_default_counter(),
- user.getAlgorithm().mpw_default_type(),
- user.getAlgorithm() );
+ this( user, siteName, null, null, user.getAlgorithm() );
}
- public MPFileSite(final MPFileUser user, final String siteName, final UnsignedInteger siteCounter, final MPResultType resultType,
- final MPAlgorithm algorithm) {
- this.user = user;
- this.siteName = siteName;
- this.siteCounter = siteCounter;
- this.resultType = resultType;
- this.algorithm = algorithm;
- this.lastUsed = new Instant();
+ public MPFileSite(final MPFileUser user, final String siteName, @Nullable final UnsignedInteger siteCounter,
+ @Nullable final MPResultType resultType, final MPAlgorithm algorithm) {
+ this( user, siteName, null, siteCounter, resultType, algorithm,
+ null, null, null, 0, new Instant() );
}
protected MPFileSite(final MPFileUser user, final String siteName, @Nullable final String siteContent,
- final UnsignedInteger siteCounter,
- final MPResultType resultType, final MPAlgorithm algorithm,
+ @Nullable final UnsignedInteger siteCounter, @Nullable final MPResultType resultType, final MPAlgorithm algorithm,
@Nullable final String loginContent, @Nullable final MPResultType loginType,
@Nullable final String url, final int uses, final Instant lastUsed) {
this.user = user;
this.siteName = siteName;
this.siteContent = siteContent;
- this.siteCounter = siteCounter;
- this.resultType = resultType;
+ this.siteCounter = (siteCounter == null)? user.getAlgorithm().mpw_default_counter(): siteCounter;
+ this.resultType = (resultType == null)? user.getAlgorithm().mpw_default_type(): resultType;
this.algorithm = algorithm;
this.loginContent = loginContent;
- this.loginType = loginType;
+ this.loginType = (loginType == null)? MPResultType.GeneratedName: loginType;
this.url = url;
this.uses = uses;
this.lastUsed = lastUsed;
@@ -163,7 +154,6 @@ public class MPFileSite extends MPSite {
this.algorithm = algorithm;
}
- @Nullable
public MPResultType getLoginType() {
return loginType;
}
diff --git a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileUserManager.java b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileUserManager.java
index 692c7b2a..ca503ba0 100644
--- a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileUserManager.java
+++ b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileUserManager.java
@@ -108,7 +108,7 @@ public class MPFileUserManager extends MPUserManager {
super.deleteUser( user );
// Remove deleted users.
- File userFile = getUserFile( user );
+ File userFile = getUserFile( user, user.getFormat() );
if (userFile.exists() && !userFile.delete())
logger.err( "Couldn't delete file: %s", userFile );
}
@@ -119,13 +119,14 @@ public class MPFileUserManager extends MPUserManager {
public void save(final MPFileUser user, final MPMasterKey masterKey)
throws MPInvalidatedException {
try {
+ final MPMarshalFormat format = user.getFormat();
new CharSink() {
@Override
public Writer openStream()
throws IOException {
- return new OutputStreamWriter( new FileOutputStream( getUserFile( user ) ), Charsets.UTF_8 );
+ return new OutputStreamWriter( new FileOutputStream( getUserFile( user, format ) ), Charsets.UTF_8 );
}
- }.write( user.getFormat().marshaller().marshall( user, masterKey, MPMarshaller.ContentMode.PROTECTED ) );
+ }.write( format.marshaller().marshall( user, masterKey, MPMarshaller.ContentMode.PROTECTED ) );
}
catch (final MPMarshalException | IOException e) {
logger.err( e, "Unable to save sites for user: %s", user );
@@ -133,8 +134,8 @@ public class MPFileUserManager extends MPUserManager {
}
@Nonnull
- private File getUserFile(final MPFileUser user) {
- return new File( path, user.getFullName() + ".mpsites" );
+ private File getUserFile(final MPFileUser user, final MPMarshalFormat format) {
+ return new File( path, user.getFullName() + format.fileSuffix() );
}
/**
diff --git a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONFile.java b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONFile.java
new file mode 100644
index 00000000..aac71e0b
--- /dev/null
+++ b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONFile.java
@@ -0,0 +1,193 @@
+//==============================================================================
+// 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 .
+//==============================================================================
+
+package com.lyndir.masterpassword.model;
+
+import com.google.common.primitives.UnsignedInteger;
+import com.lyndir.lhunath.opal.system.CodeUtils;
+import com.lyndir.masterpassword.*;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.joda.time.Instant;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+
+
+/**
+ * @author lhunath, 2018-04-27
+ */
+public class MPJSONFile {
+
+ private static final DateTimeFormatter dateFormatter = ISODateTimeFormat.dateTimeNoMillis();
+
+ Export export;
+ User user;
+
+ public MPJSONFile(final MPFileUser user, final MPMasterKey masterKey, final MPMarshaller.ContentMode contentMode)
+ throws MPInvalidatedException {
+ // if (!user.fullName || !strlen( user.fullName )) {
+ // *error = (MPMarshalError){ MPMarshalErrorMissing, "Missing full name." };
+ // return false;
+ // }
+ // if (!user.masterPassword || !strlen( user.masterPassword )) {
+ // *error = (MPMarshalError){ MPMarshalErrorMasterPassword, "Missing master password." };
+ // return false;
+ // }
+ // if (!mpw_update_masterKey( &masterKey, &masterKeyAlgorithm, user.algorithm, user.fullName, user.masterPassword )) {
+ // *error = (MPMarshalError){ MPMarshalErrorInternal, "Couldn't derive master key." };
+ // return false;
+ // }
+
+ // Section: "export"
+ Export fileExport = this.export = new Export();
+ fileExport.format = 1;
+ fileExport.redacted = contentMode.isRedacted();
+ fileExport.date = dateFormatter.print( new Instant() );
+
+ // Section: "user"
+ User fileUser = this.user = new User();
+ fileUser.avatar = user.getAvatar();
+ fileUser.fullName = user.getFullName();
+
+ fileUser.lastUsed = dateFormatter.print( user.getLastUsed() );
+ fileUser.keyId = CodeUtils.encodeHex( masterKey.getKeyID( user.getAlgorithm() ) );
+
+ fileUser.algorithm = user.getAlgorithm().version();
+ fileUser.defaultType = user.getDefaultType();
+
+ // Section "sites"
+ fileUser.sites = new LinkedHashMap<>();
+ for (final MPFileSite site : user.getSites()) {
+ Site fileSite;
+ String content = null, loginContent = null;
+ if (!contentMode.isRedacted()) {
+ // Clear Text
+ content = masterKey.siteResult( site.getSiteName(), site.getSiteCounter(),
+ MPKeyPurpose.Authentication, null, site.getResultType(), site.getSiteContent(),
+ site.getAlgorithm() );
+ loginContent = masterKey.siteResult( site.getSiteName(), site.getAlgorithm().mpw_default_counter(),
+ MPKeyPurpose.Identification, null, site.getLoginType(), site.getLoginContent(),
+ site.getAlgorithm() );
+ } else {
+ // Redacted
+ if (site.getResultType().supportsTypeFeature( MPSiteFeature.ExportContent ))
+ content = site.getSiteContent();
+ if (site.getLoginType().supportsTypeFeature( MPSiteFeature.ExportContent ))
+ loginContent = site.getLoginContent();
+ }
+
+ fileUser.sites.put( site.getSiteName(), fileSite = new Site() );
+ fileSite.type = site.getResultType();
+ fileSite.counter = site.getSiteCounter();
+ fileSite.algorithm = site.getAlgorithm().version();
+ fileSite.password = content;
+ fileSite.login_name = loginContent;
+ fileSite.loginType = site.getLoginType();
+
+ fileSite.uses = site.getUses();
+ fileSite.lastUsed = dateFormatter.print( site.getLastUsed() );
+
+ fileSite.questions = new LinkedHashMap<>();
+ // for (size_t q = 0; q < site.questions_count; ++q) {
+ // MPMarshalledQuestion *question = &site.questions[q];
+ // if (!question.keyword)
+ // continue;
+ //
+ // json_object *json_site_question = json_object_new_object();
+ // json_object_object_add( json_site_questions, question.keyword, json_site_question );
+ // json_object_object_add( json_site_question, "type = question.type;
+ //
+ // if (!user.redacted) {
+ // // Clear Text
+ // const char *answerContent = mpw_siteResult( masterKey, site.name, MPCounterValueInitial,
+ // MPKeyPurposeRecovery, question.keyword, question.type, question.content, site.algorithm );
+ // json_object_object_add( json_site_question, "answer = answerContent;
+ // }
+ // else {
+ // // Redacted
+ // if (site.type & MPSiteFeatureExportContent && question.content && strlen( question.content ))
+ // json_object_object_add( json_site_question, "answer = question.content;
+ // }
+ // }
+
+ // json_object *json_site_mpw = json_object_new_object();
+ // fileSite._ext_mpw = json_site_mpw;
+ // if (site.url)
+ // json_object_object_add( json_site_mpw, "url", site.url );
+ }
+ }
+
+ public MPFileUser toUser() {
+ return new MPFileUser( user.fullName, CodeUtils.decodeHex( user.keyId ), user.algorithm.getAlgorithm(), user.avatar, user.defaultType, dateFormatter.parseDateTime( user.lastUsed ), MPMarshalFormat.JSON );
+ }
+
+ public static class Export {
+
+ int format;
+ boolean redacted;
+ String date;
+ }
+
+
+ public static class User {
+
+ String fullName;
+
+ MPMasterKey.Version algorithm;
+ boolean redacted;
+
+ int avatar;
+ MPResultType defaultType;
+ String lastUsed;
+ String keyId;
+
+ Map sites;
+ }
+
+
+ public static class Site {
+
+ @Nullable
+ String password;
+ @Nullable
+ String login_name;
+ String name;
+ String content;
+ MPResultType type;
+ UnsignedInteger counter;
+ MPMasterKey.Version algorithm;
+
+ String loginContent;
+ MPResultType loginType;
+
+ String url;
+ int uses;
+ String lastUsed;
+
+ Map questions;
+ }
+
+
+ public static class Question {
+
+ String keyword;
+ String content;
+ MPResultType type;
+ }
+}
diff --git a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONMarshaller.java b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONMarshaller.java
index d833ec32..c1a55f98 100644
--- a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONMarshaller.java
+++ b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONMarshaller.java
@@ -18,8 +18,8 @@
package com.lyndir.masterpassword.model;
-import com.lyndir.masterpassword.MPInvalidatedException;
-import com.lyndir.masterpassword.MPMasterKey;
+import com.google.gson.*;
+import com.lyndir.masterpassword.*;
import javax.annotation.Nonnull;
@@ -28,10 +28,17 @@ import javax.annotation.Nonnull;
*/
public class MPJSONMarshaller implements MPMarshaller {
+ private final Gson gson = new GsonBuilder()
+ .registerTypeAdapter( MPMasterKey.Version.class, new EnumOrdinalAdapter() )
+ .registerTypeAdapter( MPResultType.class, new MPResultTypeAdapter() )
+ .setFieldNamingStrategy( FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES )
+ .setPrettyPrinting().create();
+
@Nonnull
@Override
public String marshall(final MPFileUser user, final MPMasterKey masterKey, final ContentMode contentMode)
throws MPInvalidatedException, MPMarshalException {
- throw new MPMarshalException( "Not yet implemented" );
+
+ return gson.toJson( new MPJSONFile( user, masterKey, contentMode ) );
}
}
diff --git a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONUnmarshaller.java b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONUnmarshaller.java
index 0b6d159f..511774b2 100644
--- a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONUnmarshaller.java
+++ b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONUnmarshaller.java
@@ -18,8 +18,11 @@
package com.lyndir.masterpassword.model;
-import java.io.File;
-import java.io.IOException;
+import com.google.gson.*;
+import com.lyndir.masterpassword.MPMasterKey;
+import com.lyndir.masterpassword.MPResultType;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
import javax.annotation.Nonnull;
@@ -28,17 +31,27 @@ import javax.annotation.Nonnull;
*/
public class MPJSONUnmarshaller implements MPUnmarshaller {
+ private final Gson gson = new GsonBuilder()
+ .registerTypeAdapter( MPMasterKey.Version.class, new EnumOrdinalAdapter() )
+ .registerTypeAdapter( MPResultType.class, new MPResultTypeAdapter() )
+ .setFieldNamingStrategy( FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES )
+ .setPrettyPrinting().create();
+
@Nonnull
@Override
public MPFileUser unmarshall(@Nonnull final File file)
throws IOException, MPMarshalException {
- throw new MPMarshalException( "Not yet implemented" );
+
+ try (Reader reader = new InputStreamReader( new FileInputStream( file ), StandardCharsets.UTF_8 )) {
+ return gson.fromJson( reader, MPJSONFile.class ).toUser();
+ }
}
@Nonnull
@Override
public MPFileUser unmarshall(@Nonnull final String content)
throws MPMarshalException {
- throw new MPMarshalException( "Not yet implemented" );
+
+ return gson.fromJson( content, MPJSONFile.class ).toUser();
}
}
diff --git a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPResultTypeAdapter.java b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPResultTypeAdapter.java
new file mode 100644
index 00000000..db46f23b
--- /dev/null
+++ b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPResultTypeAdapter.java
@@ -0,0 +1,46 @@
+//==============================================================================
+// 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 .
+//==============================================================================
+
+package com.lyndir.masterpassword.model;
+
+import com.google.gson.*;
+import com.lyndir.masterpassword.MPResultType;
+import java.lang.reflect.Type;
+
+
+/**
+ * @author lhunath, 2018-04-27
+ */
+public class MPResultTypeAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public MPResultType deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
+ throws JsonParseException {
+ try {
+ return MPResultType.forType( json.getAsInt() );
+ }
+ catch (final ClassCastException | IllegalStateException e) {
+ throw new JsonParseException( "Not an ordinal value: " + json, e );
+ }
+ }
+
+ @Override
+ public JsonElement serialize(final MPResultType src, final Type typeOfSrc, final JsonSerializationContext context) {
+ return new JsonPrimitive( src.getType() );
+ }
+}
diff --git a/core/java/tests/build.gradle b/core/java/tests/build.gradle
index 39df10ba..b4033b02 100644
--- a/core/java/tests/build.gradle
+++ b/core/java/tests/build.gradle
@@ -6,6 +6,7 @@ description = 'Master Password Test Suite'
dependencies {
compile project( ':masterpassword-algorithm' )
+ compile project( ':masterpassword-model' )
testCompile group: 'org.testng', name: 'testng', version: '6.8.5'
testCompile group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.2'
diff --git a/core/java/tests/src/test/java/com/lyndir/masterpassword/MPModelTest.java b/core/java/tests/src/test/java/com/lyndir/masterpassword/MPModelTest.java
new file mode 100644
index 00000000..75e67807
--- /dev/null
+++ b/core/java/tests/src/test/java/com/lyndir/masterpassword/MPModelTest.java
@@ -0,0 +1,37 @@
+//==============================================================================
+// 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 .
+//==============================================================================
+
+package com.lyndir.masterpassword;
+
+import com.lyndir.masterpassword.model.MPJSONUnmarshaller;
+import java.io.File;
+import org.testng.annotations.Test;
+
+
+/**
+ * @author lhunath, 2018-04-27
+ */
+public class MPModelTest {
+
+ @Test
+ public void testMasterKey()
+ throws Exception {
+ System.err.println( new MPJSONUnmarshaller().unmarshall(
+ new File( "/Users/lhunath/.mpw.d/Maarten Billemont.mpsites.json" ) ) );
+ }
+}