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;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Collection;
+import java.util.UUID;
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;
private JsonSignalProtocolStore signalProtocolStore;
private JsonGroupStore groupStore;
private JsonContactsStore contactStore;
- private JsonThreadStore threadStore;
private SignalAccount() {
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
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();
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;
}
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();
+ 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");
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);
}
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()) {
+ if (thread.id == null || thread.id.isEmpty()) {
+ continue;
+ }
+ 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) {
+ }
+ }
}
}
}
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("signalingKey", signalingKey)
.put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId)
- .put("profileKey", Base64.encodeBytes(profileKey))
+ .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()));
}
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;
}
return registrationLockPin;
}
+ public String getRegistrationLock() {
+ return null; // TODO implement KBS
+ }
+
public void setRegistrationLockPin(final String registrationLockPin) {
this.registrationLockPin = registrationLockPin;
}
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;
}