*/
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;
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;
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;
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() {
}
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() {
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
}
}
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 {
}
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;
}
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;
}
}
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;
}
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) {
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 {
}
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);
}
}
public List<GroupInfo> getGroups() {
- return groupStore.getGroups();
+ return account.getGroupStore().getGroups();
}
@Override
.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);
}
final GroupInfo g = getGroupForSending(groupId);
g.members.remove(this.username);
- groupStore.updateGroup(g);
+ account.getGroupStore().updateGroup(g);
sendMessageLegacy(messageBuilder, g.members);
}
Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
- groupStore.updateGroup(g);
+ account.getGroupStore().updateGroup(g);
SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
@Override
public String getContactName(String number) {
- ContactInfo contact = contactStore.getContact(number);
+ ContactInfo contact = account.getContactStore().getContact(number);
if (contact == null) {
return "";
} else {
@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;
System.err.println("Updating contact " + number + " name " + contact.name + " -> " + name);
}
contact.name = name;
- contactStore.updateContact(contact);
- save();
+ account.getContactStore().updateContact(contact);
+ account.save();
}
@Override
return sendUpdateGroupMessage(groupId, name, members, avatar);
}
-
/**
* Change the expiration timer for a thread (number of groupId)
*
* @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 {
}
}
+ 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) {
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;
}
}
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()) {
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 {
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()));
}
}
handleEndSession(recipient.getNumber());
}
}
- save();
+ account.save();
}
}
} 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) {
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) {
group.members.addAll(groupInfo.getMembers().get());
}
- groupStore.updateGroup(group);
+ account.getGroupStore().updateGroup(group);
break;
case DELIVER:
if (group == null) {
}
} else {
group.members.remove(source);
- groupStore.updateGroup(group);
+ account.getGroupStore().updateGroup(group);
}
break;
case REQUEST_INFO:
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) {
}
}
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;
}
handleMessage(envelope, content, ignoreAttachments);
}
- save();
+ account.save();
handler.handleMessage(envelope, content, null);
try {
Files.delete(fileEntry.toPath());
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) {
}
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());
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();
e.printStackTrace();
}
}
+ // TODO Handle rm.isBlockedListRequest(); rm.isConfigurationRequest();
}
if (syncMessage.getGroups().isPresent()) {
File tmpFile = null;
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());
}
if (g.getAvatar().isPresent()) {
retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
}
- groupStore.updateGroup(syncGroup);
+ account.getGroupStore().updateGroup(syncGroup);
}
}
} catch (Exception e) {
}
}
}
- 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;
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();
}
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);
}
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
}
}
}
}
}
- 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)) {
}
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);
}
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),
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)) {
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) {
}
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);
}
/**
* @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;
}
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;
* @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;
}
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;
* @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) {
}
}
}
- save();
+ account.save();
return true;
}
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);
+ }
}
--- /dev/null
+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;
+ }
+}