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> <JavaCodeStyleSettings>
<option name="GENERATE_FINAL_LOCALS" value="true" /> <option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" 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" /> <option name="JD_P_AT_EMPTY_LINES" value="false" />
</JavaCodeStyleSettings> </JavaCodeStyleSettings>
<XML> <XML>

View file

@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_1_8
mainClassName = 'org.asamk.signal.Main' mainClassName = 'org.asamk.signal.Main'
version = '0.6.5' version = '0.6.6'
compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8'
@ -20,7 +20,7 @@ repositories {
} }
dependencies { 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 'org.bouncycastle:bcprov-jdk15on:1.64'
compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1'
compile 'org.freedesktop.dbus:dbus-java:2.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0'

Binary file not shown.

View file

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View file

@ -287,6 +287,34 @@ sendContacts
Send a synchronization message with the local contacts list to all linked devices. 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. 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 daemon
~~~~~~ ~~~~~~
signal-cli can run in daemon mode and provides an experimental dbus interface. For 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 { 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; 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); 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(); 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.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -16,8 +17,9 @@ class JsonDataMessage {
JsonDataMessage(SignalServiceDataMessage dataMessage) { JsonDataMessage(SignalServiceDataMessage dataMessage) {
this.timestamp = dataMessage.getTimestamp(); this.timestamp = dataMessage.getTimestamp();
if (dataMessage.getGroupInfo().isPresent()) { if (dataMessage.getGroupContext().isPresent() && dataMessage.getGroupContext().get().getGroupV1().isPresent()) {
this.groupInfo = new JsonGroupInfo(dataMessage.getGroupInfo().get()); SignalServiceGroup groupInfo = dataMessage.getGroupContext().get().getGroupV1().get();
this.groupInfo = new JsonGroupInfo(groupInfo);
} }
if (dataMessage.getBody().isPresent()) { if (dataMessage.getBody().isPresent()) {
this.message = dataMessage.getBody().get(); this.message = dataMessage.getBody().get();

View file

@ -47,14 +47,15 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
System.out.println(message.getBody().get()); System.out.println(message.getBody().get());
if (!message.isEndSession() && if (!message.isEndSession() &&
!(message.getGroupInfo().isPresent() && !(message.getGroupContext().isPresent() &&
message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) {
try { try {
conn.sendSignal(new Signal.MessageReceived( conn.sendSignal(new Signal.MessageReceived(
objectPath, objectPath,
message.getTimestamp(), message.getTimestamp(),
envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(), 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() : "", message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m))); JsonDbusReceiveMessageHandler.getAttachments(message, m)));
} catch (DBusException e) { } catch (DBusException e) {
@ -66,7 +67,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
if (sync_message.getSent().isPresent()) { if (sync_message.getSent().isPresent()) {
SentTranscriptMessage transcript = sync_message.getSent().get(); 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(); SignalServiceDataMessage message = transcript.getMessage();
try { try {
@ -75,7 +76,8 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
transcript.getTimestamp(), transcript.getTimestamp(),
envelope.getSourceAddress().getNumber().get(), envelope.getSourceAddress().getNumber().get(),
transcript.getDestination().isPresent() ? transcript.getDestination().get().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() : "", message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m))); JsonDbusReceiveMessageHandler.getAttachments(message, m)));
} catch (DBusException e) { } 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,7 +4,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;

View file

@ -38,6 +38,7 @@ import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.File; import java.io.File;
@ -105,6 +106,12 @@ public class Main {
ts = m; ts = m;
try { try {
m.init(); 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) { } catch (Exception e) {
System.err.println("Error loading state file: " + e.getMessage()); System.err.println("Error loading state file: " + e.getMessage());
return 2; 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.storage.groups.GroupInfo;
import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.DateUtils;
import org.asamk.signal.util.Util; 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.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceContent; 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("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("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"); 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 { } else {
System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")"); 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"); System.out.println("Received sync read messages list");
for (ReadMessage rm : syncMessage.getRead().get()) { for (ReadMessage rm : syncMessage.getRead().get()) {
ContactInfo fromContact = m.getContact(rm.getSender().getNumber().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()) { if (syncMessage.getRequest().isPresent()) {
@ -129,6 +123,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
String dest = sentTranscriptMessage.getDestination().get().getNumber().get(); String dest = sentTranscriptMessage.getDestination().get().getNumber().get();
ContactInfo destContact = m.getContact(dest); ContactInfo destContact = m.getContact(dest);
to = (destContact == null ? "" : "" + destContact.name + "") + 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 { } else {
to = "Unknown"; to = "Unknown";
} }
@ -144,14 +145,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Blocked numbers:"); System.out.println("Blocked numbers:");
final BlockedListMessage blockedList = syncMessage.getBlockedList().get(); final BlockedListMessage blockedList = syncMessage.getBlockedList().get();
for (SignalServiceAddress address : blockedList.getAddresses()) { for (SignalServiceAddress address : blockedList.getAddresses()) {
System.out.println(" - " + address.getNumber()); System.out.println(" - " + address.getNumber().get());
} }
} }
if (syncMessage.getVerified().isPresent()) { if (syncMessage.getVerified().isPresent()) {
System.out.println("Received sync message with verified identities:"); System.out.println("Received sync message with verified identities:");
final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified()); 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); System.out.println(" " + safetyNumber);
} }
if (syncMessage.getConfiguration().isPresent()) { if (syncMessage.getConfiguration().isPresent()) {
@ -168,7 +169,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (syncMessage.getViewOnceOpen().isPresent()) { if (syncMessage.getViewOnceOpen().isPresent()) {
final ViewOnceOpenMessage viewOnceOpenMessage = syncMessage.getViewOnceOpen().get(); final ViewOnceOpenMessage viewOnceOpenMessage = syncMessage.getViewOnceOpen().get();
System.out.println("Received sync message with view once open message:"); 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()); System.out.println(" - Timestamp:" + viewOnceOpenMessage.getTimestamp());
} }
if (syncMessage.getStickerPackOperations().isPresent()) { if (syncMessage.getStickerPackOperations().isPresent()) {
@ -253,8 +254,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (message.getBody().isPresent()) { if (message.getBody().isPresent()) {
System.out.println("Body: " + message.getBody().get()); System.out.println("Body: " + message.getBody().get());
} }
if (message.getGroupInfo().isPresent()) { if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupInfo().get(); SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
System.out.println("Group info:"); System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) { 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(); final SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
System.out.println("Reaction:"); System.out.println("Reaction:");
System.out.println(" - Emoji: " + reaction.getEmoji()); 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(" - Target timestamp: " + reaction.getTargetSentTimestamp());
System.out.println(" - Is remove: " + reaction.isRemove()); System.out.println(" - Is remove: " + reaction.isRemove());
} }
@ -330,7 +331,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (message.getQuote().isPresent()) { if (message.getQuote().isPresent()) {
SignalServiceDataMessage.Quote quote = message.getQuote().get(); SignalServiceDataMessage.Quote quote = message.getQuote().get();
System.out.println("Quote: (" + quote.getId() + ")"); 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()); System.out.println(" Text: " + quote.getText());
if (quote.getAttachments().size() > 0) { if (quote.getAttachments().size() > 0) {
System.out.println(" Attachments: "); 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.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.GroupIdFormatException; import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;

View file

@ -33,6 +33,7 @@ public class Commands {
addCommand("updateGroup", new UpdateGroupCommand()); addCommand("updateGroup", new UpdateGroupCommand());
addCommand("updateProfile", new UpdateProfileCommand()); addCommand("updateProfile", new UpdateProfileCommand());
addCommand("verify", new VerifyCommand()); addCommand("verify", new VerifyCommand());
addCommand("uploadStickerPack", new UploadStickerPackCommand());
} }
public static Map<String, Command> getCommands() { 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.manager.Manager;
import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.contacts.ContactInfo;
import java.util.List; import java.util.List;
public class ListContactsCommand implements LocalCommand { public class ListContactsCommand implements LocalCommand {
@Override @Override
public void attachToSubparser(final Subparser subparser) { 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.manager.Manager;
import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.GroupInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64; import org.whispersystems.util.Base64;
import java.util.List; import java.util.List;
public class ListGroupsCommand implements LocalCommand { 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) { if (detailed) {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s", 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 { } else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", 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"); boolean detailed = ns.getBoolean("detailed");
for (GroupInfo group : groups) { for (GroupInfo group : groups) {
printGroup(group, detailed, m.getUsername()); printGroup(group, detailed, m.getSelfAddress());
} }
return 0; 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.storage.protocol.JsonIdentityKeyStore;
import org.asamk.signal.util.Hex; import org.asamk.signal.util.Hex;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.List; import java.util.List;
import java.util.Map;
public class ListIdentitiesCommand implements LocalCommand { public class ListIdentitiesCommand implements LocalCommand {
private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) { private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) {
String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey())); String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey()));
System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirId.getAddress().getNumber().orNull(),
theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits)); theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toString(theirId.getFingerprint()), digits));
} }
@Override @Override
@ -34,17 +32,15 @@ public class ListIdentitiesCommand implements LocalCommand {
return 1; return 1;
} }
if (ns.get("number") == null) { if (ns.get("number") == null) {
for (Map.Entry<String, List<JsonIdentityKeyStore.Identity>> keys : m.getIdentities().entrySet()) { for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) {
for (JsonIdentityKeyStore.Identity id : keys.getValue()) { printIdentityFingerprint(m, identity);
printIdentityFingerprint(m, keys.getKey(), id);
}
} }
} else { } else {
String number = ns.getString("number"); String number = ns.getString("number");
try { try {
Pair<String, List<JsonIdentityKeyStore.Identity>> key = m.getIdentities(number); List<JsonIdentityKeyStore.Identity> identities = m.getIdentities(number);
for (JsonIdentityKeyStore.Identity id : key.second()) { for (JsonIdentityKeyStore.Identity id : identities) {
printIdentityFingerprint(m, key.first(), id); printIdentityFingerprint(m, id);
} }
} catch (InvalidNumberException e) { } catch (InvalidNumberException e) {
System.out.println("Invalid number: " + e.getMessage()); 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.manager.Manager;
import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.DateUtils;
import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.DBusSigHandler;
import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.util.Base64; import org.whispersystems.util.Base64;

View file

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

View file

@ -13,6 +13,7 @@ import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; 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.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException; import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
public class SendCommand implements DbusCommand { public class SendCommand implements DbusCommand {
@ -75,6 +77,9 @@ public class SendCommand implements DbusCommand {
} catch (DBusExecutionException e) { } catch (DBusExecutionException e) {
handleDBusExecutionException(e); handleDBusExecutionException(e);
return 1; return 1;
} catch (InvalidNumberException e) {
handleInvalidNumberException(e);
return 1;
} }
} }
@ -126,6 +131,9 @@ public class SendCommand implements DbusCommand {
} catch (GroupIdFormatException e) { } catch (GroupIdFormatException e) {
handleGroupIdFormatException(e); handleGroupIdFormatException(e);
return 1; 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.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; 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.NotAGroupMemberException;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.Util; 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.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException; 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.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException; import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
public class SendReactionCommand implements LocalCommand { public class SendReactionCommand implements LocalCommand {
@ -61,7 +62,7 @@ public class SendReactionCommand implements LocalCommand {
String emoji = ns.getString("emoji"); String emoji = ns.getString("emoji");
boolean isRemove = ns.getBoolean("remove"); 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"); long targetTimestamp = ns.getLong("target_timestamp");
try { try {
@ -90,6 +91,9 @@ public class SendReactionCommand implements LocalCommand {
} catch (GroupIdFormatException e) { } catch (GroupIdFormatException e) {
handleGroupIdFormatException(e); handleGroupIdFormatException(e);
return 1; 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 net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.ErrorUtils;
import org.asamk.signal.util.Hex; import org.asamk.signal.util.Hex;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.Locale; 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."); System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters.");
return 1; 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) { 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."); 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; return 1;
} }
} else if (fingerprint.length() == 60) { } 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) { 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."); 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; return 1;

View file

@ -2,6 +2,7 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser; import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.GroupIdFormatException; import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.manager.Manager; 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.NotAGroupMemberException;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.util.Base64; import org.whispersystems.util.Base64;
import java.io.IOException; 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.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException; import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
public class UpdateGroupCommand implements DbusCommand { public class UpdateGroupCommand implements DbusCommand {
@ -88,6 +90,9 @@ public class UpdateGroupCommand implements DbusCommand {
} catch (GroupIdFormatException e) { } catch (GroupIdFormatException e) {
handleGroupIdFormatException(e); handleGroupIdFormatException(e);
return 1; 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; package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
@ -49,6 +50,8 @@ public class BaseConfig {
zkGroupServerPublicParams zkGroupServerPublicParams
); );
static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false);
private BaseConfig() { private BaseConfig() {
} }
} }

View file

@ -34,6 +34,10 @@ class KeyUtils {
return getSecretBytes(16); return getSecretBytes(16);
} }
static byte[] createStickerUploadKey() {
return getSecretBytes(32);
}
private static String getSecret(int size) { private static String getSecret(int size) {
byte[] secret = getSecretBytes(size); byte[] secret = getSecretBytes(size);
return Base64.encodeBytes(secret); 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.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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.StreamDetails;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64; import org.whispersystems.util.Base64;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
@ -35,12 +34,10 @@ import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.UUID;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty; import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
@ -149,38 +146,19 @@ class Utils {
return new DeviceLinkInfo(deviceIdentifier, deviceKey); 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 { static SignalServiceEnvelope loadEnvelope(File file) throws IOException {
try (FileInputStream f = new FileInputStream(file)) { try (FileInputStream f = new FileInputStream(file)) {
DataInputStream in = new DataInputStream(f); DataInputStream in = new DataInputStream(f);
int version = in.readInt(); int version = in.readInt();
if (version > 2) { if (version > 3) {
return null; return null;
} }
int type = in.readInt(); int type = in.readInt();
String source = in.readUTF(); String source = in.readUTF();
UUID sourceUuid = null;
if (version >= 3) {
sourceUuid = UuidUtil.parseOrNull(in.readUTF());
}
int sourceDevice = in.readInt(); int sourceDevice = in.readInt();
if (version == 1) { if (version == 1) {
// read legacy relay field // read legacy relay field
@ -208,16 +186,20 @@ class Utils {
uuid = null; 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 { static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
try (FileOutputStream f = new FileOutputStream(file)) { try (FileOutputStream f = new FileOutputStream(file)) {
try (DataOutputStream out = new DataOutputStream(f)) { try (DataOutputStream out = new DataOutputStream(f)) {
out.writeInt(2); // version out.writeInt(3); // version
out.writeInt(envelope.getType()); 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.writeInt(envelope.getSourceDevice());
out.writeLong(envelope.getTimestamp()); out.writeLong(envelope.getTimestamp());
if (envelope.hasContent()) { if (envelope.hasContent()) {
@ -256,10 +238,25 @@ class Utils {
return outputFile; return outputFile;
} }
static String computeSafetyNumber(String ownUsername, IdentityKey ownIdentityKey, String theirUsername, IdentityKey theirIdentityKey) { static String computeSafetyNumber(SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
// Version 1: E164 user int version;
byte[] ownId;
byte[] theirId;
if (BaseConfig.capabilities.isUuid()
&& ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) {
// Version 2: UUID user // Version 2: UUID user
Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(1, ownUsername.getBytes(), ownIdentityKey, theirUsername.getBytes(), theirIdentityKey); 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(); 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.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode; 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.contacts.JsonContactsStore;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.storage.groups.JsonGroupStore; import org.asamk.signal.storage.groups.JsonGroupStore;
import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; 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.IOUtils;
import org.asamk.signal.util.Util; import org.asamk.signal.util.Util;
import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.InvalidInputException;
@ -32,6 +36,7 @@ import java.nio.channels.Channels;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.nio.channels.FileLock; import java.nio.channels.FileLock;
import java.util.Collection; import java.util.Collection;
import java.util.UUID;
public class SignalAccount { public class SignalAccount {
@ -39,6 +44,7 @@ public class SignalAccount {
private FileChannel fileChannel; private FileChannel fileChannel;
private FileLock lock; private FileLock lock;
private String username; private String username;
private UUID uuid;
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
private boolean isMultiDevice = false; private boolean isMultiDevice = false;
private String password; private String password;
@ -53,7 +59,6 @@ public class SignalAccount {
private JsonSignalProtocolStore signalProtocolStore; private JsonSignalProtocolStore signalProtocolStore;
private JsonGroupStore groupStore; private JsonGroupStore groupStore;
private JsonContactsStore contactStore; private JsonContactsStore contactStore;
private JsonThreadStore threadStore;
private SignalAccount() { private SignalAccount() {
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
@ -82,27 +87,26 @@ public class SignalAccount {
account.profileKey = profileKey; account.profileKey = profileKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.groupStore = new JsonGroupStore(); account.groupStore = new JsonGroupStore();
account.threadStore = new JsonThreadStore();
account.contactStore = new JsonContactsStore(); account.contactStore = new JsonContactsStore();
account.registered = false; account.registered = false;
return account; 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); IOUtils.createPrivateDirectories(dataPath);
SignalAccount account = new SignalAccount(); SignalAccount account = new SignalAccount();
account.openFileChannel(getFileName(dataPath, username)); account.openFileChannel(getFileName(dataPath, username));
account.username = username; account.username = username;
account.uuid = uuid;
account.password = password; account.password = password;
account.profileKey = profileKey; account.profileKey = profileKey;
account.deviceId = deviceId; account.deviceId = deviceId;
account.signalingKey = signalingKey; account.signalingKey = signalingKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.groupStore = new JsonGroupStore(); account.groupStore = new JsonGroupStore();
account.threadStore = new JsonThreadStore();
account.contactStore = new JsonContactsStore(); account.contactStore = new JsonContactsStore();
account.registered = true; account.registered = true;
account.isMultiDevice = true; account.isMultiDevice = true;
@ -138,6 +142,14 @@ public class SignalAccount {
rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel)); 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"); JsonNode node = rootNode.get("deviceId");
if (node != null) { if (node != null) {
deviceId = node.asInt(); deviceId = node.asInt();
@ -189,10 +201,27 @@ public class SignalAccount {
} }
JsonNode threadStoreNode = rootNode.get("threadStore"); JsonNode threadStoreNode = rootNode.get("threadStore");
if (threadStoreNode != null) { if (threadStoreNode != null) {
threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class); 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) {
}
} }
if (threadStore == null) {
threadStore = new JsonThreadStore();
} }
} }
@ -202,6 +231,7 @@ public class SignalAccount {
} }
ObjectNode rootNode = jsonProcessor.createObjectNode(); ObjectNode rootNode = jsonProcessor.createObjectNode();
rootNode.put("username", username) rootNode.put("username", username)
.put("uuid", uuid == null ? null : uuid.toString())
.put("deviceId", deviceId) .put("deviceId", deviceId)
.put("isMultiDevice", isMultiDevice) .put("isMultiDevice", isMultiDevice)
.put("password", password) .put("password", password)
@ -214,7 +244,6 @@ public class SignalAccount {
.putPOJO("axolotlStore", signalProtocolStore) .putPOJO("axolotlStore", signalProtocolStore)
.putPOJO("groupStore", groupStore) .putPOJO("groupStore", groupStore)
.putPOJO("contactStore", contactStore) .putPOJO("contactStore", contactStore)
.putPOJO("threadStore", threadStore)
; ;
try { try {
synchronized (fileChannel) { synchronized (fileChannel) {
@ -245,6 +274,10 @@ public class SignalAccount {
} }
} }
public void setResolver(final SignalServiceAddressResolver resolver) {
signalProtocolStore.setResolver(resolver);
}
public void addPreKeys(Collection<PreKeyRecord> records) { public void addPreKeys(Collection<PreKeyRecord> records) {
for (PreKeyRecord record : records) { for (PreKeyRecord record : records) {
signalProtocolStore.storePreKey(record.getId(), record); signalProtocolStore.storePreKey(record.getId(), record);
@ -269,16 +302,20 @@ public class SignalAccount {
return contactStore; return contactStore;
} }
public JsonThreadStore getThreadStore() {
return threadStore;
}
public String getUsername() { public String getUsername() {
return username; return username;
} }
public UUID getUuid() {
return uuid;
}
public void setUuid(final UUID uuid) {
this.uuid = uuid;
}
public SignalServiceAddress getSelfAddress() { public SignalServiceAddress getSelfAddress() {
return new SignalServiceAddress(null, username); return new SignalServiceAddress(uuid, username);
} }
public int getDeviceId() { public int getDeviceId() {

View file

@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.UUID;
public class ContactInfo { public class ContactInfo {
@JsonProperty @JsonProperty
@ -13,9 +15,15 @@ public class ContactInfo {
@JsonProperty @JsonProperty
public String number; public String number;
@JsonProperty
public UUID uuid;
@JsonProperty @JsonProperty
public String color; public String color;
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
@JsonProperty @JsonProperty
public String profileKey; public String profileKey;
@ -28,8 +36,16 @@ public class ContactInfo {
@JsonProperty(defaultValue = "false") @JsonProperty(defaultValue = "false")
public boolean archived; public boolean archived;
public ContactInfo() {
}
public ContactInfo(SignalServiceAddress address) {
this.number = address.getNumber().orNull();
this.uuid = address.getUuid().orNull();
}
@JsonIgnore @JsonIgnore
public SignalServiceAddress getAddress() { 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; package org.asamk.signal.storage.contacts;
import com.fasterxml.jackson.annotation.JsonProperty; 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.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
public class JsonContactsStore { public class JsonContactsStore {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty("contacts") @JsonProperty("contacts")
@JsonSerialize(using = JsonContactsStore.MapToListSerializer.class) private List<ContactInfo> contacts = new ArrayList<>();
@JsonDeserialize(using = ContactsDeserializer.class)
private Map<String, ContactInfo> contacts = new HashMap<>();
public void updateContact(ContactInfo contact) { 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;
}
} }
public ContactInfo getContact(String number) { contacts.add(contact);
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() { public List<ContactInfo> getContacts() {
return new ArrayList<>(contacts.values()); return new ArrayList<>(contacts);
} }
/** /**
@ -44,27 +43,4 @@ public class JsonContactsStore {
public void clear() { public void clear() {
contacts.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.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; 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 org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.UUID;
public class GroupInfo { public class GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty @JsonProperty
public final byte[] groupId; public final byte[] groupId;
@ -18,9 +32,13 @@ public class GroupInfo {
public String name; public String name;
@JsonProperty @JsonProperty
public Set<String> members = new HashSet<>(); @JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public Set<SignalServiceAddress> members = new HashSet<>();
@JsonProperty @JsonProperty
public String color; public String color;
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
@JsonProperty(defaultValue = "false") @JsonProperty(defaultValue = "false")
public boolean blocked; public boolean blocked;
@JsonProperty @JsonProperty
@ -38,7 +56,7 @@ public class GroupInfo {
this.groupId = groupId; 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.groupId = groupId;
this.name = name; this.name = name;
this.members.addAll(members); this.members.addAll(members);
@ -47,6 +65,7 @@ public class GroupInfo {
this.blocked = blocked; this.blocked = blocked;
this.inboxPosition = inboxPosition; this.inboxPosition = inboxPosition;
this.archived = archived; this.archived = archived;
this.messageExpirationTime = messageExpirationTime;
} }
@JsonIgnore @JsonIgnore
@ -56,16 +75,108 @@ public class GroupInfo {
@JsonIgnore @JsonIgnore
public Set<SignalServiceAddress> getMembers() { public Set<SignalServiceAddress> getMembers() {
Set<SignalServiceAddress> addresses = new HashSet<>(members.size()); return members;
for (String member : members) {
addresses.add(new SignalServiceAddress(null, member));
}
return addresses;
} }
public void addMembers(Collection<SignalServiceAddress> members) { @JsonIgnore
public Set<String> getMembersE164() {
Set<String> membersE164 = new HashSet<>();
for (SignalServiceAddress member : members) { 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 com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.TrustLevel; import org.asamk.signal.TrustLevel;
import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore; 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 org.whispersystems.util.Base64;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.UUID;
public class JsonIdentityKeyStore implements IdentityKeyStore { 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 IdentityKeyPair identityKeyPair;
private final int localRegistrationId; private final int localRegistrationId;
private SignalServiceAddressResolver resolver;
public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) { public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) {
this.identityKeyPair = identityKeyPair; this.identityKeyPair = identityKeyPair;
this.localRegistrationId = localRegistrationId; 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 @Override
public IdentityKeyPair getIdentityKeyPair() { public IdentityKeyPair getIdentityKeyPair() {
return identityKeyPair; return identityKeyPair;
@ -47,85 +63,116 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
@Override @Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { 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 serviceAddress User address, i.e. phone number and/or uuid
* @param identityKey The user's public key * @param identityKey The user's public key
* @param trustLevel * @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. * @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) { public boolean saveIdentity(SignalServiceAddress serviceAddress, 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) { for (Identity id : identities) {
if (!id.identityKey.equals(identityKey)) if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue; continue;
}
if (id.trustLevel.compareTo(trustLevel) < 0) { if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
id.trustLevel = trustLevel; id.address = serviceAddress;
}
if (added != null) {
id.added = added;
} }
// Identity already exists, not updating the trust level
return true; 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; 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 @Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
// TODO implement possibility for different handling of incoming/outgoing trust decisions // TODO implement possibility for different handling of incoming/outgoing trust decisions
List<Identity> identities = trustedKeys.get(address.getName()); SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
if (identities == null) { boolean trustOnFirstUse = true;
// Trust on first use
return true;
}
for (Identity id : identities) { for (Identity id : identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
if (id.identityKey.equals(identityKey)) { if (id.identityKey.equals(identityKey)) {
return id.isTrusted(); return id.isTrusted();
} else {
trustOnFirstUse = false;
} }
} }
return false; return trustOnFirstUse;
} }
@Override @Override
public IdentityKey getIdentity(SignalProtocolAddress address) { public IdentityKey getIdentity(SignalProtocolAddress address) {
List<Identity> identities = trustedKeys.get(address.getName()); SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
if (identities == null || identities.size() == 0) { Identity identity = getIdentity(serviceAddress);
return null; return identity == null ? null : identity.getIdentityKey();
} }
public Identity getIdentity(SignalServiceAddress serviceAddress) {
long maxDate = 0; long maxDate = 0;
Identity maxIdentity = null; Identity maxIdentity = null;
for (Identity id : identities) { for (Identity id : this.identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
final long time = id.getDateAdded().getTime(); final long time = id.getDateAdded().getTime();
if (maxIdentity == null || maxDate <= time) { if (maxIdentity == null || maxDate <= time) {
maxDate = time; maxDate = time;
maxIdentity = id; maxIdentity = id;
} }
} }
return maxIdentity.getIdentityKey(); return maxIdentity;
} }
public Map<String, List<Identity>> getIdentities() { public List<Identity> getIdentities() {
// TODO deep copy // TODO deep copy
return trustedKeys; return identities;
} }
public List<Identity> getIdentities(String name) { public List<Identity> getIdentities(SignalServiceAddress serviceAddress) {
// TODO deep copy List<Identity> identities = new ArrayList<>();
return trustedKeys.get(name); for (Identity identity : this.identities) {
if (identity.address.matches(serviceAddress)) {
identities.add(identity);
}
}
return identities;
} }
public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<JsonIdentityKeyStore> { public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<JsonIdentityKeyStore> {
@ -143,12 +190,26 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
JsonNode trustedKeysNode = node.get("trustedKeys"); JsonNode trustedKeysNode = node.get("trustedKeys");
if (trustedKeysNode.isArray()) { if (trustedKeysNode.isArray()) {
for (JsonNode trustedKey : trustedKeysNode) { 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 { try {
IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0); 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; 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(); 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) { } catch (InvalidKeyException | IOException e) {
System.out.println(String.format("Error while decoding key for: %s", trustedKeyName)); 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.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId());
json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize())); json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
json.writeArrayFieldStart("trustedKeys"); json.writeArrayFieldStart("trustedKeys");
for (Map.Entry<String, List<Identity>> trustedKey : jsonIdentityKeyStore.trustedKeys.entrySet()) { for (Identity trustedKey : jsonIdentityKeyStore.identities) {
for (Identity id : trustedKey.getValue()) {
json.writeStartObject(); json.writeStartObject();
json.writeStringField("name", trustedKey.getKey()); if (trustedKey.getAddress().getNumber().isPresent()) {
json.writeStringField("identityKey", Base64.encodeBytes(id.identityKey.serialize())); json.writeStringField("name", trustedKey.getAddress().getNumber().get());
json.writeNumberField("trustLevel", id.trustLevel.ordinal());
json.writeNumberField("addedTimestamp", id.added.getTime());
json.writeEndObject();
} }
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.writeEndArray();
json.writeEndObject(); json.writeEndObject();
@ -187,22 +251,33 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
public static class Identity { public static class Identity {
SignalServiceAddress address;
IdentityKey identityKey; IdentityKey identityKey;
TrustLevel trustLevel; TrustLevel trustLevel;
Date added; Date added;
public Identity(IdentityKey identityKey, TrustLevel trustLevel) { public Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) {
this.address = address;
this.identityKey = identityKey; this.identityKey = identityKey;
this.trustLevel = trustLevel; this.trustLevel = trustLevel;
this.added = new Date(); 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.identityKey = identityKey;
this.trustLevel = trustLevel; this.trustLevel = trustLevel;
this.added = added; this.added = added;
} }
public SignalServiceAddress getAddress() {
return address;
}
public void setAddress(final SignalServiceAddress address) {
this.address = address;
}
boolean isTrusted() { boolean isTrusted() {
return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || return trustLevel == TrustLevel.TRUSTED_UNVERIFIED ||
trustLevel == TrustLevel.TRUSTED_VERIFIED; 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.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore; 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 org.whispersystems.util.Base64;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.UUID;
class JsonSessionStore implements SessionStore { class JsonSessionStore implements SessionStore {
private final Map<SignalProtocolAddress, byte[]> sessions = new HashMap<>(); private final List<SessionInfo> sessions = new ArrayList<>();
private SignalServiceAddressResolver resolver;
public JsonSessionStore() { public JsonSessionStore() {
} }
private void addSessions(Map<SignalProtocolAddress, byte[]> sessions) { public void setResolver(final SignalServiceAddressResolver resolver) {
this.sessions.putAll(sessions); this.resolver = resolver;
}
private SignalServiceAddress resolveSignalServiceAddress(String identifier) {
if (resolver != null) {
return resolver.resolveSignalServiceAddress(identifier);
} else {
return Util.getSignalServiceAddressFromIdentifier(identifier);
}
} }
@Override @Override
public synchronized SessionRecord loadSession(SignalProtocolAddress remoteAddress) { 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 { try {
if (containsSession(remoteAddress)) { return new SessionRecord(info.sessionRecord);
return new SessionRecord(sessions.get(remoteAddress));
} else {
return new SessionRecord();
}
} catch (IOException e) { } catch (IOException e) {
throw new AssertionError(e); System.err.println("Failed to load session, resetting session: " + e);
final SessionRecord sessionRecord = new SessionRecord();
info.sessionRecord = sessionRecord.serialize();
return sessionRecord;
} }
} }
}
return new SessionRecord();
}
@Override @Override
public synchronized List<Integer> getSubDeviceSessions(String name) { public synchronized List<Integer> getSubDeviceSessions(String name) {
List<Integer> deviceIds = new LinkedList<>(); SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name);
for (SignalProtocolAddress key : sessions.keySet()) { List<Integer> deviceIds = new LinkedList<>();
if (key.getName().equals(name) && for (SessionInfo info : sessions) {
key.getDeviceId() != 1) { if (info.address.matches(serviceAddress) && info.deviceId != 1) {
deviceIds.add(key.getDeviceId()); deviceIds.add(info.deviceId);
} }
} }
@ -61,26 +78,45 @@ class JsonSessionStore implements SessionStore {
@Override @Override
public synchronized void storeSession(SignalProtocolAddress address, SessionRecord record) { 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 @Override
public synchronized boolean containsSession(SignalProtocolAddress address) { 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 @Override
public synchronized void deleteSession(SignalProtocolAddress address) { 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 @Override
public synchronized void deleteAllSessions(String name) { public synchronized void deleteAllSessions(String name) {
for (SignalProtocolAddress key : new ArrayList<>(sessions.keySet())) { SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name);
if (key.getName().equals(name)) { deleteAllSessions(serviceAddress);
sessions.remove(key);
}
} }
public synchronized void deleteAllSessions(SignalServiceAddress serviceAddress) {
sessions.removeIf(info -> info.address.matches(serviceAddress));
} }
public static class JsonSessionStoreDeserializer extends JsonDeserializer<JsonSessionStore> { public static class JsonSessionStoreDeserializer extends JsonDeserializer<JsonSessionStore> {
@ -89,23 +125,36 @@ class JsonSessionStore implements SessionStore {
public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser); JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Map<SignalProtocolAddress, byte[]> sessionMap = new HashMap<>(); JsonSessionStore sessionStore = new JsonSessionStore();
if (node.isArray()) { if (node.isArray()) {
for (JsonNode session : node) { 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 { 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) { } catch (IOException e) {
System.out.println(String.format("Error while decoding session for: %s", sessionName)); System.out.println(String.format("Error while decoding session for: %s", sessionName));
} }
} }
} }
JsonSessionStore sessionStore = new JsonSessionStore();
sessionStore.addSessions(sessionMap);
return sessionStore; return sessionStore;
} }
} }
@ -114,14 +163,20 @@ class JsonSessionStore implements SessionStore {
@Override @Override
public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
json.writeStartArray(); json.writeStartArray();
for (Map.Entry<SignalProtocolAddress, byte[]> preKey : jsonSessionStore.sessions.entrySet()) { for (SessionInfo sessionInfo : jsonSessionStore.sessions) {
json.writeStartObject(); json.writeStartObject();
json.writeStringField("name", preKey.getKey().getName()); if (sessionInfo.address.getNumber().isPresent()) {
json.writeNumberField("deviceId", preKey.getKey().getDeviceId()); json.writeStringField("name", sessionInfo.address.getNumber().get());
json.writeStringField("record", Base64.encodeBytes(preKey.getValue())); }
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.writeEndObject();
} }
json.writeEndArray(); 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.SessionRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List; import java.util.List;
import java.util.Map;
public class JsonSignalProtocolStore implements SignalProtocolStore { public class JsonSignalProtocolStore implements SignalProtocolStore {
@ -56,6 +56,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId); this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId);
} }
public void setResolver(final SignalServiceAddressResolver resolver) {
sessionStore.setResolver(resolver);
identityKeyStore.setResolver(resolver);
}
@Override @Override
public IdentityKeyPair getIdentityKeyPair() { public IdentityKeyPair getIdentityKeyPair() {
return identityKeyStore.getIdentityKeyPair(); return identityKeyStore.getIdentityKeyPair();
@ -71,16 +76,20 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
return identityKeyStore.saveIdentity(address, identityKey); return identityKeyStore.saveIdentity(address, identityKey);
} }
public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) { public void saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
identityKeyStore.saveIdentity(name, identityKey, trustLevel, null); 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(); return identityKeyStore.getIdentities();
} }
public List<JsonIdentityKeyStore.Identity> getIdentities(String name) { public List<JsonIdentityKeyStore.Identity> getIdentities(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentities(name); return identityKeyStore.getIdentities(serviceAddress);
} }
@Override @Override
@ -93,6 +102,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
return identityKeyStore.getIdentity(address); return identityKeyStore.getIdentity(address);
} }
public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentity(serviceAddress);
}
@Override @Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
return preKeyStore.loadPreKey(preKeyId); return preKeyStore.loadPreKey(preKeyId);
@ -143,6 +156,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
sessionStore.deleteAllSessions(name); sessionStore.deleteAllSessions(name);
} }
public void deleteAllSessions(SignalServiceAddress serviceAddress) {
sessionStore.deleteAllSessions(serviceAddress);
}
@Override @Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
return signedPreKeyStore.loadSignedPreKey(signedPreKeyId); 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.List;
import java.util.Map; import java.util.Map;
public class JsonThreadStore { public class LegacyJsonThreadStore {
private static final ObjectMapper jsonProcessor = new ObjectMapper(); private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty("threads") @JsonProperty("threads")
@JsonSerialize(using = JsonThreadStore.MapToListSerializer.class) @JsonSerialize(using = MapToListSerializer.class)
@JsonDeserialize(using = ThreadsDeserializer.class) @JsonDeserialize(using = ThreadsDeserializer.class)
private Map<String, ThreadInfo> threads = new HashMap<>(); 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() { public List<ThreadInfo> getThreads() {
return new ArrayList<>(threads.values()); 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.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException; import java.io.IOException;
@ -59,4 +60,10 @@ public class ErrorUtils {
System.err.println(e.getMessage()); System.err.println(e.getMessage());
System.err.println("Aborting sending."); 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() { 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) { public static String toStringCondensed(byte[] bytes) {
StringBuffer buf = new StringBuffer(); StringBuffer buf = new StringBuffer();
for (final byte aByte : bytes) { for (final byte aByte : bytes) {
@ -20,7 +29,6 @@ public class Hex {
private static void appendHexChar(StringBuffer buf, int b) { private static void appendHexChar(StringBuffer buf, int b) {
buf.append(HEX_DIGITS[(b >> 4) & 0xf]); buf.append(HEX_DIGITS[(b >> 4) & 0xf]);
buf.append(HEX_DIGITS[b & 0xf]); buf.append(HEX_DIGITS[b & 0xf]);
buf.append(" ");
} }
public static byte[] toByteArray(String s) { public static byte[] toByteArray(String s) {

View file

@ -1,5 +1,8 @@
package org.asamk.signal.util; package org.asamk.signal.util;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -35,6 +38,12 @@ public class IOUtils {
return output.toString(); 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 { public static void createPrivateDirectories(String directoryPath) throws IOException {
final File file = new File(directoryPath); final File file = new File(directoryPath);
if (file.exists()) { if (file.exists()) {

View file

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

View file

@ -3,6 +3,10 @@ package org.asamk.signal.util;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.asamk.signal.GroupIdFormatException; 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 org.whispersystems.util.Base64;
import java.io.IOException; import java.io.IOException;
@ -51,4 +55,16 @@ public class Util {
throw new GroupIdFormatException(groupId, e); 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);
}
}
} }