From 1031414ba285fd9f209c6d84d6b26aed2c48a579 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Thu, 3 May 2018 13:49:34 +0200 Subject: [PATCH] WIP - JSON mpsites serialization. --- .../lyndir/masterpassword/MPResultType.java | 5 + core/java/model/build.gradle | 1 + .../model/EnumOrdinalAdapter.java | 53 +++++ .../masterpassword/model/MPFileSite.java | 28 +-- .../model/MPFileUserManager.java | 11 +- .../masterpassword/model/MPJSONFile.java | 193 ++++++++++++++++++ .../model/MPJSONMarshaller.java | 13 +- .../model/MPJSONUnmarshaller.java | 21 +- .../model/MPResultTypeAdapter.java | 46 +++++ core/java/tests/build.gradle | 1 + .../lyndir/masterpassword/MPModelTest.java | 37 ++++ 11 files changed, 378 insertions(+), 31 deletions(-) create mode 100644 core/java/model/src/main/java/com/lyndir/masterpassword/model/EnumOrdinalAdapter.java create mode 100644 core/java/model/src/main/java/com/lyndir/masterpassword/model/MPJSONFile.java create mode 100644 core/java/model/src/main/java/com/lyndir/masterpassword/model/MPResultTypeAdapter.java create mode 100644 core/java/tests/src/test/java/com/lyndir/masterpassword/MPModelTest.java 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" ) ) ); + } +}