]> nmode's Git Repositories - signal-cli/blobdiff - src/main/java/org/asamk/signal/storage/SignalAccount.java
Set uuid after verify and linking and request it at startup for existing clients
[signal-cli] / src / main / java / org / asamk / signal / storage / SignalAccount.java
index cdc3efa5cf667bf3d0a84622b4e6d662c156f959..ebf7a8463e0283b1feafab484aab45797f3cb5a2 100644 (file)
@@ -9,18 +9,24 @@ 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.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.JsonSignalProtocolStore;
-import org.asamk.signal.storage.threads.JsonThreadStore;
+import org.asamk.signal.storage.threads.LegacyJsonThreadStore;
+import org.asamk.signal.storage.threads.ThreadInfo;
 import org.asamk.signal.util.IOUtils;
 import org.asamk.signal.util.Util;
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.profiles.ProfileKey;
 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 org.whispersystems.util.Base64;
 
 import java.io.File;
 import java.io.IOException;
@@ -29,6 +35,7 @@ import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.util.Collection;
+import java.util.UUID;
 
 public class SignalAccount {
 
@@ -36,12 +43,13 @@ public class SignalAccount {
     private FileChannel fileChannel;
     private FileLock lock;
     private String username;
+    private UUID uuid;
     private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
     private boolean isMultiDevice = false;
     private String password;
     private String registrationLockPin;
     private String signalingKey;
-    private byte[] profileKey;
+    private ProfileKey profileKey;
     private int preKeyIdOffset;
     private int nextSignedPreKeyId;
 
@@ -50,7 +58,6 @@ public class SignalAccount {
     private JsonSignalProtocolStore signalProtocolStore;
     private JsonGroupStore groupStore;
     private JsonContactsStore contactStore;
-    private JsonThreadStore threadStore;
 
     private SignalAccount() {
         jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
@@ -69,7 +76,7 @@ public class SignalAccount {
         return account;
     }
 
-    public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, byte[] profileKey) throws IOException {
+    public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey) throws IOException {
         IOUtils.createPrivateDirectories(dataPath);
 
         SignalAccount account = new SignalAccount();
@@ -79,27 +86,26 @@ public class SignalAccount {
         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 {
+    public static SignalAccount createLinkedAccount(String dataPath, String username, UUID uuid, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException {
         IOUtils.createPrivateDirectories(dataPath);
 
         SignalAccount account = new SignalAccount();
         account.openFileChannel(getFileName(dataPath, username));
 
         account.username = username;
+        account.uuid = uuid;
         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;
@@ -129,16 +135,31 @@ public class SignalAccount {
     }
 
     private void load() throws IOException {
-        JsonNode rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
+        JsonNode rootNode;
+        synchronized (fileChannel) {
+            fileChannel.position(0);
+            rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
+        }
 
+        JsonNode uuidNode = rootNode.get("uuid");
+        if (uuidNode != null && !uuidNode.isNull()) {
+            try {
+                uuid = UUID.fromString(uuidNode.asText());
+            } catch (IllegalArgumentException e) {
+                throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e);
+            }
+        }
         JsonNode node = rootNode.get("deviceId");
         if (node != null) {
             deviceId = node.asInt();
         }
+        if (rootNode.has("isMultiDevice")) {
+            isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
+        }
         username = Util.getNotNullNode(rootNode, "username").asText();
         password = Util.getNotNullNode(rootNode, "password").asText();
         JsonNode pinNode = rootNode.get("registrationLockPin");
-        registrationLockPin = pinNode == null ? null : pinNode.asText();
+        registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText();
         if (rootNode.has("signalingKey")) {
             signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText();
         }
@@ -153,7 +174,11 @@ public class SignalAccount {
             nextSignedPreKeyId = 0;
         }
         if (rootNode.has("profileKey")) {
-            profileKey = Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText());
+            try {
+                profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()));
+            } catch (InvalidInputException e) {
+                throw new IOException("Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", e);
+            }
         }
 
         signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
@@ -175,10 +200,24 @@ public class SignalAccount {
         }
         JsonNode threadStoreNode = rootNode.get("threadStore");
         if (threadStoreNode != null) {
-            threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class);
-        }
-        if (threadStore == null) {
-            threadStore = new JsonThreadStore();
+            LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
+            // Migrate thread info to group and contact store
+            for (ThreadInfo thread : threadStore.getThreads()) {
+                try {
+                    ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
+                    if (contactInfo != null) {
+                        contactInfo.messageExpirationTime = thread.messageExpirationTime;
+                        contactStore.updateContact(contactInfo);
+                    } else {
+                        GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id));
+                        if (groupInfo != null) {
+                            groupInfo.messageExpirationTime = thread.messageExpirationTime;
+                            groupStore.updateGroup(groupInfo);
+                        }
+                    }
+                } catch (Exception ignored) {
+                }
+            }
         }
     }
 
@@ -188,23 +227,27 @@ public class SignalAccount {
         }
         ObjectNode rootNode = jsonProcessor.createObjectNode();
         rootNode.put("username", username)
+                .put("uuid", uuid == null ? null : uuid.toString())
                 .put("deviceId", deviceId)
+                .put("isMultiDevice", isMultiDevice)
                 .put("password", password)
                 .put("registrationLockPin", registrationLockPin)
                 .put("signalingKey", signalingKey)
                 .put("preKeyIdOffset", preKeyIdOffset)
                 .put("nextSignedPreKeyId", nextSignedPreKeyId)
+                .put("profileKey", Base64.encodeBytes(profileKey.serialize()))
                 .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);
+            synchronized (fileChannel) {
+                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()));
         }
@@ -251,14 +294,22 @@ public class SignalAccount {
         return contactStore;
     }
 
-    public JsonThreadStore getThreadStore() {
-        return threadStore;
-    }
-
     public String getUsername() {
         return username;
     }
 
+    public UUID getUuid() {
+        return uuid;
+    }
+
+    public void setUuid(final UUID uuid) {
+        this.uuid = uuid;
+    }
+
+    public SignalServiceAddress getSelfAddress() {
+        return new SignalServiceAddress(uuid, username);
+    }
+
     public int getDeviceId() {
         return deviceId;
     }
@@ -275,6 +326,10 @@ public class SignalAccount {
         return registrationLockPin;
     }
 
+    public String getRegistrationLock() {
+        return null; // TODO implement KBS
+    }
+
     public void setRegistrationLockPin(final String registrationLockPin) {
         this.registrationLockPin = registrationLockPin;
     }
@@ -287,11 +342,11 @@ public class SignalAccount {
         this.signalingKey = signalingKey;
     }
 
-    public byte[] getProfileKey() {
+    public ProfileKey getProfileKey() {
         return profileKey;
     }
 
-    public void setProfileKey(final byte[] profileKey) {
+    public void setProfileKey(final ProfileKey profileKey) {
         this.profileKey = profileKey;
     }