2
0

Transition to Jackson so we can retain unrecognized properties in the source JSON.

This commit is contained in:
Maarten Billemont
2018-05-14 11:27:49 -04:00
parent f0d523fb35
commit 38a357cb28
12 changed files with 246 additions and 196 deletions

View File

@@ -7,13 +7,14 @@ description = 'Master Password Site Model'
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'
compile 'joda-time:joda-time:2.4'
compile 'com.fasterxml.jackson.core:jackson-core:2.9.5'
compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.5'
compile 'com.fasterxml.jackson.core:jackson-databind:2.9.5'
//compile group: 'com.google.code.gson', name: 'gson', version: '2.8.2'
compileOnly 'com.google.auto.value:auto-value:1.2'
apt group: 'com.google.auto.value', name: 'auto-value', version: '1.2'
testCompile group: 'org.testng', name: 'testng', version: '6.8.5'
testCompile group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.2'
testCompile 'org.testng:testng:6.8.5'
testCompile 'ch.qos.logback:logback-classic:1.1.2'
}
test.useTestNG()

View File

@@ -1,53 +0,0 @@
//==============================================================================
// 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 <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.model;
import com.google.gson.*;
import java.lang.reflect.Type;
/**
* @author lhunath, 2018-04-27
*/
public class EnumOrdinalAdapter implements JsonSerializer<Enum<?>>, JsonDeserializer<Enum<?>> {
@Override
@SuppressWarnings("unchecked")
public Enum<?> deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
Enum<?>[] enumConstants = ((Class<Enum<?>>) 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() );
}
}

View File

@@ -51,6 +51,9 @@ public class MPFileUser extends MPUser<MPFileSite> implements Comparable<MPFileU
private MPResultType defaultType;
private ReadableInstant lastUsed;
@Nullable
private MPJSONFile json;
public MPFileUser(final String fullName) {
this( fullName, null, MPMasterKey.Version.CURRENT.getAlgorithm() );
}
@@ -157,6 +160,15 @@ public class MPFileUser extends MPUser<MPFileSite> implements Comparable<MPFileU
return results.build();
}
public void setJSON(final MPJSONFile json) {
this.json = json;
}
@Nonnull
public MPJSONFile getJSON() {
return (json == null)? json = new MPJSONFile(): json;
}
/**
* Performs an authentication attempt against the keyID for this user.
*

View File

@@ -18,29 +18,21 @@
package com.lyndir.masterpassword.model;
import com.google.gson.*;
import com.lyndir.masterpassword.MPResultType;
import java.lang.reflect.Type;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import java.util.*;
/**
* @author lhunath, 2018-04-27
* @author lhunath, 2018-05-14
*/
public class MPResultTypeAdapter implements JsonSerializer<MPResultType>, JsonDeserializer<MPResultType> {
class MPJSONAnyObject {
@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 );
}
}
@JsonAnySetter
final Map<String, Object> any = new LinkedHashMap<>();
@Override
public JsonElement serialize(final MPResultType src, final Type typeOfSrc, final JsonSerializationContext context) {
return new JsonPrimitive( src.getType() );
@JsonAnyGetter
public Map<String, Object> getAny() {
return Collections.unmodifiableMap( any );
}
}

View File

@@ -18,11 +18,19 @@
package com.lyndir.masterpassword.model;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
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 java.io.IOException;
import java.util.*;
import javax.annotation.Nullable;
import org.joda.time.Instant;
@@ -30,61 +38,72 @@ import org.joda.time.Instant;
/**
* @author lhunath, 2018-04-27
*/
public class MPJSONFile {
public class MPJSONFile extends MPJSONAnyObject {
public MPJSONFile(final MPFileUser user)
protected static final ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.setSerializationInclusion( JsonInclude.Include.NON_EMPTY );
objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE );
}
public MPJSONFile write(final MPFileUser modelUser)
throws MPKeyUnavailableException {
// Section: "export"
Export fileExport = this.export = new Export();
fileExport.format = 1;
fileExport.redacted = user.getContentMode().isRedacted();
fileExport.date = MPConstant.dateTimeFormatter.print( new Instant() );
if (export == null)
export = new Export();
export.format = 1;
export.redacted = modelUser.getContentMode().isRedacted();
export.date = MPConstant.dateTimeFormatter.print( new Instant() );
// Section: "user"
User fileUser = this.user = new User();
fileUser.avatar = user.getAvatar();
fileUser.full_name = user.getFullName();
fileUser.last_used = MPConstant.dateTimeFormatter.print( user.getLastUsed() );
fileUser.key_id = CodeUtils.encodeHex( user.getKeyID() );
fileUser.algorithm = user.getAlgorithm().version();
fileUser.default_type = user.getDefaultType();
if (user == null)
user = new User();
user.avatar = modelUser.getAvatar();
user.full_name = modelUser.getFullName();
user.last_used = MPConstant.dateTimeFormatter.print( modelUser.getLastUsed() );
user.key_id = CodeUtils.encodeHex( modelUser.getKeyID() );
user.algorithm = modelUser.getAlgorithm().version();
user.default_type = modelUser.getDefaultType();
// Section "sites"
sites = new LinkedHashMap<>();
for (final MPFileSite site : user.getSites()) {
Site fileSite;
if (sites == null)
sites = new LinkedHashMap<>();
for (final MPFileSite modelSite : modelUser.getSites()) {
String content = null, loginContent = null;
if (!fileExport.redacted) {
if (!export.redacted) {
// Clear Text
content = site.getResult();
loginContent = user.getMasterKey().siteResult(
site.getSiteName(), site.getAlgorithm().mpw_default_counter(),
MPKeyPurpose.Identification, null, site.getLoginType(), site.getLoginState(), site.getAlgorithm() );
content = modelSite.getResult();
loginContent = modelUser.getMasterKey().siteResult(
modelSite.getSiteName(), modelSite.getAlgorithm().mpw_default_counter(),
MPKeyPurpose.Identification, null, modelSite.getLoginType(), modelSite.getLoginState(), modelSite.getAlgorithm() );
} else {
// Redacted
if (site.getResultType().supportsTypeFeature( MPSiteFeature.ExportContent ))
content = site.getSiteState();
if (site.getLoginType().supportsTypeFeature( MPSiteFeature.ExportContent ))
loginContent = site.getLoginState();
if (modelSite.getResultType().supportsTypeFeature( MPSiteFeature.ExportContent ))
content = modelSite.getSiteState();
if (modelSite.getLoginType().supportsTypeFeature( MPSiteFeature.ExportContent ))
loginContent = modelSite.getLoginState();
}
sites.put( site.getSiteName(), fileSite = new Site() );
fileSite.type = site.getResultType();
fileSite.counter = site.getSiteCounter().longValue();
fileSite.algorithm = site.getAlgorithm().version();
fileSite.password = content;
fileSite.login_name = loginContent;
fileSite.login_type = site.getLoginType();
Site site = sites.get( modelSite.getSiteName() );
if (site == null)
sites.put( modelSite.getSiteName(), site = new Site() );
site.type = modelSite.getResultType();
site.counter = modelSite.getSiteCounter().longValue();
site.algorithm = modelSite.getAlgorithm().version();
site.password = content;
site.login_name = loginContent;
site.login_type = modelSite.getLoginType();
fileSite.uses = site.getUses();
fileSite.last_used = MPConstant.dateTimeFormatter.print( site.getLastUsed() );
site.uses = modelSite.getUses();
site.last_used = MPConstant.dateTimeFormatter.print( modelSite.getLastUsed() );
fileSite._ext_mpw = new Site.Ext();
fileSite._ext_mpw.url = site.getUrl();
if (site._ext_mpw == null)
site._ext_mpw = new Site.Ext();
site._ext_mpw.url = modelSite.getUrl();
fileSite.questions = new LinkedHashMap<>();
if (site.questions == null)
site.questions = new LinkedHashMap<>();
// for (size_t q = 0; q < site.questions_count; ++q) {
// MPMarshalledQuestion *question = &site.questions[q];
// if (!question.keyword)
@@ -112,37 +131,44 @@ public class MPJSONFile {
// if (site.url)
// json_object_object_add( json_site_mpw, "url", site.url );
}
return this;
}
public MPFileUser toUser(@Nullable final char[] masterPassword)
public MPFileUser read(@Nullable final char[] masterPassword)
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException {
MPFileUser user = new MPFileUser(
this.user.full_name, CodeUtils.decodeHex( this.user.key_id ), this.user.algorithm.getAlgorithm(),
this.user.avatar, this.user.default_type, MPConstant.dateTimeFormatter.parseDateTime( this.user.last_used ),
MPAlgorithm algorithm = ifNotNullElse( user.algorithm, MPMasterKey.Version.CURRENT ).getAlgorithm();
MPFileUser model = new MPFileUser(
user.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar,
(user.default_type != null)? user.default_type: algorithm.mpw_default_password_type(),
(user.last_used != null)? MPConstant.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
MPMarshalFormat.JSON, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE );
model.setJSON( this );
if (masterPassword != null)
user.authenticate( masterPassword );
model.authenticate( masterPassword );
for (final Map.Entry<String, Site> siteEntry : sites.entrySet()) {
String siteName = siteEntry.getKey();
Site fileSite = siteEntry.getValue();
MPFileSite site = new MPFileSite(
user, siteName, export.redacted? fileSite.password: null, UnsignedInteger.valueOf( fileSite.counter ),
model, siteName, export.redacted? fileSite.password: null, UnsignedInteger.valueOf( fileSite.counter ),
fileSite.type, fileSite.algorithm.getAlgorithm(),
export.redacted? fileSite.login_name: null, fileSite.login_type,
fileSite._ext_mpw.url, fileSite.uses, MPConstant.dateTimeFormatter.parseDateTime( fileSite.last_used ) );
(fileSite._ext_mpw != null)? fileSite._ext_mpw.url: null, fileSite.uses,
(fileSite.last_used != null)? MPConstant.dateTimeFormatter.parseDateTime( fileSite.last_used ): new Instant() );
if (!export.redacted) {
if (fileSite.password != null)
site.setSitePassword( fileSite.type, fileSite.password );
site.setSitePassword( (fileSite.type != null)? fileSite.type: MPResultType.StoredPersonal, fileSite.password );
if (fileSite.login_name != null)
site.setLoginName( fileSite.login_type, fileSite.login_name );
site.setLoginName( (fileSite.login_type != null)? fileSite.login_type: MPResultType.StoredPersonal,
fileSite.login_name );
}
user.addSite( site );
model.addSite( site );
}
return user;
return model;
}
// -- Data
@@ -152,54 +178,65 @@ public class MPJSONFile {
Map<String, Site> sites;
public static class Export {
public static class Export extends MPJSONAnyObject {
int format;
boolean redacted;
String date;
@Nullable
String date;
}
public static class User {
public static class User extends MPJSONAnyObject {
int avatar;
String full_name;
String last_used;
int avatar;
String full_name;
String last_used;
@Nullable
String key_id;
@Nullable
MPMasterKey.Version algorithm;
@Nullable
MPResultType default_type;
}
public static class Site {
public static class Site extends MPJSONAnyObject {
MPResultType type;
@Nullable
MPResultType type;
long counter;
MPMasterKey.Version algorithm;
@Nullable
String password;
String password;
@Nullable
String login_name;
@Nullable
String login_name;
MPResultType login_type;
int uses;
String last_used;
int uses;
@Nullable
String last_used;
@Nullable
Map<String, Question> questions;
@Nullable
Ext _ext_mpw;
public static class Ext {
public static class Ext extends MPJSONAnyObject {
@Nullable
String url;
}
public static class Question {
public static class Question extends MPJSONAnyObject {
@Nullable
MPResultType type;
@Nullable
String answer;
}
}
}

View File

@@ -18,8 +18,10 @@
package com.lyndir.masterpassword.model;
import com.google.gson.*;
import com.lyndir.masterpassword.*;
import static com.lyndir.masterpassword.model.MPJSONFile.objectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.lyndir.masterpassword.MPKeyUnavailableException;
import javax.annotation.Nonnull;
@@ -28,17 +30,16 @@ 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.IDENTITY )
.setPrettyPrinting().create();
@Nonnull
@Override
public String marshall(final MPFileUser user)
throws MPKeyUnavailableException, MPMarshalException {
return gson.toJson( new MPJSONFile( user ) );
try {
return objectMapper.writeValueAsString( user.getJSON().write( user ) );
}
catch (final JsonProcessingException e) {
throw new MPMarshalException( "Couldn't compose JSON for: " + user, e );
}
}
}

View File

@@ -18,10 +18,13 @@
package com.lyndir.masterpassword.model;
import com.google.gson.*;
import com.lyndir.masterpassword.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import static com.lyndir.masterpassword.model.MPJSONFile.objectMapper;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.lyndir.masterpassword.MPKeyUnavailableException;
import java.io.File;
import java.io.IOException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -31,24 +34,19 @@ import javax.annotation.Nullable;
*/
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, @Nullable final char[] masterPassword)
throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException {
try (Reader reader = new InputStreamReader( new FileInputStream( file ), StandardCharsets.UTF_8 )) {
try {
return gson.fromJson( reader, MPJSONFile.class ).toUser( masterPassword );
}
catch (final JsonSyntaxException e) {
throw new MPMarshalException( "Couldn't parse JSON in: " + file, e );
}
try {
return objectMapper.readValue( file, MPJSONFile.class ).read( masterPassword );
}
catch (final JsonParseException e) {
throw new MPMarshalException( "Couldn't parse JSON in: " + file, e );
}
catch (final JsonMappingException e) {
throw new MPMarshalException( "Couldn't map JSON in: " + file, e );
}
}
@@ -58,10 +56,16 @@ public class MPJSONUnmarshaller implements MPUnmarshaller {
throws MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException {
try {
return gson.fromJson( content, MPJSONFile.class ).toUser( masterPassword );
return objectMapper.readValue( content, MPJSONFile.class ).read( masterPassword );
}
catch (final JsonSyntaxException e) {
throw new MPMarshalException( "Couldn't parse JSON", e );
catch (final JsonParseException e) {
throw new MPMarshalException( "Couldn't parse JSON.", e );
}
catch (final JsonMappingException e) {
throw new MPMarshalException( "Couldn't map JSON.", e );
}
catch (final IOException e) {
throw new MPMarshalException( "Couldn't read JSON.", e );
}
}
}