2
0

WIP - JSON mpsites serialization.

This commit is contained in:
Maarten Billemont 2018-05-03 13:49:34 +02:00
parent cb74b1f3fc
commit 1031414ba2
11 changed files with 378 additions and 31 deletions

View File

@ -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)

View File

@ -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'

View File

@ -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 <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

@ -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;
}

View File

@ -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() );
}
/**

View File

@ -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 <http://www.gnu.org/licenses/>.
//==============================================================================
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<String, Site> 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<String, Question> questions;
}
public static class Question {
String keyword;
String content;
MPResultType type;
}
}

View File

@ -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 ) );
}
}

View File

@ -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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
//==============================================================================
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<MPResultType>, JsonDeserializer<MPResultType> {
@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() );
}
}

View File

@ -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'

View File

@ -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 <http://www.gnu.org/licenses/>.
//==============================================================================
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" ) ) );
}
}