diff --git a/core/java/algorithm/build.gradle b/core/java/algorithm/build.gradle index 49a8fb70..e3e11937 100644 --- a/core/java/algorithm/build.gradle +++ b/core/java/algorithm/build.gradle @@ -11,6 +11,7 @@ dependencies { compile group: 'com.lyndir.lhunath.opal', name: 'opal-crypto', version: '1.6-p11' compile group: 'com.lambdaworks', name: 'scrypt', version: '1.4.0' + compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.5' compile group: 'org.jetbrains', name: 'annotations', version: '13.0' compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.1' } diff --git a/core/java/tests/src/test/java/com/lyndir/masterpassword/MPModelTest.java b/core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPKeyUnavailableException.java similarity index 51% rename from core/java/tests/src/test/java/com/lyndir/masterpassword/MPModelTest.java rename to core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPKeyUnavailableException.java index e0af955a..2eac4233 100644 --- a/core/java/tests/src/test/java/com/lyndir/masterpassword/MPModelTest.java +++ b/core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPKeyUnavailableException.java @@ -18,28 +18,8 @@ package com.lyndir.masterpassword; -import com.google.common.base.Charsets; -import com.google.common.io.CharStreams; -import com.lyndir.masterpassword.model.*; -import java.io.*; -import org.testng.Assert; -import org.testng.annotations.Test; - - /** - * @author lhunath, 2018-04-27 + * @author lhunath, 2017-09-21 */ -public class MPModelTest { - - @Test - public void testMasterKey() - throws Exception { - File file = new File( "/Users/lhunath/.mpw.d/Maarten Billemont.mpsites.json" ); - String orig = CharStreams.toString( new InputStreamReader( new FileInputStream( file ), Charsets.UTF_8 ) ); - System.out.println(orig); - MPFileUser user = new MPJSONUnmarshaller().unmarshall( file, null ); - String result = new MPJSONMarshaller().marshall( user ); - System.out.println(result); - Assert.assertEquals( result, orig, "Marshalled sites do not match original sites." ); - } +public class MPKeyUnavailableException extends Exception { } diff --git a/core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java b/core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java index f9883326..660d8a6c 100644 --- a/core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java +++ b/core/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java @@ -18,6 +18,8 @@ package com.lyndir.masterpassword; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; import com.google.common.base.Preconditions; import com.google.common.primitives.UnsignedInteger; import com.lyndir.lhunath.opal.system.CodeUtils; @@ -232,11 +234,13 @@ public class MPMasterKey { return algorithm; } + @JsonCreator public static Version fromInt(final int algorithmVersion) { return values()[algorithmVersion]; } + @JsonValue public int toInt() { return ordinal(); 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 7b09a4d5..6b0eb968 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 @@ -18,6 +18,8 @@ package com.lyndir.masterpassword; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.lyndir.lhunath.opal.system.logging.Logger; @@ -179,6 +181,7 @@ public enum MPResultType { return typeFeatures.contains( feature ); } + @JsonValue public int getType() { int mask = typeIndex | typeClass.getMask(); for (final MPSiteFeature typeFeature : typeFeatures) @@ -226,6 +229,7 @@ public enum MPResultType { * * @return The type registered with the given type. */ + @JsonCreator public static MPResultType forType(final int type) { for (final MPResultType resultType : values()) diff --git a/core/java/model/build.gradle b/core/java/model/build.gradle index fc2e1569..039b4fbd 100644 --- a/core/java/model/build.gradle +++ b/core/java/model/build.gradle @@ -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() 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 deleted file mode 100644 index feb0961b..00000000 --- a/core/java/model/src/main/java/com/lyndir/masterpassword/model/EnumOrdinalAdapter.java +++ /dev/null @@ -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 . -//============================================================================== - -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/MPFileUser.java b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileUser.java index 099ddf83..97fbbb48 100755 --- a/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileUser.java +++ b/core/java/model/src/main/java/com/lyndir/masterpassword/model/MPFileUser.java @@ -51,6 +51,9 @@ public class MPFileUser extends MPUser implements Comparable implements Comparable, JsonDeserializer { +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 any = new LinkedHashMap<>(); - @Override - public JsonElement serialize(final MPResultType src, final Type typeOfSrc, final JsonSerializationContext context) { - return new JsonPrimitive( src.getType() ); + @JsonAnyGetter + public Map getAny() { + return Collections.unmodifiableMap( any ); } } 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 index d52ba3f0..97cd8db7 100644 --- 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 @@ -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 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 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 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; } } } + 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 f6833e89..2c5fd380 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,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 ); + } } } 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 67ed3a0e..28dcc53e 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,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 ); } } } diff --git a/core/java/tests/src/main/resources/test.mpsites.json b/core/java/tests/src/main/resources/test.mpsites.json new file mode 100644 index 00000000..edaa668d --- /dev/null +++ b/core/java/tests/src/main/resources/test.mpsites.json @@ -0,0 +1,67 @@ +{ + "export": { + "format": 1, + "redacted": true, + "date": "2018-05-10T03:41:18Z", + "_ext_mpw": { + "save": "me" + }, + "_ext_other": { + "save": "me" + } + }, + "user": { + "avatar": 3, + "full_name": "Robert Lee Mitchell", + "last_used": "2018-05-10T03:41:18Z", + "key_id": "98EEF4D1DF46D849574A82A03C3177056B15DFFCA29BB3899DE4628453675302", + "algorithm": 3, + "default_type": 17, + "_ext_mpw": { + "save": "me" + }, + "_ext_other": { + "save": "me" + } + }, + "sites": { + "masterpasswordapp.com": { + "type": 17, + "counter": 1, + "algorithm": 3, + "login_type": 30, + "uses": 2, + "last_used": "2018-05-10T03:41:18Z", + "questions": { + "": { + "type": 31 + }, + "mother": { + "type": 31 + } + }, + "_ext_mpw": { + "url": "https://masterpasswordapp.com", + "save": "me" + }, + "_ext_other": { + "save": "me" + } + }, + "personal.site": { + "type": 1056, + "counter": 1, + "algorithm": 3, + "password": "ZTgr4cY6L28wG7DsO+iz\/hrTQxM3UHz0x8ZU99LjgxjHG+bLIJygkbg\/7HdjEIFH6A3z+Dt2H1gpt9yPyQGZcewTiPXJX0pNpVsIKAAdzVNcUfYoqkWjoFRoZD7sM\/ctxWDH4JUuJ+rjoBkWtRLK9kYBvu7UD1QdlEZI\/wPKv1A=", + "login_type": 30, + "uses": 1, + "last_used": "2018-05-10T03:48:35Z" + } + }, + "_ext_mpw": { + "save": "me" + }, + "_ext_other": { + "save": "me" + } + }