This commit is contained in:
narodnik 2020-04-01 12:52:54 +02:00
commit edb184ba97
46 changed files with 1264 additions and 585 deletions

View file

@ -4,6 +4,28 @@
<JavaCodeStyleSettings>
<option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
<option name="JD_P_AT_EMPTY_LINES" value="false" />
</JavaCodeStyleSettings>
<XML>

View file

@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_1_8
mainClassName = 'org.asamk.signal.Main'
version = '0.6.5'
version = '0.6.6'
compileJava.options.encoding = 'UTF-8'
@ -20,7 +20,7 @@ repositories {
}
dependencies {
compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_3'
compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_7'
compile 'org.bouncycastle:bcprov-jdk15on:1.64'
compile 'net.sourceforge.argparse4j:argparse4j:0.8.1'
compile 'org.freedesktop.dbus:dbus-java:2.7.0'

Binary file not shown.

View file

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

@ -287,6 +287,34 @@ sendContacts
Send a synchronization message with the local contacts list to all linked devices.
This command should only be used if this is the master device.
uploadStickerPack
~~~~~~~~~~~~~~~~~
Upload a new sticker pack, consisting of a manifest file and the stickers in WebP
format (maximum size for a sticker file is 100KiB).
The required manifest.json has the following format:
```json
{
"title": "<STICKER_PACK_TITLE>",
"author": "<STICKER_PACK_AUTHOR>",
"cover": { // Optional cover, by default the first sticker is used as cover
"file": "<name of webp file, mandatory>",
"emoji": "<optional>"
},
"stickers": [
{
"file": "<name of webp file, mandatory>",
"emoji": "<optional>"
}
...
]
}
```
PATH::
The path of the manifest.json or a zip file containing the sticker pack you
wish to upload.
daemon
~~~~~~
signal-cli can run in daemon mode and provides an experimental dbus interface. For

View file

@ -13,11 +13,11 @@ 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, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException;
void sendMessage(String message, List<String> attachments, List<String> recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
void sendMessage(String message, List<String> attachments, List<String> recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException;
void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions;
void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException;
void sendGroupMessage(String message, List<String> attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException;
@ -35,7 +35,7 @@ public interface Signal extends DBusInterface {
List<String> getGroupMembers(byte[] groupId);
byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException;
byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException;
boolean isRegistered();

View file

@ -2,6 +2,7 @@ package org.asamk.signal;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import java.util.ArrayList;
import java.util.List;
@ -16,8 +17,9 @@ class JsonDataMessage {
JsonDataMessage(SignalServiceDataMessage dataMessage) {
this.timestamp = dataMessage.getTimestamp();
if (dataMessage.getGroupInfo().isPresent()) {
this.groupInfo = new JsonGroupInfo(dataMessage.getGroupInfo().get());
if (dataMessage.getGroupContext().isPresent() && dataMessage.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = dataMessage.getGroupContext().get().getGroupV1().get();
this.groupInfo = new JsonGroupInfo(groupInfo);
}
if (dataMessage.getBody().isPresent()) {
this.message = dataMessage.getBody().get();

View file

@ -47,14 +47,15 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
System.out.println(message.getBody().get());
if (!message.isEndSession() &&
!(message.getGroupInfo().isPresent() &&
message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) {
!(message.getGroupContext().isPresent() &&
message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) {
try {
conn.sendSignal(new Signal.MessageReceived(
objectPath,
message.getTimestamp(),
envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(),
message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0],
message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()
? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0],
message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m)));
} catch (DBusException e) {
@ -66,7 +67,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
if (sync_message.getSent().isPresent()) {
SentTranscriptMessage transcript = sync_message.getSent().get();
if (!envelope.isUnidentifiedSender() && envelope.hasSource() && (transcript.getDestination().isPresent() || transcript.getMessage().getGroupInfo().isPresent())) {
if (!envelope.isUnidentifiedSender() && envelope.hasSource() && (transcript.getDestination().isPresent() || transcript.getMessage().getGroupContext().isPresent())) {
SignalServiceDataMessage message = transcript.getMessage();
try {
@ -75,7 +76,8 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
transcript.getTimestamp(),
envelope.getSourceAddress().getNumber().get(),
transcript.getDestination().isPresent() ? transcript.getDestination().get().getNumber().get() : "",
message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0],
message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()
? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0],
message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m)));
} catch (DBusException e) {

View file

@ -0,0 +1,29 @@
package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class JsonStickerPack {
@JsonProperty
public String title;
@JsonProperty
public String author;
@JsonProperty
public JsonSticker cover;
@JsonProperty
public List<JsonSticker> stickers;
public static class JsonSticker {
@JsonProperty
public String emoji;
@JsonProperty
public String file;
}
}

View file

@ -4,14 +4,13 @@ import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList;
import java.util.List;
enum JsonSyncMessageType {
CONTACTS_SYNC,
GROUPS_SYNC,
REQUEST_SYNC
CONTACTS_SYNC,
GROUPS_SYNC,
REQUEST_SYNC
}
class JsonSyncMessage {
@ -22,25 +21,25 @@ class JsonSyncMessage {
JsonSyncMessageType type;
JsonSyncMessage(SignalServiceSyncMessage syncMessage) {
if (syncMessage.getSent().isPresent()) {
this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get());
}
if (syncMessage.getBlockedList().isPresent()) {
this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size());
for (SignalServiceAddress address : syncMessage.getBlockedList().get().getAddresses()) {
this.blockedNumbers.add(address.getNumber().get());
}
}
if (syncMessage.getRead().isPresent()) {
this.readMessages = syncMessage.getRead().get();
}
if (syncMessage.getSent().isPresent()) {
this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get());
}
if (syncMessage.getBlockedList().isPresent()) {
this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size());
for (SignalServiceAddress address : syncMessage.getBlockedList().get().getAddresses()) {
this.blockedNumbers.add(address.getNumber().get());
}
}
if (syncMessage.getRead().isPresent()) {
this.readMessages = syncMessage.getRead().get();
}
if (syncMessage.getContacts().isPresent()) {
this.type = JsonSyncMessageType.CONTACTS_SYNC;
} else if (syncMessage.getGroups().isPresent()) {
this.type = JsonSyncMessageType.GROUPS_SYNC;
} else if (syncMessage.getRequest().isPresent()) {
this.type = JsonSyncMessageType.REQUEST_SYNC;
}
}
if (syncMessage.getContacts().isPresent()) {
this.type = JsonSyncMessageType.CONTACTS_SYNC;
} else if (syncMessage.getGroups().isPresent()) {
this.type = JsonSyncMessageType.GROUPS_SYNC;
} else if (syncMessage.getRequest().isPresent()) {
this.type = JsonSyncMessageType.REQUEST_SYNC;
}
}
}

View file

@ -38,6 +38,7 @@ import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.File;
@ -105,6 +106,12 @@ public class Main {
ts = m;
try {
m.init();
} catch (AuthorizationFailedException e) {
if (!"register".equals(ns.getString("command"))) {
// Register command should still be possible, if current authorization fails
System.err.println("Authorization failed, was the number registered elsewhere?");
return 2;
}
} catch (Exception e) {
System.err.println("Error loading state file: " + e.getMessage());
return 2;

View file

@ -5,7 +5,6 @@ import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.util.DateUtils;
import org.asamk.signal.util.Util;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
@ -70,11 +69,6 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("The users key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getName() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getName() + "' to mark it as trusted");
System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getName() + "' to trust it without verification");
} else if (exception instanceof ProtocolUntrustedIdentityException) {
ProtocolUntrustedIdentityException e = (ProtocolUntrustedIdentityException) exception;
System.out.println("The users key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getSender() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getSender() + "' to mark it as trusted");
System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getSender() + "' to trust it without verification");
} else {
System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")");
}
@ -109,7 +103,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Received sync read messages list");
for (ReadMessage rm : syncMessage.getRead().get()) {
ContactInfo fromContact = m.getContact(rm.getSender().getNumber().get());
System.out.println("From: " + (fromContact == null ? "" : "" + fromContact.name + "") + rm.getSender().getNumber() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp()));
System.out.println("From: " + (fromContact == null ? "" : "" + fromContact.name + "") + rm.getSender().getNumber().get() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp()));
}
}
if (syncMessage.getRequest().isPresent()) {
@ -129,6 +123,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
String dest = sentTranscriptMessage.getDestination().get().getNumber().get();
ContactInfo destContact = m.getContact(dest);
to = (destContact == null ? "" : "" + destContact.name + "") + dest;
} else if (sentTranscriptMessage.getRecipients().size() > 0) {
StringBuilder toBuilder = new StringBuilder();
for (SignalServiceAddress dest : sentTranscriptMessage.getRecipients()) {
ContactInfo destContact = m.getContact(dest.getNumber().get());
toBuilder.append(destContact == null ? "" : "" + destContact.name + "").append(dest.getNumber().get()).append(" ");
}
to = toBuilder.toString();
} else {
to = "Unknown";
}
@ -144,14 +145,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Blocked numbers:");
final BlockedListMessage blockedList = syncMessage.getBlockedList().get();
for (SignalServiceAddress address : blockedList.getAddresses()) {
System.out.println(" - " + address.getNumber());
System.out.println(" - " + address.getNumber().get());
}
}
if (syncMessage.getVerified().isPresent()) {
System.out.println("Received sync message with verified identities:");
final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified());
String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey()));
String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey()));
System.out.println(" " + safetyNumber);
}
if (syncMessage.getConfiguration().isPresent()) {
@ -168,7 +169,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (syncMessage.getViewOnceOpen().isPresent()) {
final ViewOnceOpenMessage viewOnceOpenMessage = syncMessage.getViewOnceOpen().get();
System.out.println("Received sync message with view once open message:");
System.out.println(" - Sender:" + viewOnceOpenMessage.getSender().getNumber());
System.out.println(" - Sender:" + viewOnceOpenMessage.getSender().getNumber().get());
System.out.println(" - Timestamp:" + viewOnceOpenMessage.getTimestamp());
}
if (syncMessage.getStickerPackOperations().isPresent()) {
@ -253,8 +254,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (message.getBody().isPresent()) {
System.out.println("Body: " + message.getBody().get());
}
if (message.getGroupInfo().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupInfo().get();
if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
@ -322,7 +323,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
final SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
System.out.println("Reaction:");
System.out.println(" - Emoji: " + reaction.getEmoji());
System.out.println(" - Target author: " + reaction.getTargetAuthor().getNumber());
System.out.println(" - Target author: " + reaction.getTargetAuthor().getNumber().get());
System.out.println(" - Target timestamp: " + reaction.getTargetSentTimestamp());
System.out.println(" - Is remove: " + reaction.isRemove());
}
@ -330,7 +331,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (message.getQuote().isPresent()) {
SignalServiceDataMessage.Quote quote = message.getQuote().get();
System.out.println("Quote: (" + quote.getId() + ")");
System.out.println(" Author: " + quote.getAuthor().getNumber());
System.out.println(" Author: " + quote.getAuthor().getNumber().get());
System.out.println(" Text: " + quote.getText());
if (quote.getAttachments().size() > 0) {
System.out.println(" Attachments: ");

View file

@ -0,0 +1,8 @@
package org.asamk.signal;
public class StickerPackInvalidException extends Exception {
public StickerPackInvalidException(String message) {
super(message);
}
}

View file

@ -2,6 +2,7 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.manager.Manager;

View file

@ -33,6 +33,7 @@ public class Commands {
addCommand("updateGroup", new UpdateGroupCommand());
addCommand("updateProfile", new UpdateProfileCommand());
addCommand("verify", new VerifyCommand());
addCommand("uploadStickerPack", new UploadStickerPackCommand());
}
public static Map<String, Command> getCommands() {

View file

@ -5,9 +5,11 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.contacts.ContactInfo;
import java.util.List;
public class ListContactsCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
}

View file

@ -6,19 +6,20 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.groups.GroupInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
import java.util.List;
public class ListGroupsCommand implements LocalCommand {
private static void printGroup(GroupInfo group, boolean detailed, String username) {
private static void printGroup(GroupInfo group, boolean detailed, SignalServiceAddress address) {
if (detailed) {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s",
Base64.encodeBytes(group.groupId), group.name, group.members.contains(username), group.blocked, group.members));
Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked, group.getMembersE164()));
} else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
Base64.encodeBytes(group.groupId), group.name, group.members.contains(username), group.blocked));
Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked));
}
}
@ -40,7 +41,7 @@ public class ListGroupsCommand implements LocalCommand {
boolean detailed = ns.getBoolean("detailed");
for (GroupInfo group : groups) {
printGroup(group, detailed, m.getUsername());
printGroup(group, detailed, m.getSelfAddress());
}
return 0;
}

View file

@ -7,18 +7,16 @@ import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.List;
import java.util.Map;
public class ListIdentitiesCommand implements LocalCommand {
private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) {
String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey()));
System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername,
theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits));
private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) {
String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey()));
System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirId.getAddress().getNumber().orNull(),
theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toString(theirId.getFingerprint()), digits));
}
@Override
@ -34,17 +32,15 @@ public class ListIdentitiesCommand implements LocalCommand {
return 1;
}
if (ns.get("number") == null) {
for (Map.Entry<String, List<JsonIdentityKeyStore.Identity>> keys : m.getIdentities().entrySet()) {
for (JsonIdentityKeyStore.Identity id : keys.getValue()) {
printIdentityFingerprint(m, keys.getKey(), id);
}
for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) {
printIdentityFingerprint(m, identity);
}
} else {
String number = ns.getString("number");
try {
Pair<String, List<JsonIdentityKeyStore.Identity>> key = m.getIdentities(number);
for (JsonIdentityKeyStore.Identity id : key.second()) {
printIdentityFingerprint(m, key.first(), id);
List<JsonIdentityKeyStore.Identity> identities = m.getIdentities(number);
for (JsonIdentityKeyStore.Identity id : identities) {
printIdentityFingerprint(m, id);
}
} catch (InvalidNumberException e) {
System.out.println("Invalid number: " + e.getMessage());

View file

@ -10,7 +10,6 @@ import org.asamk.signal.ReceiveMessageHandler;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.DateUtils;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.DBusSigHandler;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.util.Base64;
@ -54,7 +53,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
});
dbusconnection.addSigHandler(Signal.ReceiptReceived.class,
receiptReceived -> System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n",
receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp()))));
receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp()))));
dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> {
System.out.print(String.format("Sync Envelope from: %s to: %s\nTimestamp: %s\nBody: %s\n",
syncReceived.getSource(), syncReceived.getDestination(), DateUtils.formatTimestamp(syncReceived.getTimestamp()), syncReceived.getMessage()));

View file

@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import java.io.IOException;
@ -22,6 +23,9 @@ public class RegisterCommand implements LocalCommand {
try {
m.register(ns.getBoolean("voice"));
return 0;
} catch (CaptchaRequiredException e) {
System.err.println("Captcha required for verification (" + e.getMessage() + ")");
return 1;
} catch (IOException e) {
System.err.println("Request verify error: " + e.getMessage());
return 3;

View file

@ -13,6 +13,7 @@ import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.nio.charset.Charset;
@ -25,6 +26,7 @@ import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
public class SendCommand implements DbusCommand {
@ -75,6 +77,9 @@ public class SendCommand implements DbusCommand {
} catch (DBusExecutionException e) {
handleDBusExecutionException(e);
return 1;
} catch (InvalidNumberException e) {
handleInvalidNumberException(e);
return 1;
}
}
@ -126,6 +131,9 @@ public class SendCommand implements DbusCommand {
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;
} catch (InvalidNumberException e) {
handleInvalidNumberException(e);
return 1;
}
}
}

View file

@ -2,6 +2,7 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;

View file

@ -9,8 +9,8 @@ import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.NotAGroupMemberException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
@ -19,6 +19,7 @@ import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
public class SendReactionCommand implements LocalCommand {
@ -61,7 +62,7 @@ public class SendReactionCommand implements LocalCommand {
String emoji = ns.getString("emoji");
boolean isRemove = ns.getBoolean("remove");
SignalServiceAddress targetAuthor = new SignalServiceAddress(null, ns.getString("target_author"));
String targetAuthor = ns.getString("target_author");
long targetTimestamp = ns.getLong("target_timestamp");
try {
@ -90,6 +91,9 @@ public class SendReactionCommand implements LocalCommand {
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;
} catch (InvalidNumberException e) {
handleInvalidNumberException(e);
return 1;
}
}
}

View file

@ -6,7 +6,9 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.ErrorUtils;
import org.asamk.signal.util.Hex;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.Locale;
@ -50,13 +52,25 @@ public class TrustCommand implements LocalCommand {
System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters.");
return 1;
}
boolean res = m.trustIdentityVerified(number, fingerprintBytes);
boolean res;
try {
res = m.trustIdentityVerified(number, fingerprintBytes);
} catch (InvalidNumberException e) {
ErrorUtils.handleInvalidNumberException(e);
return 1;
}
if (!res) {
System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct.");
return 1;
}
} else if (fingerprint.length() == 60) {
boolean res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint);
boolean res;
try {
res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint);
} catch (InvalidNumberException e) {
ErrorUtils.handleInvalidNumberException(e);
return 1;
}
if (!res) {
System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct.");
return 1;

View file

@ -2,6 +2,7 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.manager.Manager;

View file

@ -10,6 +10,7 @@ import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.NotAGroupMemberException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.util.Base64;
import java.io.IOException;
@ -20,6 +21,7 @@ import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
public class UpdateGroupCommand implements DbusCommand {
@ -88,6 +90,9 @@ public class UpdateGroupCommand implements DbusCommand {
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;
} catch (InvalidNumberException e) {
handleInvalidNumberException(e);
return 1;
}
}
}

View file

@ -0,0 +1,34 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.StickerPackInvalidException;
import org.asamk.signal.manager.Manager;
import java.io.IOException;
public class UploadStickerPackCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("path")
.help("The path of the manifest.json or a zip file containing the sticker pack you wish to upload.");
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
try {
String path = ns.getString("path");
String url = m.uploadStickerPack(path);
System.out.println(url);
return 0;
} catch (IOException e) {
System.err.println("Upload error: " + e.getMessage());
return 3;
} catch (StickerPackInvalidException e) {
System.err.println("Invalid sticker pack: " + e.getMessage());
return 3;
}
}
}

View file

@ -1,5 +1,6 @@
package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
@ -49,6 +50,8 @@ public class BaseConfig {
zkGroupServerPublicParams
);
static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false);
private BaseConfig() {
}
}

View file

@ -34,6 +34,10 @@ class KeyUtils {
return getSecretBytes(16);
}
static byte[] createStickerUploadKey() {
return getSecretBytes(32);
}
private static String getSecret(int size) {
byte[] secret = getSecretBytes(size);
return Base64.encodeBytes(secret);

File diff suppressed because it is too large Load diff

View file

@ -13,9 +13,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64;
import java.io.BufferedInputStream;
@ -35,12 +34,10 @@ import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
@ -149,38 +146,19 @@ class Utils {
return new DeviceLinkInfo(deviceIdentifier, deviceKey);
}
static Set<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients, String localNumber) {
Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
for (String recipient : recipients) {
try {
recipientsTS.add(getPushAddress(recipient, localNumber));
} catch (InvalidNumberException e) {
System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
System.err.println("Aborting sending.");
return null;
}
}
return recipientsTS;
}
static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException {
return PhoneNumberFormatter.formatNumber(number, localNumber);
}
private static SignalServiceAddress getPushAddress(String number, String localNumber) throws InvalidNumberException {
String e164number = canonicalizeNumber(number, localNumber);
return new SignalServiceAddress(null, e164number);
}
static SignalServiceEnvelope loadEnvelope(File file) throws IOException {
try (FileInputStream f = new FileInputStream(file)) {
DataInputStream in = new DataInputStream(f);
int version = in.readInt();
if (version > 2) {
if (version > 3) {
return null;
}
int type = in.readInt();
String source = in.readUTF();
UUID sourceUuid = null;
if (version >= 3) {
sourceUuid = UuidUtil.parseOrNull(in.readUTF());
}
int sourceDevice = in.readInt();
if (version == 1) {
// read legacy relay field
@ -208,16 +186,20 @@ class Utils {
uuid = null;
}
}
return new SignalServiceEnvelope(type, Optional.of(new SignalServiceAddress(null, source)), sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid);
Optional<SignalServiceAddress> addressOptional = sourceUuid == null && source.isEmpty()
? Optional.absent()
: Optional.of(new SignalServiceAddress(sourceUuid, source));
return new SignalServiceEnvelope(type, addressOptional, sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid);
}
}
static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
try (FileOutputStream f = new FileOutputStream(file)) {
try (DataOutputStream out = new DataOutputStream(f)) {
out.writeInt(2); // version
out.writeInt(3); // version
out.writeInt(envelope.getType());
out.writeUTF(envelope.getSourceE164().get());
out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : "");
out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : "");
out.writeInt(envelope.getSourceDevice());
out.writeLong(envelope.getTimestamp());
if (envelope.hasContent()) {
@ -256,10 +238,25 @@ class Utils {
return outputFile;
}
static String computeSafetyNumber(String ownUsername, IdentityKey ownIdentityKey, String theirUsername, IdentityKey theirIdentityKey) {
// Version 1: E164 user
// Version 2: UUID user
Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(1, ownUsername.getBytes(), ownIdentityKey, theirUsername.getBytes(), theirIdentityKey);
static String computeSafetyNumber(SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
int version;
byte[] ownId;
byte[] theirId;
if (BaseConfig.capabilities.isUuid()
&& ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) {
// Version 2: UUID user
version = 2;
ownId = UuidUtil.toByteArray(ownAddress.getUuid().get());
theirId = UuidUtil.toByteArray(theirAddress.getUuid().get());
} else {
// Version 1: E164 user
version = 1;
ownId = ownAddress.getNumber().get().getBytes();
theirId = theirAddress.getNumber().get().getBytes();
}
Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, ownId, ownIdentityKey, theirId, theirIdentityKey);
return fingerprint.getDisplayableFingerprint().getDisplayText();
}

View file

@ -10,10 +10,14 @@ 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.protocol.SignalServiceAddressResolver;
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;
@ -32,6 +36,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 {
@ -39,6 +44,7 @@ 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;
@ -53,7 +59,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
@ -82,27 +87,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, ProfileKey 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;
@ -138,6 +142,14 @@ public class SignalAccount {
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();
@ -189,10 +201,27 @@ 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()) {
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) {
}
}
}
}
@ -202,6 +231,7 @@ 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)
@ -214,7 +244,6 @@ public class SignalAccount {
.putPOJO("axolotlStore", signalProtocolStore)
.putPOJO("groupStore", groupStore)
.putPOJO("contactStore", contactStore)
.putPOJO("threadStore", threadStore)
;
try {
synchronized (fileChannel) {
@ -245,6 +274,10 @@ public class SignalAccount {
}
}
public void setResolver(final SignalServiceAddressResolver resolver) {
signalProtocolStore.setResolver(resolver);
}
public void addPreKeys(Collection<PreKeyRecord> records) {
for (PreKeyRecord record : records) {
signalProtocolStore.storePreKey(record.getId(), record);
@ -269,16 +302,20 @@ 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(null, username);
return new SignalServiceAddress(uuid, username);
}
public int getDeviceId() {

View file

@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.UUID;
public class ContactInfo {
@JsonProperty
@ -13,9 +15,15 @@ public class ContactInfo {
@JsonProperty
public String number;
@JsonProperty
public UUID uuid;
@JsonProperty
public String color;
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
@JsonProperty
public String profileKey;
@ -28,8 +36,16 @@ public class ContactInfo {
@JsonProperty(defaultValue = "false")
public boolean archived;
public ContactInfo() {
}
public ContactInfo(SignalServiceAddress address) {
this.number = address.getNumber().orNull();
this.uuid = address.getUuid().orNull();
}
@JsonIgnore
public SignalServiceAddress getAddress() {
return new SignalServiceAddress(null, number);
return new SignalServiceAddress(uuid, number);
}
}

View file

@ -1,41 +1,40 @@
package org.asamk.signal.storage.contacts;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList;
import java.util.HashMap;
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 List<ContactInfo> contacts = new ArrayList<>();
public void updateContact(ContactInfo contact) {
contacts.put(contact.number, contact);
final SignalServiceAddress contactAddress = contact.getAddress();
for (int i = 0; i < contacts.size(); i++) {
if (contacts.get(i).getAddress().matches(contactAddress)) {
contacts.set(i, contact);
return;
}
}
contacts.add(contact);
}
public ContactInfo getContact(String number) {
return contacts.get(number);
public ContactInfo getContact(SignalServiceAddress address) {
for (ContactInfo contact : contacts) {
if (contact.getAddress().matches(address)) {
return contact;
}
}
return null;
}
public List<ContactInfo> getContacts() {
return new ArrayList<>(contacts.values());
return new ArrayList<>(contacts);
}
/**
@ -44,27 +43,4 @@ public class JsonContactsStore {
public void clear() {
contacts.clear();
}
private 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());
}
}
private 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<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
ContactInfo c = jsonProcessor.treeToValue(n, ContactInfo.class);
contacts.put(c.number, c);
}
return contacts;
}
}
}

View file

@ -2,15 +2,29 @@ package org.asamk.signal.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty
public final byte[] groupId;
@ -18,9 +32,13 @@ public class GroupInfo {
public String name;
@JsonProperty
public Set<String> members = new HashSet<>();
@JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public Set<SignalServiceAddress> members = new HashSet<>();
@JsonProperty
public String color;
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
@JsonProperty(defaultValue = "false")
public boolean blocked;
@JsonProperty
@ -38,7 +56,7 @@ public class GroupInfo {
this.groupId = groupId;
}
public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection<String> members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived) {
public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection<SignalServiceAddress> members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived, @JsonProperty("messageExpirationTime") int messageExpirationTime) {
this.groupId = groupId;
this.name = name;
this.members.addAll(members);
@ -47,6 +65,7 @@ public class GroupInfo {
this.blocked = blocked;
this.inboxPosition = inboxPosition;
this.archived = archived;
this.messageExpirationTime = messageExpirationTime;
}
@JsonIgnore
@ -56,16 +75,108 @@ public class GroupInfo {
@JsonIgnore
public Set<SignalServiceAddress> getMembers() {
Set<SignalServiceAddress> addresses = new HashSet<>(members.size());
for (String member : members) {
addresses.add(new SignalServiceAddress(null, member));
}
return addresses;
return members;
}
public void addMembers(Collection<SignalServiceAddress> members) {
@JsonIgnore
public Set<String> getMembersE164() {
Set<String> membersE164 = new HashSet<>();
for (SignalServiceAddress member : members) {
this.members.add(member.getNumber().get());
if (!member.getNumber().isPresent()) {
continue;
}
membersE164.add(member.getNumber().get());
}
return membersE164;
}
@JsonIgnore
public Set<SignalServiceAddress> getMembersWithout(SignalServiceAddress address) {
Set<SignalServiceAddress> members = new HashSet<>(this.members.size());
for (SignalServiceAddress member : this.members) {
if (!member.matches(address)) {
members.add(member);
}
}
return members;
}
public void addMembers(Collection<SignalServiceAddress> addresses) {
for (SignalServiceAddress address : addresses) {
removeMember(address);
this.members.add(address);
}
}
public void removeMember(SignalServiceAddress address) {
this.members.removeIf(member -> member.matches(address));
}
@JsonIgnore
public boolean isMember(SignalServiceAddress address) {
for (SignalServiceAddress member : this.members) {
if (member.matches(address)) {
return true;
}
}
return false;
}
private static final class JsonSignalServiceAddress {
@JsonProperty
private UUID uuid;
@JsonProperty
private String number;
JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) {
this.uuid = uuid;
this.number = number;
}
JsonSignalServiceAddress(SignalServiceAddress address) {
this.uuid = address.getUuid().orNull();
this.number = address.getNumber().orNull();
}
SignalServiceAddress toSignalServiceAddress() {
return new SignalServiceAddress(uuid, number);
}
}
private static class MembersSerializer extends JsonSerializer<Set<SignalServiceAddress>> {
@Override
public void serialize(final Set<SignalServiceAddress> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
jgen.writeStartArray(value.size());
for (SignalServiceAddress address : value) {
if (address.getUuid().isPresent()) {
jgen.writeObject(new JsonSignalServiceAddress(address));
} else {
jgen.writeString(address.getNumber().get());
}
}
jgen.writeEndArray();
}
}
private static class MembersDeserializer extends JsonDeserializer<Set<SignalServiceAddress>> {
@Override
public Set<SignalServiceAddress> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
Set<SignalServiceAddress> addresses = new HashSet<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
if (n.isTextual()) {
addresses.add(new SignalServiceAddress(null, n.textValue()));
} else {
JsonSignalServiceAddress address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class);
addresses.add(address.toSignalServiceAddress());
}
}
return addresses;
}
}
}

View file

@ -9,32 +9,48 @@ import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.TrustLevel;
import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class JsonIdentityKeyStore implements IdentityKeyStore {
private final Map<String, List<Identity>> trustedKeys = new HashMap<>();
private final List<Identity> identities = new ArrayList<>();
private final IdentityKeyPair identityKeyPair;
private final int localRegistrationId;
private SignalServiceAddressResolver resolver;
public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) {
this.identityKeyPair = identityKeyPair;
this.localRegistrationId = localRegistrationId;
}
public void setResolver(final SignalServiceAddressResolver resolver) {
this.resolver = resolver;
}
private SignalServiceAddress resolveSignalServiceAddress(String identifier) {
if (resolver != null) {
return resolver.resolveSignalServiceAddress(identifier);
} else {
return Util.getSignalServiceAddressFromIdentifier(identifier);
}
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyPair;
@ -47,85 +63,116 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return saveIdentity(address.getName(), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null);
return saveIdentity(resolveSignalServiceAddress(address.getName()), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null);
}
/**
* Adds or updates the given identityKey for the user name and sets the trustLevel and added timestamp.
* Adds the given identityKey for the user name and sets the trustLevel and added timestamp.
* If the identityKey already exists, the trustLevel and added timestamp are NOT updated.
*
* @param name User name, i.e. phone number
* @param identityKey The user's public key
* @param trustLevel
* @param added Added timestamp, if null and the key is newly added, the current time is used.
* @param serviceAddress User address, i.e. phone number and/or uuid
* @param identityKey The user's public key
* @param trustLevel Level of trust: untrusted, trusted, trusted and verified
* @param added Added timestamp, if null and the key is newly added, the current time is used.
*/
public boolean saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
List<Identity> identities = trustedKeys.get(name);
if (identities == null) {
identities = new ArrayList<>();
trustedKeys.put(name, identities);
} else {
for (Identity id : identities) {
if (!id.identityKey.equals(identityKey))
continue;
if (id.trustLevel.compareTo(trustLevel) < 0) {
id.trustLevel = trustLevel;
}
if (added != null) {
id.added = added;
}
return true;
public boolean saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
for (Identity id : identities) {
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue;
}
if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
id.address = serviceAddress;
}
// Identity already exists, not updating the trust level
return true;
}
identities.add(new Identity(identityKey, trustLevel, added != null ? added : new Date()));
identities.add(new Identity(serviceAddress, identityKey, trustLevel, added != null ? added : new Date()));
return false;
}
/**
* Update trustLevel for the given identityKey for the user name.
*
* @param serviceAddress User address, i.e. phone number and/or uuid
* @param identityKey The user's public key
* @param trustLevel Level of trust: untrusted, trusted, trusted and verified
*/
public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
for (Identity id : identities) {
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue;
}
if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
id.address = serviceAddress;
}
id.trustLevel = trustLevel;
return;
}
identities.add(new Identity(serviceAddress, identityKey, trustLevel, new Date()));
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
// TODO implement possibility for different handling of incoming/outgoing trust decisions
List<Identity> identities = trustedKeys.get(address.getName());
if (identities == null) {
// Trust on first use
return true;
}
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
boolean trustOnFirstUse = true;
for (Identity id : identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
if (id.identityKey.equals(identityKey)) {
return id.isTrusted();
} else {
trustOnFirstUse = false;
}
}
return false;
return trustOnFirstUse;
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
List<Identity> identities = trustedKeys.get(address.getName());
if (identities == null || identities.size() == 0) {
return null;
}
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
Identity identity = getIdentity(serviceAddress);
return identity == null ? null : identity.getIdentityKey();
}
public Identity getIdentity(SignalServiceAddress serviceAddress) {
long maxDate = 0;
Identity maxIdentity = null;
for (Identity id : identities) {
for (Identity id : this.identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
final long time = id.getDateAdded().getTime();
if (maxIdentity == null || maxDate <= time) {
maxDate = time;
maxIdentity = id;
}
}
return maxIdentity.getIdentityKey();
return maxIdentity;
}
public Map<String, List<Identity>> getIdentities() {
public List<Identity> getIdentities() {
// TODO deep copy
return trustedKeys;
return identities;
}
public List<Identity> getIdentities(String name) {
// TODO deep copy
return trustedKeys.get(name);
public List<Identity> getIdentities(SignalServiceAddress serviceAddress) {
List<Identity> identities = new ArrayList<>();
for (Identity identity : this.identities) {
if (identity.address.matches(serviceAddress)) {
identities.add(identity);
}
}
return identities;
}
public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<JsonIdentityKeyStore> {
@ -143,12 +190,26 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
JsonNode trustedKeysNode = node.get("trustedKeys");
if (trustedKeysNode.isArray()) {
for (JsonNode trustedKey : trustedKeysNode) {
String trustedKeyName = trustedKey.get("name").asText();
String trustedKeyName = trustedKey.has("name")
? trustedKey.get("name").asText()
: null;
if (UuidUtil.isUuid(trustedKeyName)) {
// Ignore identities that were incorrectly created with UUIDs as name
continue;
}
UUID uuid = trustedKey.hasNonNull("uuid")
? UuidUtil.parseOrNull(trustedKey.get("uuid").asText())
: null;
final SignalServiceAddress serviceAddress = uuid == null
? Util.getSignalServiceAddressFromIdentifier(trustedKeyName)
: new SignalServiceAddress(uuid, trustedKeyName);
try {
IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0);
TrustLevel trustLevel = trustedKey.has("trustLevel") ? TrustLevel.fromInt(trustedKey.get("trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
Date added = trustedKey.has("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp").asLong()) : new Date();
keyStore.saveIdentity(trustedKeyName, id, trustLevel, added);
keyStore.saveIdentity(serviceAddress, id, trustLevel, added);
} catch (InvalidKeyException | IOException e) {
System.out.println(String.format("Error while decoding key for: %s", trustedKeyName));
}
@ -170,15 +231,18 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId());
json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
json.writeArrayFieldStart("trustedKeys");
for (Map.Entry<String, List<Identity>> trustedKey : jsonIdentityKeyStore.trustedKeys.entrySet()) {
for (Identity id : trustedKey.getValue()) {
json.writeStartObject();
json.writeStringField("name", trustedKey.getKey());
json.writeStringField("identityKey", Base64.encodeBytes(id.identityKey.serialize()));
json.writeNumberField("trustLevel", id.trustLevel.ordinal());
json.writeNumberField("addedTimestamp", id.added.getTime());
json.writeEndObject();
for (Identity trustedKey : jsonIdentityKeyStore.identities) {
json.writeStartObject();
if (trustedKey.getAddress().getNumber().isPresent()) {
json.writeStringField("name", trustedKey.getAddress().getNumber().get());
}
if (trustedKey.getAddress().getUuid().isPresent()) {
json.writeStringField("uuid", trustedKey.getAddress().getUuid().get().toString());
}
json.writeStringField("identityKey", Base64.encodeBytes(trustedKey.identityKey.serialize()));
json.writeNumberField("trustLevel", trustedKey.trustLevel.ordinal());
json.writeNumberField("addedTimestamp", trustedKey.added.getTime());
json.writeEndObject();
}
json.writeEndArray();
json.writeEndObject();
@ -187,22 +251,33 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
public static class Identity {
SignalServiceAddress address;
IdentityKey identityKey;
TrustLevel trustLevel;
Date added;
public Identity(IdentityKey identityKey, TrustLevel trustLevel) {
public Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) {
this.address = address;
this.identityKey = identityKey;
this.trustLevel = trustLevel;
this.added = new Date();
}
Identity(IdentityKey identityKey, TrustLevel trustLevel, Date added) {
Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
this.address = address;
this.identityKey = identityKey;
this.trustLevel = trustLevel;
this.added = added;
}
public SignalServiceAddress getAddress() {
return address;
}
public void setAddress(final SignalServiceAddress address) {
this.address = address;
}
boolean isTrusted() {
return trustLevel == TrustLevel.TRUSTED_UNVERIFIED ||
trustLevel == TrustLevel.TRUSTED_VERIFIED;

View file

@ -8,51 +8,68 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
class JsonSessionStore implements SessionStore {
private final Map<SignalProtocolAddress, byte[]> sessions = new HashMap<>();
private final List<SessionInfo> sessions = new ArrayList<>();
private SignalServiceAddressResolver resolver;
public JsonSessionStore() {
}
private void addSessions(Map<SignalProtocolAddress, byte[]> sessions) {
this.sessions.putAll(sessions);
public void setResolver(final SignalServiceAddressResolver resolver) {
this.resolver = resolver;
}
@Override
public synchronized SessionRecord loadSession(SignalProtocolAddress remoteAddress) {
try {
if (containsSession(remoteAddress)) {
return new SessionRecord(sessions.get(remoteAddress));
} else {
return new SessionRecord();
}
} catch (IOException e) {
throw new AssertionError(e);
private SignalServiceAddress resolveSignalServiceAddress(String identifier) {
if (resolver != null) {
return resolver.resolveSignalServiceAddress(identifier);
} else {
return Util.getSignalServiceAddressFromIdentifier(identifier);
}
}
@Override
public synchronized List<Integer> getSubDeviceSessions(String name) {
List<Integer> deviceIds = new LinkedList<>();
public synchronized SessionRecord loadSession(SignalProtocolAddress address) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
for (SessionInfo info : sessions) {
if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) {
try {
return new SessionRecord(info.sessionRecord);
} catch (IOException e) {
System.err.println("Failed to load session, resetting session: " + e);
final SessionRecord sessionRecord = new SessionRecord();
info.sessionRecord = sessionRecord.serialize();
return sessionRecord;
}
}
}
for (SignalProtocolAddress key : sessions.keySet()) {
if (key.getName().equals(name) &&
key.getDeviceId() != 1) {
deviceIds.add(key.getDeviceId());
return new SessionRecord();
}
@Override
public synchronized List<Integer> getSubDeviceSessions(String name) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name);
List<Integer> deviceIds = new LinkedList<>();
for (SessionInfo info : sessions) {
if (info.address.matches(serviceAddress) && info.deviceId != 1) {
deviceIds.add(info.deviceId);
}
}
@ -61,26 +78,45 @@ class JsonSessionStore implements SessionStore {
@Override
public synchronized void storeSession(SignalProtocolAddress address, SessionRecord record) {
sessions.put(address, record.serialize());
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
for (SessionInfo info : sessions) {
if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) {
if (!info.address.getUuid().isPresent() || !info.address.getNumber().isPresent()) {
info.address = serviceAddress;
}
info.sessionRecord = record.serialize();
return;
}
}
sessions.add(new SessionInfo(serviceAddress, address.getDeviceId(), record.serialize()));
}
@Override
public synchronized boolean containsSession(SignalProtocolAddress address) {
return sessions.containsKey(address);
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
for (SessionInfo info : sessions) {
if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) {
return true;
}
}
return false;
}
@Override
public synchronized void deleteSession(SignalProtocolAddress address) {
sessions.remove(address);
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
sessions.removeIf(info -> info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId());
}
@Override
public synchronized void deleteAllSessions(String name) {
for (SignalProtocolAddress key : new ArrayList<>(sessions.keySet())) {
if (key.getName().equals(name)) {
sessions.remove(key);
}
}
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name);
deleteAllSessions(serviceAddress);
}
public synchronized void deleteAllSessions(SignalServiceAddress serviceAddress) {
sessions.removeIf(info -> info.address.matches(serviceAddress));
}
public static class JsonSessionStoreDeserializer extends JsonDeserializer<JsonSessionStore> {
@ -89,23 +125,36 @@ class JsonSessionStore implements SessionStore {
public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Map<SignalProtocolAddress, byte[]> sessionMap = new HashMap<>();
JsonSessionStore sessionStore = new JsonSessionStore();
if (node.isArray()) {
for (JsonNode session : node) {
String sessionName = session.get("name").asText();
String sessionName = session.has("name")
? session.get("name").asText()
: null;
if (UuidUtil.isUuid(sessionName)) {
// Ignore sessions that were incorrectly created with UUIDs as name
continue;
}
UUID uuid = session.hasNonNull("uuid")
? UuidUtil.parseOrNull(session.get("uuid").asText())
: null;
final SignalServiceAddress serviceAddress = uuid == null
? Util.getSignalServiceAddressFromIdentifier(sessionName)
: new SignalServiceAddress(uuid, sessionName);
final int deviceId = session.get("deviceId").asInt();
final String record = session.get("record").asText();
try {
sessionMap.put(new SignalProtocolAddress(sessionName, session.get("deviceId").asInt()), Base64.decode(session.get("record").asText()));
SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record));
sessionStore.sessions.add(sessionInfo);
} catch (IOException e) {
System.out.println(String.format("Error while decoding session for: %s", sessionName));
}
}
}
JsonSessionStore sessionStore = new JsonSessionStore();
sessionStore.addSessions(sessionMap);
return sessionStore;
}
}
@ -114,14 +163,20 @@ class JsonSessionStore implements SessionStore {
@Override
public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
json.writeStartArray();
for (Map.Entry<SignalProtocolAddress, byte[]> preKey : jsonSessionStore.sessions.entrySet()) {
for (SessionInfo sessionInfo : jsonSessionStore.sessions) {
json.writeStartObject();
json.writeStringField("name", preKey.getKey().getName());
json.writeNumberField("deviceId", preKey.getKey().getDeviceId());
json.writeStringField("record", Base64.encodeBytes(preKey.getValue()));
if (sessionInfo.address.getNumber().isPresent()) {
json.writeStringField("name", sessionInfo.address.getNumber().get());
}
if (sessionInfo.address.getUuid().isPresent()) {
json.writeStringField("uuid", sessionInfo.address.getUuid().get().toString());
}
json.writeNumberField("deviceId", sessionInfo.deviceId);
json.writeStringField("record", Base64.encodeBytes(sessionInfo.sessionRecord));
json.writeEndObject();
}
json.writeEndArray();
}
}
}

View file

@ -13,9 +13,9 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
import java.util.Map;
public class JsonSignalProtocolStore implements SignalProtocolStore {
@ -56,6 +56,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId);
}
public void setResolver(final SignalServiceAddressResolver resolver) {
sessionStore.setResolver(resolver);
identityKeyStore.setResolver(resolver);
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyStore.getIdentityKeyPair();
@ -71,16 +76,20 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
return identityKeyStore.saveIdentity(address, identityKey);
}
public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) {
identityKeyStore.saveIdentity(name, identityKey, trustLevel, null);
public void saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
identityKeyStore.saveIdentity(serviceAddress, identityKey, trustLevel, null);
}
public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel);
}
public List<JsonIdentityKeyStore.Identity> getIdentities() {
return identityKeyStore.getIdentities();
}
public List<JsonIdentityKeyStore.Identity> getIdentities(String name) {
return identityKeyStore.getIdentities(name);
public List<JsonIdentityKeyStore.Identity> getIdentities(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentities(serviceAddress);
}
@Override
@ -93,6 +102,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
return identityKeyStore.getIdentity(address);
}
public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentity(serviceAddress);
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
return preKeyStore.loadPreKey(preKeyId);
@ -143,6 +156,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
sessionStore.deleteAllSessions(name);
}
public void deleteAllSessions(SignalServiceAddress serviceAddress) {
sessionStore.deleteAllSessions(serviceAddress);
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
return signedPreKeyStore.loadSignedPreKey(signedPreKeyId);

View file

@ -0,0 +1,18 @@
package org.asamk.signal.storage.protocol;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SessionInfo {
public SignalServiceAddress address;
public int deviceId;
public byte[] sessionRecord;
public SessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) {
this.address = address;
this.deviceId = deviceId;
this.sessionRecord = sessionRecord;
}
}

View file

@ -0,0 +1,13 @@
package org.asamk.signal.storage.protocol;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface SignalServiceAddressResolver {
/**
* Get a SignalServiceAddress with number and/or uuid from an identifier name.
*
* @param identifier can be either a serialized uuid or a e164 phone number
*/
SignalServiceAddress resolveSignalServiceAddress(String identifier);
}

View file

@ -18,23 +18,15 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonThreadStore {
public class LegacyJsonThreadStore {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty("threads")
@JsonSerialize(using = JsonThreadStore.MapToListSerializer.class)
@JsonSerialize(using = MapToListSerializer.class)
@JsonDeserialize(using = ThreadsDeserializer.class)
private Map<String, ThreadInfo> threads = new HashMap<>();
public void updateThread(ThreadInfo thread) {
threads.put(thread.id, thread);
}
public ThreadInfo getThread(String id) {
return threads.get(id);
}
public List<ThreadInfo> getThreads() {
return new ArrayList<>(threads.values());
}

View file

@ -8,6 +8,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
@ -59,4 +60,10 @@ public class ErrorUtils {
System.err.println(e.getMessage());
System.err.println("Aborting sending.");
}
public static void handleInvalidNumberException(InvalidNumberException e) {
System.err.println("Failed to parse recipient: " + e.getMessage());
System.err.println(e.getMessage());
System.err.println("Aborting sending.");
}
}

View file

@ -9,6 +9,15 @@ public class Hex {
private Hex() {
}
public static String toString(byte[] bytes) {
StringBuffer buf = new StringBuffer();
for (final byte aByte : bytes) {
appendHexChar(buf, aByte);
buf.append(" ");
}
return buf.toString();
}
public static String toStringCondensed(byte[] bytes) {
StringBuffer buf = new StringBuffer();
for (final byte aByte : bytes) {
@ -20,7 +29,6 @@ public class Hex {
private static void appendHexChar(StringBuffer buf, int b) {
buf.append(HEX_DIGITS[(b >> 4) & 0xf]);
buf.append(HEX_DIGITS[b & 0xf]);
buf.append(" ");
}
public static byte[] toByteArray(String s) {

View file

@ -1,5 +1,8 @@
package org.asamk.signal.util;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@ -35,6 +38,12 @@ public class IOUtils {
return output.toString();
}
public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.copy(in, baos);
return baos.toByteArray();
}
public static void createPrivateDirectories(String directoryPath) throws IOException {
final File file = new File(directoryPath);
if (file.exists()) {

View file

@ -5,17 +5,14 @@ import java.security.SecureRandom;
public class RandomUtils {
private static final ThreadLocal<SecureRandom> LOCAL_RANDOM = new ThreadLocal<SecureRandom>() {
@Override
protected SecureRandom initialValue() {
SecureRandom rand = getSecureRandomUnseeded();
private static final ThreadLocal<SecureRandom> LOCAL_RANDOM = ThreadLocal.withInitial(() -> {
SecureRandom rand = getSecureRandomUnseeded();
// Let the SecureRandom seed it self initially
rand.nextBoolean();
// Let the SecureRandom seed it self initially
rand.nextBoolean();
return rand;
}
};
return rand;
});
private static SecureRandom getSecureRandomUnseeded() {
try {

View file

@ -3,6 +3,10 @@ package org.asamk.signal.util;
import com.fasterxml.jackson.databind.JsonNode;
import org.asamk.signal.GroupIdFormatException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64;
import java.io.IOException;
@ -51,4 +55,16 @@ public class Util {
throw new GroupIdFormatException(groupId, e);
}
}
public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException {
return PhoneNumberFormatter.formatNumber(number, localNumber);
}
public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) {
if (UuidUtil.isUuid(identifier)) {
return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null);
} else {
return new SignalServiceAddress(null, identifier);
}
}
}