]> nmode's Git Repositories - signal-cli/commitdiff
Extract SignalAccount from Manager
authorAsamK <asamk@gmx.de>
Sun, 18 Nov 2018 14:34:10 +0000 (15:34 +0100)
committerAsamK <asamk@gmx.de>
Sun, 18 Nov 2018 16:12:39 +0000 (17:12 +0100)
30 files changed:
.idea/codeStyles/Project.xml
build.gradle
src/main/java/org/asamk/Signal.java
src/main/java/org/asamk/signal/AttachmentInvalidException.java
src/main/java/org/asamk/signal/JsonAttachment.java
src/main/java/org/asamk/signal/JsonCallMessage.java
src/main/java/org/asamk/signal/JsonDataMessage.java
src/main/java/org/asamk/signal/JsonError.java
src/main/java/org/asamk/signal/JsonGroupInfo.java
src/main/java/org/asamk/signal/JsonMessageEnvelope.java
src/main/java/org/asamk/signal/JsonSyncMessage.java
src/main/java/org/asamk/signal/Main.java
src/main/java/org/asamk/signal/UserAlreadyExists.java
src/main/java/org/asamk/signal/manager/BaseConfig.java
src/main/java/org/asamk/signal/manager/KeyUtils.java [moved from src/main/java/org/asamk/signal/util/KeyUtils.java with 78% similarity]
src/main/java/org/asamk/signal/manager/Manager.java
src/main/java/org/asamk/signal/storage/SignalAccount.java [new file with mode: 0644]
src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java
src/main/java/org/asamk/signal/storage/groups/GroupInfo.java
src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java
src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java
src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java
src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java
src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java
src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java
src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java
src/main/java/org/asamk/signal/util/IOUtils.java
src/main/java/org/asamk/signal/util/Util.java
src/main/resources/org/asamk/signal/manager/whisper.store [moved from src/main/resources/org/asamk/signal/whisper.store with 100% similarity]

index 1c2072370c279ee98f4abd50a5fe8ec25cd25c2c..2bf5e0cf4e7a5b81eb4b066f93ac2d311807365d 100644 (file)
@@ -2,10 +2,18 @@
   <code_scheme name="Project" version="173">
     <option name="LINE_SEPARATOR" value="&#10;" />
     <JavaCodeStyleSettings>
+      <option name="GENERATE_FINAL_LOCALS" value="true" />
+      <option name="GENERATE_FINAL_PARAMETERS" value="true" />
       <option name="JD_P_AT_EMPTY_LINES" value="false" />
     </JavaCodeStyleSettings>
     <XML>
       <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
     </XML>
+    <codeStyleSettings language="JAVA">
+      <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
+      <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
+      <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
+      <option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
+    </codeStyleSettings>
   </code_scheme>
 </component>
\ No newline at end of file
index 80242e8c07ccf34c28eeedb4d8741550fc81a5d5..66b2510ad59d4cac0ec4add26f300ccaa9c32b97 100644 (file)
@@ -20,7 +20,7 @@ repositories {
 }
 
 dependencies {
-    compile 'com.github.turasa:signal-service-java:2.12.2_unofficial_1'
+    compile 'com.github.turasa:signal-service-java:2.12.2_unofficial_2'
     compile 'org.bouncycastle:bcprov-jdk15on:1.60'
     compile 'net.sourceforge.argparse4j:argparse4j:0.8.1'
     compile 'org.freedesktop.dbus:dbus-java:2.7.0'
index 88b15926b485956933a598ddb23478fda9b19769..7c311813ac4041e9906a75519e9c115f3d3ce3b9 100644 (file)
@@ -11,6 +11,7 @@ import java.io.IOException;
 import java.util.List;
 
 public interface Signal extends DBusInterface {
+
     void sendMessage(String message, List<String> attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
 
     void sendMessage(String message, List<String> attachments, List<String> recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
@@ -32,6 +33,7 @@ public interface Signal extends DBusInterface {
     byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException;
 
     class MessageReceived extends DBusSignal {
+
         private long timestamp;
         private String sender;
         private byte[] groupId;
@@ -69,6 +71,7 @@ public interface Signal extends DBusInterface {
     }
 
     class ReceiptReceived extends DBusSignal {
+
         private long timestamp;
         private String sender;
 
index 8a023f6283fac5c7da87d2b5bfb093d1c1f36f83..839c79408cf91f3da0ae32565462f7b737dc0219 100644 (file)
@@ -3,6 +3,7 @@ package org.asamk.signal;
 import org.freedesktop.dbus.exceptions.DBusExecutionException;
 
 public class AttachmentInvalidException extends DBusExecutionException {
+
     public AttachmentInvalidException(String message) {
         super(message);
     }
index 53946df9df262d9a9a2ae2c87ba2681ed4b662ef..29e8592ef9361bbf811449a77a0cff2022daad6d 100644 (file)
@@ -4,6 +4,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
 
 class JsonAttachment {
+
     String contentType;
     long id;
     int size;
index 98a01b29823fb8a8da455dc402cd72ef6fbab6a8..b10e6f7b534437eb255938e53f8b07c2fe0feb36 100644 (file)
@@ -5,6 +5,7 @@ import org.whispersystems.signalservice.api.messages.calls.*;
 import java.util.List;
 
 class JsonCallMessage {
+
     OfferMessage offerMessage;
     AnswerMessage answerMessage;
     BusyMessage busyMessage;
index eda54025e8036d528ec0c3cdd585b0e8bb9973b3..34f6249e3e5ba8c46a29547fd3f6c22395bfc5c4 100644 (file)
@@ -7,6 +7,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 class JsonDataMessage {
+
     long timestamp;
     String message;
     int expiresInSeconds;
index 05fe3ae637df4d60af479b9dad25490420a8bd87..5ef2cd7c870369e7a5f3b276cf2e6afe04568928 100644 (file)
@@ -1,6 +1,7 @@
 package org.asamk.signal;
 
 class JsonError {
+
     String message;
 
     JsonError(Throwable exception) {
index 89c5515f730e04d483faefedf5d5b3cb92e62466..073ad3ff7fbc0dbc85bf7564c25fe02dfb183594 100644 (file)
@@ -6,6 +6,7 @@ import org.whispersystems.signalservice.internal.util.Base64;
 import java.util.List;
 
 class JsonGroupInfo {
+
     String groupId;
     List<String> members;
     String name;
index 2ce39cc5a6e281d25ad61d13033c528d87e04f7f..9971b01192c3a279aa3a62712be42702b1280812 100644 (file)
@@ -5,6 +5,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 
 class JsonMessageEnvelope {
+
     String source;
     int sourceDevice;
     String relay;
index 92ad1cc5ea784cb107a6b382a830afdf82768afb..febf64a444719931a97498ebb13a185329da5c6b 100644 (file)
@@ -6,6 +6,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
 import java.util.List;
 
 class JsonSyncMessage {
+
     JsonDataMessage sentMessage;
     List<String> blockedNumbers;
     List<ReadMessage> readMessages;
index e534f05ea7dbbd4aad6eda1ebc5d4780b9fa5aaa..810f1deb4b59cca63590aec05b9bdd53ae3343d0 100644 (file)
@@ -129,13 +129,11 @@ public class Main {
 
                 m = new Manager(username, settingsPath);
                 ts = m;
-                if (m.userExists()) {
-                    try {
-                        m.init();
-                    } catch (Exception e) {
-                        System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage());
-                        return 2;
-                    }
+                try {
+                    m.init();
+                } catch (Exception e) {
+                    System.err.println("Error loading state file: " + e.getMessage());
+                    return 2;
                 }
             }
 
@@ -145,9 +143,6 @@ public class Main {
                         System.err.println("register is not yet implemented via dbus");
                         return 1;
                     }
-                    if (!m.userHasKeys()) {
-                        m.createNewIdentity();
-                    }
                     try {
                         m.register(ns.getBoolean("voice"));
                     } catch (IOException e) {
@@ -252,9 +247,6 @@ public class Main {
                         return 1;
                     }
 
-                    // When linking, username is null and we always have to create keys
-                    m.createNewIdentity();
-
                     String deviceName = ns.getString("name");
                     if (deviceName == null) {
                         deviceName = "cli";
@@ -736,7 +728,6 @@ public class Main {
         System.err.println("Aborting sending.");
     }
 
-
     private static void handleDBusExecutionException(DBusExecutionException e) {
         System.err.println("Cannot connect to dbus: " + e.getMessage());
         System.err.println("Aborting.");
@@ -949,6 +940,7 @@ public class Main {
     }
 
     private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
+
         final Manager m;
 
         public ReceiveMessageHandler(Manager m) {
@@ -1212,6 +1204,7 @@ public class Main {
     }
 
     private static class DbusReceiveMessageHandler extends ReceiveMessageHandler {
+
         final DBusConnection conn;
 
         public DbusReceiveMessageHandler(Manager m, DBusConnection conn) {
@@ -1228,6 +1221,7 @@ public class Main {
     }
 
     private static class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler {
+
         final Manager m;
         final ObjectMapper jsonProcessor;
 
@@ -1259,6 +1253,7 @@ public class Main {
     }
 
     private static class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
+
         final DBusConnection conn;
 
         public JsonDbusReceiveMessageHandler(Manager m, DBusConnection conn) {
@@ -1266,13 +1261,6 @@ public class Main {
             this.conn = conn;
         }
 
-        @Override
-        public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) {
-            super.handleMessage(envelope, content, exception);
-
-            sendReceivedMessageToDbus(envelope, content, conn, m);
-        }
-
         private static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, Manager m) {
             if (envelope.isReceipt()) {
                 try {
@@ -1313,5 +1301,12 @@ public class Main {
                 }
             }
         }
+
+        @Override
+        public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) {
+            super.handleMessage(envelope, content, exception);
+
+            sendReceivedMessageToDbus(envelope, content, conn, m);
+        }
     }
 }
index 2c018ed9e257ffb2beff51bc87ddb9d1c54994ec..047b5fc7ff7f12d98858265bb2759ab83a41ed46 100644 (file)
@@ -1,6 +1,7 @@
 package org.asamk.signal;
 
 public class UserAlreadyExists extends Exception {
+
     private String username;
     private String fileName;
 
index 19d43fc853caf5a7a94252bfdb58cb3f594475b1..0f503352426cff327fe7390f4dc2d681b0585866 100644 (file)
@@ -8,26 +8,25 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
 
 public class BaseConfig {
 
-    private BaseConfig() {
-    }
-
     public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
     public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
+
     final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION;
+    final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF";
+    final static int PREKEY_MINIMUM_COUNT = 20;
+    final static int PREKEY_BATCH_SIZE = 100;
+    final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
 
     private final static String URL = "https://textsecure-service.whispersystems.org";
     private final static String CDN_URL = "https://cdn.signal.org";
-
     private final static TrustStore TRUST_STORE = new WhisperTrustStore();
+
     final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration(
             new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
             new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
             new SignalContactDiscoveryUrl[0]
     );
 
-    final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF";
-
-    final static int PREKEY_MINIMUM_COUNT = 20;
-    static final int PREKEY_BATCH_SIZE = 100;
-    static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
+    private BaseConfig() {
+    }
 }
similarity index 78%
rename from src/main/java/org/asamk/signal/util/KeyUtils.java
rename to src/main/java/org/asamk/signal/manager/KeyUtils.java
index ab42138406667504f8cb242d27eaf58fb525dda7..225cf68206b7a6b313c5ff79e3cfe5c4bf7b5ee8 100644 (file)
@@ -1,28 +1,28 @@
-package org.asamk.signal.util;
+package org.asamk.signal.manager;
 
 import org.whispersystems.signalservice.internal.util.Base64;
 
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 
-public class KeyUtils {
+class KeyUtils {
 
     private KeyUtils() {
     }
 
-    public static String createSignalingKey() {
+    static String createSignalingKey() {
         return getSecret(52);
     }
 
-    public static byte[] createProfileKey() {
+    static byte[] createProfileKey() {
         return getSecretBytes(32);
     }
 
-    public static String createPassword() {
+    static String createPassword() {
         return getSecret(18);
     }
 
-    public static byte[] createGroupId() {
+    static byte[] createGroupId() {
         return getSecretBytes(16);
     }
 
index a97c11ea3070dda811d4904bad681f4495c4a44b..e8860ef8a6af4803452c8516be0cf4fc72f682e1 100644 (file)
  */
 package org.asamk.signal.manager;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.http.util.TextUtils;
 import org.asamk.Signal;
 import org.asamk.signal.*;
+import org.asamk.signal.storage.SignalAccount;
 import org.asamk.signal.storage.contacts.ContactInfo;
-import org.asamk.signal.storage.contacts.JsonContactsStore;
 import org.asamk.signal.storage.groups.GroupInfo;
 import org.asamk.signal.storage.groups.JsonGroupStore;
 import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
-import org.asamk.signal.storage.protocol.JsonSignalProtocolStore;
-import org.asamk.signal.storage.threads.JsonThreadStore;
 import org.asamk.signal.storage.threads.ThreadInfo;
 import org.asamk.signal.util.IOUtils;
-import org.asamk.signal.util.KeyUtils;
 import org.asamk.signal.util.Util;
 import org.signal.libsignal.metadata.*;
 import org.signal.libsignal.metadata.certificate.CertificateValidator;
@@ -79,9 +67,6 @@ import java.io.*;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLEncoder;
-import java.nio.channels.Channels;
-import java.nio.channels.FileChannel;
-import java.nio.channels.FileLock;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
@@ -96,27 +81,10 @@ public class Manager implements Signal {
     private final String attachmentsPath;
     private final String avatarsPath;
 
-    private FileChannel fileChannel;
-    private FileLock lock;
+    private SignalAccount account;
 
-    private final ObjectMapper jsonProcessor = new ObjectMapper();
     private String username;
-    private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
-    private boolean isMultiDevice = false;
-    private String password;
-    private String registrationLockPin;
-    private String signalingKey;
-    private byte[] profileKey;
-    private int preKeyIdOffset;
-    private int nextSignedPreKeyId;
-
-    private boolean registered = false;
-
-    private JsonSignalProtocolStore signalProtocolStore;
     private SignalServiceAccountManager accountManager;
-    private JsonGroupStore groupStore;
-    private JsonContactsStore contactStore;
-    private JsonThreadStore threadStore;
     private SignalServiceMessagePipe messagePipe = null;
     private SignalServiceMessagePipe unidentifiedMessagePipe = null;
 
@@ -129,12 +97,43 @@ public class Manager implements Signal {
         this.attachmentsPath = this.settingsPath + "/attachments";
         this.avatarsPath = this.settingsPath + "/avatars";
 
-        jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
-        jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
-        jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
-        jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
-        jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
-        jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
+    }
+
+    private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
+        List<SignalServiceAttachment> SignalServiceAttachments = null;
+        if (attachments != null) {
+            SignalServiceAttachments = new ArrayList<>(attachments.size());
+            for (String attachment : attachments) {
+                try {
+                    SignalServiceAttachments.add(createAttachment(new File(attachment)));
+                } catch (IOException e) {
+                    throw new AttachmentInvalidException(attachment, e);
+                }
+            }
+        }
+        return SignalServiceAttachments;
+    }
+
+    private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
+        InputStream attachmentStream = new FileInputStream(attachmentFile);
+        final long attachmentSize = attachmentFile.length();
+        String mime = Files.probeContentType(attachmentFile.toPath());
+        if (mime == null) {
+            mime = "application/octet-stream";
+        }
+        // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option
+        Optional<byte[]> preview = Optional.absent();
+        Optional<String> caption = Optional.absent();
+        return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, null);
+    }
+
+    private static CertificateValidator getCertificateValidator() {
+        try {
+            ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
+            return new CertificateValidator(unidentifiedSenderTrustRoot);
+        } catch (InvalidKeyException | IOException e) {
+            throw new AssertionError(e);
+        }
     }
 
     public String getUsername() {
@@ -142,15 +141,11 @@ public class Manager implements Signal {
     }
 
     private IdentityKey getIdentity() {
-        return signalProtocolStore.getIdentityKeyPair().getPublicKey();
+        return account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
     }
 
     public int getDeviceId() {
-        return deviceId;
-    }
-
-    public String getFileName() {
-        return dataPath + "/" + username;
+        return account.getDeviceId();
     }
 
     private String getMessageCachePath() {
@@ -167,118 +162,29 @@ public class Manager implements Signal {
         return new File(cachePath + "/" + now + "_" + timestamp);
     }
 
-    public boolean userExists() {
-        if (username == null) {
-            return false;
-        }
-        File f = new File(getFileName());
-        return !(!f.exists() || f.isDirectory());
-    }
-
     public boolean userHasKeys() {
-        return signalProtocolStore != null;
+        return account.getSignalProtocolStore() != null;
     }
 
-    private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
-        JsonNode node = parent.get(name);
-        if (node == null) {
-            throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
-        }
-
-        return node;
-    }
-
-    private void openFileChannel() throws IOException {
-        if (fileChannel != null)
+    public void init() throws IOException {
+        if (!SignalAccount.userExists(dataPath, username)) {
             return;
-
-        IOUtils.createPrivateDirectories(dataPath);
-        if (!new File(getFileName()).exists()) {
-            IOUtils.createPrivateFile(getFileName());
         }
-        fileChannel = new RandomAccessFile(new File(getFileName()), "rw").getChannel();
-        lock = fileChannel.tryLock();
-        if (lock == null) {
-            System.err.println("Config file is in use by another instance, waiting…");
-            lock = fileChannel.lock();
-            System.err.println("Config file lock acquired.");
-        }
-    }
-
-    public void init() throws IOException {
-        load();
+        account = SignalAccount.load(dataPath, username);
 
         migrateLegacyConfigs();
 
-        accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, password, deviceId, BaseConfig.USER_AGENT, timer);
+        accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer);
         try {
-            if (registered && accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) {
+            if (account.isRegistered() && accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) {
                 refreshPreKeys();
-                save();
+                account.save();
             }
         } catch (AuthorizationFailedException e) {
             System.err.println("Authorization failed, was the number registered elsewhere?");
         }
     }
 
-    private void load() throws IOException {
-        openFileChannel();
-        JsonNode rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
-
-        JsonNode node = rootNode.get("deviceId");
-        if (node != null) {
-            deviceId = node.asInt();
-        }
-        username = getNotNullNode(rootNode, "username").asText();
-        password = getNotNullNode(rootNode, "password").asText();
-        JsonNode pinNode = rootNode.get("registrationLockPin");
-        registrationLockPin = pinNode == null ? null : pinNode.asText();
-        if (rootNode.has("signalingKey")) {
-            signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
-        }
-        if (rootNode.has("preKeyIdOffset")) {
-            preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
-        } else {
-            preKeyIdOffset = 0;
-        }
-        if (rootNode.has("nextSignedPreKeyId")) {
-            nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
-        } else {
-            nextSignedPreKeyId = 0;
-        }
-        if (rootNode.has("profileKey")) {
-            profileKey = Base64.decode(getNotNullNode(rootNode, "profileKey").asText());
-        } else {
-            // Old config file, creating new profile key
-            profileKey = KeyUtils.createProfileKey();
-        }
-
-        signalProtocolStore = jsonProcessor.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
-        registered = getNotNullNode(rootNode, "registered").asBoolean();
-        JsonNode groupStoreNode = rootNode.get("groupStore");
-        if (groupStoreNode != null) {
-            groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
-        }
-        if (groupStore == null) {
-            groupStore = new JsonGroupStore();
-        }
-
-        JsonNode contactStoreNode = rootNode.get("contactStore");
-        if (contactStoreNode != null) {
-            contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
-        }
-        if (contactStore == null) {
-            contactStore = new JsonContactsStore();
-        }
-        JsonNode threadStoreNode = rootNode.get("threadStore");
-        if (threadStoreNode != null) {
-            threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class);
-        }
-        if (threadStore == null) {
-            threadStore = new JsonThreadStore();
-        }
-    }
-
     private void migrateLegacyConfigs() {
         // Copy group avatars that were previously stored in the attachments folder
         // to the new avatar folder
@@ -296,68 +202,48 @@ public class Manager implements Signal {
                 }
             }
             JsonGroupStore.groupsWithLegacyAvatarId.clear();
-            save();
-        }
-    }
-
-    private void save() {
-        if (username == null) {
-            return;
+            account.save();
         }
-        ObjectNode rootNode = jsonProcessor.createObjectNode();
-        rootNode.put("username", username)
-                .put("deviceId", deviceId)
-                .put("password", password)
-                .put("registrationLockPin", registrationLockPin)
-                .put("signalingKey", signalingKey)
-                .put("preKeyIdOffset", preKeyIdOffset)
-                .put("nextSignedPreKeyId", nextSignedPreKeyId)
-                .put("registered", registered)
-                .putPOJO("axolotlStore", signalProtocolStore)
-                .putPOJO("groupStore", groupStore)
-                .putPOJO("contactStore", contactStore)
-                .putPOJO("threadStore", threadStore)
-        ;
-        try {
-            openFileChannel();
-            fileChannel.position(0);
-            jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode);
-            fileChannel.truncate(fileChannel.position());
-            fileChannel.force(false);
-        } catch (Exception e) {
-            System.err.println(String.format("Error saving file: %s", e.getMessage()));
+        if (account.getProfileKey() == null) {
+            // Old config file, creating new profile key
+            account.setProfileKey(KeyUtils.createProfileKey());
         }
     }
 
-    public void createNewIdentity() {
+    private void createNewIdentity() throws IOException {
         IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
         int registrationId = KeyHelper.generateRegistrationId(false);
-        signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
-        groupStore = new JsonGroupStore();
-        registered = false;
-        save();
+        if (username == null) {
+            account = SignalAccount.createTemporaryAccount(identityKey, registrationId);
+        } else {
+            byte[] profileKey = KeyUtils.createProfileKey();
+            account = SignalAccount.create(dataPath, username, identityKey, registrationId, profileKey);
+            account.save();
+        }
     }
 
     public boolean isRegistered() {
-        return registered;
+        return account != null && account.isRegistered();
     }
 
     public void register(boolean voiceVerification) throws IOException {
-        password = KeyUtils.createPassword();
-
-        accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, password, BaseConfig.USER_AGENT, timer);
+        if (account == null) {
+            createNewIdentity();
+        }
+        account.setPassword(KeyUtils.createPassword());
+        accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, account.getUsername(), account.getPassword(), BaseConfig.USER_AGENT, timer);
 
         if (voiceVerification)
             accountManager.requestVoiceVerificationCode();
         else
             accountManager.requestSmsVerificationCode();
 
-        registered = false;
-        save();
+        account.setRegistered(false);
+        account.save();
     }
 
     public void updateAccountAttributes() throws IOException {
-        accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true, registrationLockPin, getSelfUnidentifiedAccessKey(), false);
+        accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), getSelfUnidentifiedAccessKey(), false);
     }
 
     public void unregister() throws IOException {
@@ -368,14 +254,15 @@ public class Manager implements Signal {
     }
 
     public URI getDeviceLinkUri() throws TimeoutException, IOException {
-        password = KeyUtils.createPassword();
-
-        accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, password, BaseConfig.USER_AGENT, timer);
+        if (account == null) {
+            createNewIdentity();
+        }
+        account.setPassword(KeyUtils.createPassword());
+        accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, account.getPassword(), BaseConfig.USER_AGENT, timer);
         String uuid = accountManager.getNewDeviceUuid();
 
-        registered = false;
         try {
-            return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8"));
+            return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(getIdentity().serialize()), "utf-8"));
         } catch (URISyntaxException e) {
             // Shouldn't happen
             return null;
@@ -383,29 +270,33 @@ public class Manager implements Signal {
     }
 
     public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
-        signalingKey = KeyUtils.createSignalingKey();
-        SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName);
-        deviceId = ret.getDeviceId();
+        account.setSignalingKey(KeyUtils.createSignalingKey());
+        SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(account.getSignalProtocolStore().getIdentityKeyPair(), account.getSignalingKey(), false, true, account.getSignalProtocolStore().getLocalRegistrationId(), deviceName);
+
         username = ret.getNumber();
         // TODO do this check before actually registering
-        if (userExists()) {
-            throw new UserAlreadyExists(username, getFileName());
+        if (SignalAccount.userExists(dataPath, username)) {
+            throw new UserAlreadyExists(username, SignalAccount.getFileName(dataPath, username));
+        }
+
+        // Create new account with the synced identity
+        byte[] profileKey = ret.getProfileKey();
+        if (profileKey == null) {
+            profileKey = KeyUtils.createProfileKey();
         }
-        signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId());
+        account = SignalAccount.createLinkedAccount(dataPath, username, account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey);
 
-        registered = true;
-        isMultiDevice = true;
         refreshPreKeys();
 
         requestSyncGroups();
         requestSyncContacts();
 
-        save();
+        account.save();
     }
 
     public List<DeviceInfo> getLinkedDevices() throws IOException {
         List<DeviceInfo> devices = accountManager.getDevices();
-        isMultiDevice = devices.size() > 1;
+        account.setMultiDevice(devices.size() > 1);
         return devices;
     }
 
@@ -428,27 +319,27 @@ public class Manager implements Signal {
     }
 
     private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
-        IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair();
+        IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair();
         String verificationCode = accountManager.getNewDeviceVerificationCode();
 
-        accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(profileKey), verificationCode);
-        isMultiDevice = true;
+        accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey()), verificationCode);
+        account.setMultiDevice(true);
     }
 
     private List<PreKeyRecord> generatePreKeys() {
-        List<PreKeyRecord> records = new LinkedList<>();
+        List<PreKeyRecord> records = new ArrayList<>(BaseConfig.PREKEY_BATCH_SIZE);
 
+        final int offset = account.getPreKeyIdOffset();
         for (int i = 0; i < BaseConfig.PREKEY_BATCH_SIZE; i++) {
-            int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
+            int preKeyId = (offset + i) % Medium.MAX_VALUE;
             ECKeyPair keyPair = Curve.generateKeyPair();
             PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
 
-            signalProtocolStore.storePreKey(preKeyId, record);
             records.add(record);
         }
 
-        preKeyIdOffset = (preKeyIdOffset + BaseConfig.PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE;
-        save();
+        account.addPreKeys(records);
+        account.save();
 
         return records;
     }
@@ -457,11 +348,10 @@ public class Manager implements Signal {
         try {
             ECKeyPair keyPair = Curve.generateKeyPair();
             byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
-            SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
+            SignedPreKeyRecord record = new SignedPreKeyRecord(account.getNextSignedPreKeyId(), System.currentTimeMillis(), keyPair, signature);
 
-            signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record);
-            nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
-            save();
+            account.addSignedPreKey(record);
+            account.save();
 
             return record;
         } catch (InvalidKeyException e) {
@@ -471,60 +361,32 @@ public class Manager implements Signal {
 
     public void verifyAccount(String verificationCode, String pin) throws IOException {
         verificationCode = verificationCode.replace("-", "");
-        signalingKey = KeyUtils.createSignalingKey();
-        accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false);
+        account.setSignalingKey(KeyUtils.createSignalingKey());
+        accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false);
 
         //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
-        registered = true;
-        registrationLockPin = pin;
+        account.setRegistered(true);
+        account.setRegistrationLockPin(pin);
 
         refreshPreKeys();
-        save();
+        account.save();
     }
 
     public void setRegistrationLockPin(Optional<String> pin) throws IOException {
         accountManager.setPin(pin);
         if (pin.isPresent()) {
-            registrationLockPin = pin.get();
+            account.setRegistrationLockPin(pin.get());
         } else {
-            registrationLockPin = null;
+            account.setRegistrationLockPin(null);
         }
     }
 
     private void refreshPreKeys() throws IOException {
         List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
-        SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair());
+        final IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair();
+        SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(identityKeyPair);
 
-        accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), signedPreKeyRecord, oneTimePreKeys);
-    }
-
-
-    private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
-        List<SignalServiceAttachment> SignalServiceAttachments = null;
-        if (attachments != null) {
-            SignalServiceAttachments = new ArrayList<>(attachments.size());
-            for (String attachment : attachments) {
-                try {
-                    SignalServiceAttachments.add(createAttachment(new File(attachment)));
-                } catch (IOException e) {
-                    throw new AttachmentInvalidException(attachment, e);
-                }
-            }
-        }
-        return SignalServiceAttachments;
-    }
-
-    private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
-        InputStream attachmentStream = new FileInputStream(attachmentFile);
-        final long attachmentSize = attachmentFile.length();
-        String mime = Files.probeContentType(attachmentFile.toPath());
-        if (mime == null) {
-            mime = "application/octet-stream";
-        }
-        // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option
-        Optional<byte[]> preview = Optional.absent();
-        Optional<String> caption = Optional.absent();
-        return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, null);
+        accountManager.setPreKeys(getIdentity(), signedPreKeyRecord, oneTimePreKeys);
     }
 
     private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
@@ -546,7 +408,7 @@ public class Manager implements Signal {
     }
 
     private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException {
-        GroupInfo g = groupStore.getGroup(groupId);
+        GroupInfo g = account.getGroupStore().getGroup(groupId);
         if (g == null) {
             throw new GroupNotFoundException(groupId);
         }
@@ -559,7 +421,7 @@ public class Manager implements Signal {
     }
 
     public List<GroupInfo> getGroups() {
-        return groupStore.getGroups();
+        return account.getGroupStore().getGroups();
     }
 
     @Override
@@ -576,7 +438,7 @@ public class Manager implements Signal {
                     .build();
             messageBuilder.asGroupMessage(group);
         }
-        ThreadInfo thread = threadStore.getThread(Base64.encodeBytes(groupId));
+        ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId));
         if (thread != null) {
             messageBuilder.withExpiration(thread.messageExpirationTime);
         }
@@ -599,7 +461,7 @@ public class Manager implements Signal {
 
         final GroupInfo g = getGroupForSending(groupId);
         g.members.remove(this.username);
-        groupStore.updateGroup(g);
+        account.getGroupStore().updateGroup(g);
 
         sendMessageLegacy(messageBuilder, g.members);
     }
@@ -652,7 +514,7 @@ public class Manager implements Signal {
             Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
         }
 
-        groupStore.updateGroup(g);
+        account.getGroupStore().updateGroup(g);
 
         SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
 
@@ -746,7 +608,7 @@ public class Manager implements Signal {
 
     @Override
     public String getContactName(String number) {
-        ContactInfo contact = contactStore.getContact(number);
+        ContactInfo contact = account.getContactStore().getContact(number);
         if (contact == null) {
             return "";
         } else {
@@ -756,7 +618,7 @@ public class Manager implements Signal {
 
     @Override
     public void setContactName(String number, String name) {
-        ContactInfo contact = contactStore.getContact(number);
+        ContactInfo contact = account.getContactStore().getContact(number);
         if (contact == null) {
             contact = new ContactInfo();
             contact.number = number;
@@ -765,8 +627,8 @@ public class Manager implements Signal {
             System.err.println("Updating contact " + number + " name " + contact.name + " -> " + name);
         }
         contact.name = name;
-        contactStore.updateContact(contact);
-        save();
+        account.getContactStore().updateContact(contact);
+        account.save();
     }
 
     @Override
@@ -816,7 +678,6 @@ public class Manager implements Signal {
         return sendUpdateGroupMessage(groupId, name, members, avatar);
     }
 
-
     /**
      * Change the expiration timer for a thread (number of groupId)
      *
@@ -824,9 +685,9 @@ public class Manager implements Signal {
      * @param messageExpirationTimer
      */
     public void setExpirationTimer(String numberOrGroupId, int messageExpirationTimer) {
-        ThreadInfo thread = threadStore.getThread(numberOrGroupId);
+        ThreadInfo thread = account.getThreadStore().getThread(numberOrGroupId);
         thread.messageExpirationTime = messageExpirationTimer;
-        threadStore.updateThread(thread);
+        account.getThreadStore().updateThread(thread);
     }
 
     private void requestSyncGroups() throws IOException {
@@ -849,8 +710,28 @@ public class Manager implements Signal {
         }
     }
 
+    private void requestSyncBlocked() throws IOException {
+        SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED).build();
+        SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
+        try {
+            sendSyncMessage(message);
+        } catch (UntrustedIdentityException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void requestSyncConfiguration() throws IOException {
+        SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION).build();
+        SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
+        try {
+            sendSyncMessage(message);
+        } catch (UntrustedIdentityException e) {
+            e.printStackTrace();
+        }
+    }
+
     private byte[] getSelfUnidentifiedAccessKey() {
-        return UnidentifiedAccess.deriveAccessKeyFrom(profileKey);
+        return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
     }
 
     private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
@@ -878,12 +759,12 @@ public class Manager implements Signal {
 
     private void sendSyncMessage(SignalServiceSyncMessage message)
             throws IOException, UntrustedIdentityException {
-        SignalServiceMessageSender messageSender = new SignalServiceMessageSender(BaseConfig.serviceConfiguration, username, password,
-                deviceId, signalProtocolStore, BaseConfig.USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
+        SignalServiceMessageSender messageSender = new SignalServiceMessageSender(BaseConfig.serviceConfiguration, username, account.getPassword(),
+                account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
         try {
             messageSender.sendMessage(message, getAccessForSync());
         } catch (UntrustedIdentityException e) {
-            signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+            account.getSignalProtocolStore().saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
             throw e;
         }
     }
@@ -920,8 +801,8 @@ public class Manager implements Signal {
 
         SignalServiceDataMessage message = null;
         try {
-            SignalServiceMessageSender messageSender = new SignalServiceMessageSender(BaseConfig.serviceConfiguration, username, password,
-                    deviceId, signalProtocolStore, BaseConfig.USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
+            SignalServiceMessageSender messageSender = new SignalServiceMessageSender(BaseConfig.serviceConfiguration, username, account.getPassword(),
+                    account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
 
             message = messageBuilder.build();
             if (message.getGroupInfo().isPresent()) {
@@ -929,19 +810,19 @@ public class Manager implements Signal {
                     List<SendMessageResult> result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), message);
                     for (SendMessageResult r : result) {
                         if (r.getIdentityFailure() != null) {
-                            signalProtocolStore.saveIdentity(r.getAddress().getNumber(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED);
+                            account.getSignalProtocolStore().saveIdentity(r.getAddress().getNumber(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED);
                         }
                     }
                     return result;
                 } catch (UntrustedIdentityException e) {
-                    signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+                    account.getSignalProtocolStore().saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
                     return Collections.emptyList();
                 }
             } else {
                 // Send to all individually, so sync messages are sent correctly
                 List<SendMessageResult> results = new ArrayList<>(recipientsTS.size());
                 for (SignalServiceAddress address : recipientsTS) {
-                    ThreadInfo thread = threadStore.getThread(address.getNumber());
+                    ThreadInfo thread = account.getThreadStore().getThread(address.getNumber());
                     if (thread != null) {
                         messageBuilder.withExpiration(thread.messageExpirationTime);
                     } else {
@@ -952,7 +833,7 @@ public class Manager implements Signal {
                         SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message);
                         results.add(result);
                     } catch (UntrustedIdentityException e) {
-                        signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+                        account.getSignalProtocolStore().saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
                         results.add(SendMessageResult.identityFailure(address, e.getIdentityKey()));
                     }
                 }
@@ -964,7 +845,7 @@ public class Manager implements Signal {
                     handleEndSession(recipient.getNumber());
                 }
             }
-            save();
+            account.save();
         }
     }
 
@@ -976,39 +857,26 @@ public class Manager implements Signal {
             } catch (InvalidNumberException e) {
                 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
                 System.err.println("Aborting sending.");
-                save();
+                account.save();
                 return null;
             }
         }
         return recipientsTS;
     }
 
-    private static CertificateValidator getCertificateValidator() {
-        try {
-            ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
-            return new CertificateValidator(unidentifiedSenderTrustRoot);
-        } catch (InvalidKeyException | IOException e) {
-            throw new AssertionError(e);
-        }
-    }
-
     private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException {
-        SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore, getCertificateValidator());
+        SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), account.getSignalProtocolStore(), getCertificateValidator());
         try {
             return cipher.decrypt(envelope);
         } catch (ProtocolUntrustedIdentityException e) {
             // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else
-//            signalProtocolStore.saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
+//            account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
             throw e;
         }
     }
 
     private void handleEndSession(String source) {
-        signalProtocolStore.deleteAllSessions(source);
-    }
-
-    public interface ReceiveMessageHandler {
-        void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);
+        account.getSignalProtocolStore().deleteAllSessions(source);
     }
 
     private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination, boolean ignoreAttachments) {
@@ -1016,7 +884,7 @@ public class Manager implements Signal {
         if (message.getGroupInfo().isPresent()) {
             SignalServiceGroup groupInfo = message.getGroupInfo().get();
             threadId = Base64.encodeBytes(groupInfo.getGroupId());
-            GroupInfo group = groupStore.getGroup(groupInfo.getGroupId());
+            GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
             switch (groupInfo.getType()) {
                 case UPDATE:
                     if (group == null) {
@@ -1042,7 +910,7 @@ public class Manager implements Signal {
                         group.members.addAll(groupInfo.getMembers().get());
                     }
 
-                    groupStore.updateGroup(group);
+                    account.getGroupStore().updateGroup(group);
                     break;
                 case DELIVER:
                     if (group == null) {
@@ -1062,7 +930,7 @@ public class Manager implements Signal {
                         }
                     } else {
                         group.members.remove(source);
-                        groupStore.updateGroup(group);
+                        account.getGroupStore().updateGroup(group);
                     }
                     break;
                 case REQUEST_INFO:
@@ -1088,14 +956,14 @@ public class Manager implements Signal {
             handleEndSession(isSync ? destination : source);
         }
         if (message.isExpirationUpdate() || message.getBody().isPresent()) {
-            ThreadInfo thread = threadStore.getThread(threadId);
+            ThreadInfo thread = account.getThreadStore().getThread(threadId);
             if (thread == null) {
                 thread = new ThreadInfo();
                 thread.id = threadId;
             }
             if (thread.messageExpirationTime != message.getExpiresInSeconds()) {
                 thread.messageExpirationTime = message.getExpiresInSeconds();
-                threadStore.updateThread(thread);
+                account.getThreadStore().updateThread(thread);
             }
         }
         if (message.getAttachments().isPresent() && !ignoreAttachments) {
@@ -1110,7 +978,7 @@ public class Manager implements Signal {
             }
         }
         if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
-            ContactInfo contact = contactStore.getContact(source);
+            ContactInfo contact = account.getContactStore().getContact(source);
             if (contact == null) {
                 contact = new ContactInfo();
                 contact.number = source;
@@ -1152,7 +1020,7 @@ public class Manager implements Signal {
                     }
                     handleMessage(envelope, content, ignoreAttachments);
                 }
-                save();
+                account.save();
                 handler.handleMessage(envelope, content, null);
                 try {
                     Files.delete(fileEntry.toPath());
@@ -1167,7 +1035,7 @@ public class Manager implements Signal {
 
     public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException {
         retryFailedReceivedMessages(handler, ignoreAttachments);
-        final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, password, deviceId, signalingKey, BaseConfig.USER_AGENT, null, timer);
+        final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer);
 
         try {
             if (messagePipe == null) {
@@ -1208,9 +1076,9 @@ public class Manager implements Signal {
                     }
                     handleMessage(envelope, content, ignoreAttachments);
                 }
-                save();
+                account.save();
                 handler.handleMessage(envelope, content, exception);
-                if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
+                if (!(exception instanceof ProtocolUntrustedIdentityException)) {
                     File cacheFile = null;
                     try {
                         cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
@@ -1237,7 +1105,7 @@ public class Manager implements Signal {
                 handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments);
             }
             if (content.getSyncMessage().isPresent()) {
-                isMultiDevice = true;
+                account.setMultiDevice(true);
                 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
                 if (syncMessage.getSent().isPresent()) {
                     SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
@@ -1259,6 +1127,7 @@ public class Manager implements Signal {
                             e.printStackTrace();
                         }
                     }
+                    // TODO Handle rm.isBlockedListRequest(); rm.isConfigurationRequest();
                 }
                 if (syncMessage.getGroups().isPresent()) {
                     File tmpFile = null;
@@ -1268,7 +1137,7 @@ public class Manager implements Signal {
                             DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream);
                             DeviceGroup g;
                             while ((g = s.read()) != null) {
-                                GroupInfo syncGroup = groupStore.getGroup(g.getId());
+                                GroupInfo syncGroup = account.getGroupStore().getGroup(g.getId());
                                 if (syncGroup == null) {
                                     syncGroup = new GroupInfo(g.getId());
                                 }
@@ -1284,7 +1153,7 @@ public class Manager implements Signal {
                                 if (g.getAvatar().isPresent()) {
                                     retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
                                 }
-                                groupStore.updateGroup(syncGroup);
+                                account.getGroupStore().updateGroup(syncGroup);
                             }
                         }
                     } catch (Exception e) {
@@ -1298,9 +1167,9 @@ public class Manager implements Signal {
                             }
                         }
                     }
-                    if (syncMessage.getBlockedList().isPresent()) {
-                        // TODO store list of blocked numbers
-                    }
+                }
+                if (syncMessage.getBlockedList().isPresent()) {
+                    // TODO store list of blocked numbers
                 }
                 if (syncMessage.getContacts().isPresent()) {
                     File tmpFile = null;
@@ -1310,11 +1179,14 @@ public class Manager implements Signal {
                         try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)) {
                             DeviceContactsInputStream s = new DeviceContactsInputStream(attachmentAsStream);
                             if (contactsMessage.isComplete()) {
-                                contactStore.clear();
+                                account.getContactStore().clear();
                             }
                             DeviceContact c;
                             while ((c = s.read()) != null) {
-                                ContactInfo contact = contactStore.getContact(c.getNumber());
+                                if (c.getNumber().equals(account.getUsername()) && c.getProfileKey().isPresent()) {
+                                    account.setProfileKey(c.getProfileKey().get());
+                                }
+                                ContactInfo contact = account.getContactStore().getContact(c.getNumber());
                                 if (contact == null) {
                                     contact = new ContactInfo();
                                     contact.number = c.getNumber();
@@ -1330,17 +1202,17 @@ public class Manager implements Signal {
                                 }
                                 if (c.getVerified().isPresent()) {
                                     final VerifiedMessage verifiedMessage = c.getVerified().get();
-                                    signalProtocolStore.saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
+                                    account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
                                 }
                                 if (c.getExpirationTimer().isPresent()) {
-                                    ThreadInfo thread = threadStore.getThread(c.getNumber());
+                                    ThreadInfo thread = account.getThreadStore().getThread(c.getNumber());
                                     thread.messageExpirationTime = c.getExpirationTimer().get();
-                                    threadStore.updateThread(thread);
+                                    account.getThreadStore().updateThread(thread);
                                 }
                                 if (c.isBlocked()) {
                                     // TODO store list of blocked numbers
                                 }
-                                contactStore.updateContact(contact);
+                                account.getContactStore().updateContact(contact);
 
                                 if (c.getAvatar().isPresent()) {
                                     retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
@@ -1361,7 +1233,10 @@ public class Manager implements Signal {
                 }
                 if (syncMessage.getVerified().isPresent()) {
                     final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
-                    signalProtocolStore.saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
+                    account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
+                }
+                if (syncMessage.getConfiguration().isPresent()) {
+                    // TODO
                 }
             }
         }
@@ -1502,7 +1377,7 @@ public class Manager implements Signal {
             }
         }
 
-        final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, password, deviceId, signalingKey, BaseConfig.USER_AGENT, null, timer);
+        final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer);
 
         File tmpFile = IOUtils.createTempFile();
         try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE)) {
@@ -1528,7 +1403,7 @@ public class Manager implements Signal {
     }
 
     private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException {
-        final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, password, deviceId, signalingKey, BaseConfig.USER_AGENT, null, timer);
+        final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer);
         return messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE);
     }
 
@@ -1553,8 +1428,8 @@ public class Manager implements Signal {
         try {
             try (OutputStream fos = new FileOutputStream(groupsFile)) {
                 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
-                for (GroupInfo record : groupStore.getGroups()) {
-                    ThreadInfo info = threadStore.getThread(Base64.encodeBytes(record.groupId));
+                for (GroupInfo record : account.getGroupStore().getGroups()) {
+                    ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId));
                     out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
                             new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
                             record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null),
@@ -1588,9 +1463,9 @@ public class Manager implements Signal {
         try {
             try (OutputStream fos = new FileOutputStream(contactsFile)) {
                 DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos);
-                for (ContactInfo record : contactStore.getContacts()) {
+                for (ContactInfo record : account.getContactStore().getContacts()) {
                     VerifiedMessage verifiedMessage = null;
-                    ThreadInfo info = threadStore.getThread(record.number);
+                    ThreadInfo info = account.getThreadStore().getThread(record.number);
                     if (getIdentities().containsKey(record.number)) {
                         JsonIdentityKeyStore.Identity currentIdentity = null;
                         for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) {
@@ -1610,6 +1485,15 @@ public class Manager implements Signal {
                             createContactAvatarAttachment(record.number), Optional.fromNullable(record.color),
                             Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), blocked, Optional.fromNullable(info != null ? info.messageExpirationTime : null)));
                 }
+
+                if (account.getProfileKey() != null) {
+                    // Send our own profile key as well
+                    out.write(new DeviceContact(account.getUsername(),
+                            Optional.<String>absent(), Optional.<SignalServiceAttachmentStream>absent(),
+                            Optional.<String>absent(), Optional.<VerifiedMessage>absent(),
+                            Optional.of(account.getProfileKey()),
+                            false, Optional.<Integer>absent()));
+                }
             }
 
             if (contactsFile.exists() && contactsFile.length() > 0) {
@@ -1638,19 +1522,19 @@ public class Manager implements Signal {
     }
 
     public ContactInfo getContact(String number) {
-        return contactStore.getContact(number);
+        return account.getContactStore().getContact(number);
     }
 
     public GroupInfo getGroup(byte[] groupId) {
-        return groupStore.getGroup(groupId);
+        return account.getGroupStore().getGroup(groupId);
     }
 
     public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
-        return signalProtocolStore.getIdentities();
+        return account.getSignalProtocolStore().getIdentities();
     }
 
     public List<JsonIdentityKeyStore.Identity> getIdentities(String number) {
-        return signalProtocolStore.getIdentities(number);
+        return account.getSignalProtocolStore().getIdentities(number);
     }
 
     /**
@@ -1660,7 +1544,7 @@ public class Manager implements Signal {
      * @param fingerprint Fingerprint
      */
     public boolean trustIdentityVerified(String name, byte[] fingerprint) {
-        List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
+        List<JsonIdentityKeyStore.Identity> ids = account.getSignalProtocolStore().getIdentities(name);
         if (ids == null) {
             return false;
         }
@@ -1669,13 +1553,13 @@ public class Manager implements Signal {
                 continue;
             }
 
-            signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+            account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
             try {
                 sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
             } catch (IOException | UntrustedIdentityException e) {
                 e.printStackTrace();
             }
-            save();
+            account.save();
             return true;
         }
         return false;
@@ -1688,7 +1572,7 @@ public class Manager implements Signal {
      * @param safetyNumber Safety number
      */
     public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) {
-        List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
+        List<JsonIdentityKeyStore.Identity> ids = account.getSignalProtocolStore().getIdentities(name);
         if (ids == null) {
             return false;
         }
@@ -1697,13 +1581,13 @@ public class Manager implements Signal {
                 continue;
             }
 
-            signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+            account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
             try {
                 sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
             } catch (IOException | UntrustedIdentityException e) {
                 e.printStackTrace();
             }
-            save();
+            account.save();
             return true;
         }
         return false;
@@ -1715,13 +1599,13 @@ public class Manager implements Signal {
      * @param name username of the identity
      */
     public boolean trustIdentityAllKeys(String name) {
-        List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
+        List<JsonIdentityKeyStore.Identity> ids = account.getSignalProtocolStore().getIdentities(name);
         if (ids == null) {
             return false;
         }
         for (JsonIdentityKeyStore.Identity id : ids) {
             if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
-                signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
+                account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
                 try {
                     sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
                 } catch (IOException | UntrustedIdentityException e) {
@@ -1729,7 +1613,7 @@ public class Manager implements Signal {
                 }
             }
         }
-        save();
+        account.save();
         return true;
     }
 
@@ -1737,4 +1621,9 @@ public class Manager implements Signal {
         Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey);
         return fingerprint.getDisplayableFingerprint().getDisplayText();
     }
+
+    public interface ReceiveMessageHandler {
+
+        void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);
+    }
 }
diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java
new file mode 100644 (file)
index 0000000..cdc3efa
--- /dev/null
@@ -0,0 +1,321 @@
+package org.asamk.signal.storage;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.asamk.signal.storage.contacts.JsonContactsStore;
+import org.asamk.signal.storage.groups.JsonGroupStore;
+import org.asamk.signal.storage.protocol.JsonSignalProtocolStore;
+import org.asamk.signal.storage.threads.JsonThreadStore;
+import org.asamk.signal.util.IOUtils;
+import org.asamk.signal.util.Util;
+import org.whispersystems.libsignal.IdentityKeyPair;
+import org.whispersystems.libsignal.state.PreKeyRecord;
+import org.whispersystems.libsignal.state.SignedPreKeyRecord;
+import org.whispersystems.libsignal.util.Medium;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.internal.util.Base64;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.util.Collection;
+
+public class SignalAccount {
+
+    private final ObjectMapper jsonProcessor = new ObjectMapper();
+    private FileChannel fileChannel;
+    private FileLock lock;
+    private String username;
+    private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
+    private boolean isMultiDevice = false;
+    private String password;
+    private String registrationLockPin;
+    private String signalingKey;
+    private byte[] profileKey;
+    private int preKeyIdOffset;
+    private int nextSignedPreKeyId;
+
+    private boolean registered = false;
+
+    private JsonSignalProtocolStore signalProtocolStore;
+    private JsonGroupStore groupStore;
+    private JsonContactsStore contactStore;
+    private JsonThreadStore threadStore;
+
+    private SignalAccount() {
+        jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
+        jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
+        jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
+        jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+        jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
+        jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
+    }
+
+    public static SignalAccount load(String dataPath, String username) throws IOException {
+        SignalAccount account = new SignalAccount();
+        IOUtils.createPrivateDirectories(dataPath);
+        account.openFileChannel(getFileName(dataPath, username));
+        account.load();
+        return account;
+    }
+
+    public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, byte[] profileKey) throws IOException {
+        IOUtils.createPrivateDirectories(dataPath);
+
+        SignalAccount account = new SignalAccount();
+        account.openFileChannel(getFileName(dataPath, username));
+
+        account.username = username;
+        account.profileKey = profileKey;
+        account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
+        account.groupStore = new JsonGroupStore();
+        account.threadStore = new JsonThreadStore();
+        account.contactStore = new JsonContactsStore();
+        account.registered = false;
+
+        return account;
+    }
+
+    public static SignalAccount createLinkedAccount(String dataPath, String username, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, byte[] profileKey) throws IOException {
+        IOUtils.createPrivateDirectories(dataPath);
+
+        SignalAccount account = new SignalAccount();
+        account.openFileChannel(getFileName(dataPath, username));
+
+        account.username = username;
+        account.password = password;
+        account.profileKey = profileKey;
+        account.deviceId = deviceId;
+        account.signalingKey = signalingKey;
+        account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
+        account.groupStore = new JsonGroupStore();
+        account.threadStore = new JsonThreadStore();
+        account.contactStore = new JsonContactsStore();
+        account.registered = true;
+        account.isMultiDevice = true;
+
+        return account;
+    }
+
+    public static SignalAccount createTemporaryAccount(IdentityKeyPair identityKey, int registrationId) {
+        SignalAccount account = new SignalAccount();
+
+        account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
+        account.registered = false;
+
+        return account;
+    }
+
+    public static String getFileName(String dataPath, String username) {
+        return dataPath + "/" + username;
+    }
+
+    public static boolean userExists(String dataPath, String username) {
+        if (username == null) {
+            return false;
+        }
+        File f = new File(getFileName(dataPath, username));
+        return !(!f.exists() || f.isDirectory());
+    }
+
+    private void load() throws IOException {
+        JsonNode rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
+
+        JsonNode node = rootNode.get("deviceId");
+        if (node != null) {
+            deviceId = node.asInt();
+        }
+        username = Util.getNotNullNode(rootNode, "username").asText();
+        password = Util.getNotNullNode(rootNode, "password").asText();
+        JsonNode pinNode = rootNode.get("registrationLockPin");
+        registrationLockPin = pinNode == null ? null : pinNode.asText();
+        if (rootNode.has("signalingKey")) {
+            signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText();
+        }
+        if (rootNode.has("preKeyIdOffset")) {
+            preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
+        } else {
+            preKeyIdOffset = 0;
+        }
+        if (rootNode.has("nextSignedPreKeyId")) {
+            nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
+        } else {
+            nextSignedPreKeyId = 0;
+        }
+        if (rootNode.has("profileKey")) {
+            profileKey = Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText());
+        }
+
+        signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
+        registered = Util.getNotNullNode(rootNode, "registered").asBoolean();
+        JsonNode groupStoreNode = rootNode.get("groupStore");
+        if (groupStoreNode != null) {
+            groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
+        }
+        if (groupStore == null) {
+            groupStore = new JsonGroupStore();
+        }
+
+        JsonNode contactStoreNode = rootNode.get("contactStore");
+        if (contactStoreNode != null) {
+            contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
+        }
+        if (contactStore == null) {
+            contactStore = new JsonContactsStore();
+        }
+        JsonNode threadStoreNode = rootNode.get("threadStore");
+        if (threadStoreNode != null) {
+            threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class);
+        }
+        if (threadStore == null) {
+            threadStore = new JsonThreadStore();
+        }
+    }
+
+    public void save() {
+        if (fileChannel == null) {
+            return;
+        }
+        ObjectNode rootNode = jsonProcessor.createObjectNode();
+        rootNode.put("username", username)
+                .put("deviceId", deviceId)
+                .put("password", password)
+                .put("registrationLockPin", registrationLockPin)
+                .put("signalingKey", signalingKey)
+                .put("preKeyIdOffset", preKeyIdOffset)
+                .put("nextSignedPreKeyId", nextSignedPreKeyId)
+                .put("registered", registered)
+                .putPOJO("axolotlStore", signalProtocolStore)
+                .putPOJO("groupStore", groupStore)
+                .putPOJO("contactStore", contactStore)
+                .putPOJO("threadStore", threadStore)
+        ;
+        try {
+            fileChannel.position(0);
+            jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode);
+            fileChannel.truncate(fileChannel.position());
+            fileChannel.force(false);
+        } catch (Exception e) {
+            System.err.println(String.format("Error saving file: %s", e.getMessage()));
+        }
+    }
+
+    private void openFileChannel(String fileName) throws IOException {
+        if (fileChannel != null) {
+            return;
+        }
+
+        if (!new File(fileName).exists()) {
+            IOUtils.createPrivateFile(fileName);
+        }
+        fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
+        lock = fileChannel.tryLock();
+        if (lock == null) {
+            System.err.println("Config file is in use by another instance, waiting…");
+            lock = fileChannel.lock();
+            System.err.println("Config file lock acquired.");
+        }
+    }
+
+    public void addPreKeys(Collection<PreKeyRecord> records) {
+        for (PreKeyRecord record : records) {
+            signalProtocolStore.storePreKey(record.getId(), record);
+        }
+        preKeyIdOffset = (preKeyIdOffset + records.size()) % Medium.MAX_VALUE;
+    }
+
+    public void addSignedPreKey(SignedPreKeyRecord record) {
+        signalProtocolStore.storeSignedPreKey(record.getId(), record);
+        nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
+    }
+
+    public JsonSignalProtocolStore getSignalProtocolStore() {
+        return signalProtocolStore;
+    }
+
+    public JsonGroupStore getGroupStore() {
+        return groupStore;
+    }
+
+    public JsonContactsStore getContactStore() {
+        return contactStore;
+    }
+
+    public JsonThreadStore getThreadStore() {
+        return threadStore;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public int getDeviceId() {
+        return deviceId;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(final String password) {
+        this.password = password;
+    }
+
+    public String getRegistrationLockPin() {
+        return registrationLockPin;
+    }
+
+    public void setRegistrationLockPin(final String registrationLockPin) {
+        this.registrationLockPin = registrationLockPin;
+    }
+
+    public String getSignalingKey() {
+        return signalingKey;
+    }
+
+    public void setSignalingKey(final String signalingKey) {
+        this.signalingKey = signalingKey;
+    }
+
+    public byte[] getProfileKey() {
+        return profileKey;
+    }
+
+    public void setProfileKey(final byte[] profileKey) {
+        this.profileKey = profileKey;
+    }
+
+    public int getPreKeyIdOffset() {
+        return preKeyIdOffset;
+    }
+
+    public int getNextSignedPreKeyId() {
+        return nextSignedPreKeyId;
+    }
+
+    public boolean isRegistered() {
+        return registered;
+    }
+
+    public void setRegistered(final boolean registered) {
+        this.registered = registered;
+    }
+
+    public boolean isMultiDevice() {
+        return isMultiDevice;
+    }
+
+    public void setMultiDevice(final boolean multiDevice) {
+        isMultiDevice = multiDevice;
+    }
+}
index 03b076cc8b1561da5436c5fda76019afa175a144..1d754968a9d66c9f3e7c8b9712f23105c7c411fd 100644 (file)
@@ -3,6 +3,7 @@ package org.asamk.signal.storage.contacts;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 public class ContactInfo {
+
     @JsonProperty
     public String name;
 
index 2024b86d0206a69c4736387268e90b4adb997b74..45e35e99526d11b78fc700ddc1c1de3b031846be 100644 (file)
@@ -14,13 +14,13 @@ import java.util.List;
 import java.util.Map;
 
 public class JsonContactsStore {
+
+    private static final ObjectMapper jsonProcessor = new ObjectMapper();
     @JsonProperty("contacts")
     @JsonSerialize(using = JsonContactsStore.MapToListSerializer.class)
     @JsonDeserialize(using = ContactsDeserializer.class)
     private Map<String, ContactInfo> contacts = new HashMap<>();
 
-    private static final ObjectMapper jsonProcessor = new ObjectMapper();
-
     public void updateContact(ContactInfo contact) {
         contacts.put(contact.number, contact);
     }
@@ -41,6 +41,7 @@ public class JsonContactsStore {
     }
 
     public static class MapToListSerializer extends JsonSerializer<Map<?, ?>> {
+
         @Override
         public void serialize(final Map<?, ?> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
             jgen.writeObject(value.values());
@@ -48,6 +49,7 @@ public class JsonContactsStore {
     }
 
     public static class ContactsDeserializer extends JsonDeserializer<Map<String, ContactInfo>> {
+
         @Override
         public Map<String, ContactInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
             Map<String, ContactInfo> contacts = new HashMap<>();
index 96147fe3e76b529ca08bd0006c0c6ae40f81066c..f614c87cb4c65595c846b33e302defe069147ec8 100644 (file)
@@ -8,6 +8,7 @@ import java.util.HashSet;
 import java.util.Set;
 
 public class GroupInfo {
+
     @JsonProperty
     public final byte[] groupId;
 
@@ -16,20 +17,13 @@ public class GroupInfo {
 
     @JsonProperty
     public Set<String> members = new HashSet<>();
-
-    private long avatarId;
-
-    @JsonIgnore
-    public long getAvatarId() {
-        return avatarId;
-    }
-
     @JsonProperty
     public boolean active;
-
     @JsonProperty
     public String color;
 
+    private long avatarId;
+
     public GroupInfo(byte[] groupId) {
         this.groupId = groupId;
     }
@@ -41,4 +35,9 @@ public class GroupInfo {
         this.avatarId = avatarId;
         this.color = color;
     }
+
+    @JsonIgnore
+    public long getAvatarId() {
+        return avatarId;
+    }
 }
index 1cc9f15134fee08fa76c5c194749083bd35a439c..e8e9730900fc19273b6e8ba92f741deec864e734 100644 (file)
@@ -15,15 +15,16 @@ import java.util.List;
 import java.util.Map;
 
 public class JsonGroupStore {
+
+    private static final ObjectMapper jsonProcessor = new ObjectMapper();
+
+    public static List<GroupInfo> groupsWithLegacyAvatarId = new ArrayList<>();
+
     @JsonProperty("groups")
     @JsonSerialize(using = JsonGroupStore.MapToListSerializer.class)
     @JsonDeserialize(using = JsonGroupStore.GroupsDeserializer.class)
     private Map<String, GroupInfo> groups = new HashMap<>();
 
-    public static List<GroupInfo> groupsWithLegacyAvatarId = new ArrayList<>();
-
-    private static final ObjectMapper jsonProcessor = new ObjectMapper();
-
     public void updateGroup(GroupInfo group) {
         groups.put(Base64.encodeBytes(group.groupId), group);
     }
@@ -38,6 +39,7 @@ public class JsonGroupStore {
     }
 
     public static class MapToListSerializer extends JsonSerializer<Map<?, ?>> {
+
         @Override
         public void serialize(final Map<?, ?> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
             jgen.writeObject(value.values());
@@ -45,6 +47,7 @@ public class JsonGroupStore {
     }
 
     public static class GroupsDeserializer extends JsonDeserializer<Map<String, GroupInfo>> {
+
         @Override
         public Map<String, GroupInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
             Map<String, GroupInfo> groups = new HashMap<>();
index 922a88f59d65aec654b915c83c44ac7ef3530d5a..e086b929e37a92b1c6dc0bde5893064bf3729566 100644 (file)
@@ -22,7 +22,6 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
     private final IdentityKeyPair identityKeyPair;
     private final int localRegistrationId;
 
-
     public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) {
         this.identityKeyPair = identityKeyPair;
         this.localRegistrationId = localRegistrationId;
@@ -131,7 +130,6 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
                 int localRegistrationId = node.get("registrationId").asInt();
                 IdentityKeyPair identityKeyPair = new IdentityKeyPair(Base64.decode(node.get("identityKey").asText()));
 
-
                 JsonIdentityKeyStore keyStore = new JsonIdentityKeyStore(identityKeyPair, localRegistrationId);
 
                 JsonNode trustedKeysNode = node.get("trustedKeys");
@@ -180,6 +178,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
     }
 
     public class Identity {
+
         IdentityKey identityKey;
         TrustLevel trustLevel;
         Date added;
index d6bc02fad0056abe8403d9b11c94fbf3043ed423..184c084bd5917d328a6d1bbd7adfa783a4f78ecb 100644 (file)
@@ -17,7 +17,6 @@ class JsonPreKeyStore implements PreKeyStore {
 
     private final Map<Integer, byte[]> store = new HashMap<>();
 
-
     public JsonPreKeyStore() {
 
     }
@@ -60,7 +59,6 @@ class JsonPreKeyStore implements PreKeyStore {
         public JsonPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
             JsonNode node = jsonParser.getCodec().readTree(jsonParser);
 
-
             Map<Integer, byte[]> preKeyMap = new HashMap<>();
             if (node.isArray()) {
                 for (JsonNode preKey : node) {
index 1ae2e5dfaca8d450207f7c004bd70239ec253e3f..bf6891c893b6c87d5e4d91520350b45f27820a06 100644 (file)
@@ -24,7 +24,6 @@ class JsonSessionStore implements SessionStore {
         this.sessions.putAll(sessions);
     }
 
-
     @Override
     public synchronized SessionRecord loadSession(SignalProtocolAddress remoteAddress) {
         try {
index d7d2a2653c3b35bfe726173716c18cbab593d81f..a8c400ce75b237038a85f003f2b0548a559f8eff 100644 (file)
@@ -23,7 +23,6 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore {
 
     }
 
-
     public void addSignedPreKeys(Map<Integer, byte[]> preKeys) {
         store.putAll(preKeys);
     }
@@ -77,7 +76,6 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore {
         public JsonSignedPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
             JsonNode node = jsonParser.getCodec().readTree(jsonParser);
 
-
             Map<Integer, byte[]> preKeyMap = new HashMap<>();
             if (node.isArray()) {
                 for (JsonNode preKey : node) {
index b32d629d1740f2bce529ec99e3bea47418af6eaf..a9ce6fb651d0cc5400b29bce96dd64bd7331f40c 100644 (file)
@@ -14,13 +14,14 @@ import java.util.List;
 import java.util.Map;
 
 public class JsonThreadStore {
+
+    private static final ObjectMapper jsonProcessor = new ObjectMapper();
+
     @JsonProperty("threads")
     @JsonSerialize(using = JsonThreadStore.MapToListSerializer.class)
     @JsonDeserialize(using = ThreadsDeserializer.class)
     private Map<String, ThreadInfo> threads = new HashMap<>();
 
-    private static final ObjectMapper jsonProcessor = new ObjectMapper();
-
     public void updateThread(ThreadInfo thread) {
         threads.put(thread.id, thread);
     }
@@ -34,6 +35,7 @@ public class JsonThreadStore {
     }
 
     public static class MapToListSerializer extends JsonSerializer<Map<?, ?>> {
+
         @Override
         public void serialize(final Map<?, ?> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
             jgen.writeObject(value.values());
@@ -41,6 +43,7 @@ public class JsonThreadStore {
     }
 
     public static class ThreadsDeserializer extends JsonDeserializer<Map<String, ThreadInfo>> {
+
         @Override
         public Map<String, ThreadInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
             Map<String, ThreadInfo> threads = new HashMap<>();
index 3fc28405276ec5d2b3fa14d8901968e8a9d7f4ac..67e6b4747a0a69942f2481ab726fcc0145cbedeb 100644 (file)
@@ -3,6 +3,7 @@ package org.asamk.signal.storage.threads;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 public class ThreadInfo {
+
     @JsonProperty
     public String id;
 
index 69128d015534eeabb29b33704b9ee7029f326581..9b8c3b5b3291b8f301782fa67744f7c0ce472efa 100644 (file)
@@ -33,13 +33,18 @@ public class IOUtils {
         return output.toString();
     }
 
-    public static void createPrivateDirectories(String path) throws IOException {
-        final Path file = new File(path).toPath();
+    public static void createPrivateDirectories(String directoryPath) throws IOException {
+        final File file = new File(directoryPath);
+        if (file.exists()) {
+            return;
+        }
+
+        final Path path = file.toPath();
         try {
             Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE);
-            Files.createDirectories(file, PosixFilePermissions.asFileAttribute(perms));
+            Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms));
         } catch (UnsupportedOperationException e) {
-            Files.createDirectories(file);
+            Files.createDirectories(path);
         }
     }
 
index eec7d2f782fdb95846400fdc603fdbc310520cac..93a595d1e73865e1d59b43e36e088437f0708028 100644 (file)
@@ -1,5 +1,8 @@
 package org.asamk.signal.util;
 
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.io.InvalidObjectException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.util.HashMap;
@@ -53,4 +56,13 @@ public class Util {
 
         return buf.toString();
     }
+
+    public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
+        JsonNode node = parent.get(name);
+        if (node == null) {
+            throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
+        }
+
+        return node;
+    }
 }