Merge remote-tracking branch 'upstream/master' into stdio

This commit is contained in:
technillogue 2020-12-29 19:26:44 -05:00
commit 6d18f311e6
97 changed files with 4346 additions and 1379 deletions

View file

@ -13,13 +13,19 @@ import java.util.List;
*/
public interface Signal extends DBusInterface {
long sendMessage(String message, List<String> attachments, String recipient) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
long sendMessage(
String message, List<String> attachments, String recipient
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
long sendMessage(String message, List<String> attachments, List<String> recipients) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
long sendMessage(
String message, List<String> attachments, List<String> recipients
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
void sendEndSessionMessage(List<String> recipients) throws Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
long sendGroupMessage(String message, List<String> attachments, byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid, Error.UnregisteredUser, Error.UntrustedIdentity;
long sendGroupMessage(
String message, List<String> attachments, byte[] groupId
) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid, Error.UnregisteredUser, Error.UntrustedIdentity;
String getContactName(String number) throws Error.InvalidNumber;
@ -35,7 +41,9 @@ public interface Signal extends DBusInterface {
List<String> getGroupMembers(byte[] groupId);
byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.UnregisteredUser, Error.UntrustedIdentity;
byte[] updateGroup(
byte[] groupId, String name, List<String> members, String avatar
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.UnregisteredUser, Error.UntrustedIdentity;
boolean isRegistered();
@ -47,7 +55,14 @@ public interface Signal extends DBusInterface {
private final String message;
private final List<String> attachments;
public MessageReceived(String objectpath, long timestamp, String sender, byte[] groupId, String message, List<String> attachments) throws DBusException {
public MessageReceived(
String objectpath,
long timestamp,
String sender,
byte[] groupId,
String message,
List<String> attachments
) throws DBusException {
super(objectpath, timestamp, sender, groupId, message, attachments);
this.timestamp = timestamp;
this.sender = sender;
@ -106,7 +121,15 @@ public interface Signal extends DBusInterface {
private final String message;
private final List<String> attachments;
public SyncMessageReceived(String objectpath, long timestamp, String source, String destination, byte[] groupId, String message, List<String> attachments) throws DBusException {
public SyncMessageReceived(
String objectpath,
long timestamp,
String source,
String destination,
byte[] groupId,
String message,
List<String> attachments
) throws DBusException {
super(objectpath, timestamp, source, destination, groupId, message, attachments);
this.timestamp = timestamp;
this.source = source;

View file

@ -1,6 +1,7 @@
package org.asamk.signal;
import org.asamk.Signal;
import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.manager.Manager;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
@ -29,30 +30,33 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
this.objectPath = objectPath;
}
static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, final String objectPath, Manager m) {
static void sendReceivedMessageToDbus(
SignalServiceEnvelope envelope,
SignalServiceContent content,
DBusConnection conn,
final String objectPath,
Manager m
) {
if (envelope.isReceipt()) {
try {
conn.sendMessage(new Signal.ReceiptReceived(
objectPath,
envelope.getTimestamp(),
conn.sendMessage(new Signal.ReceiptReceived(objectPath, envelope.getTimestamp(),
// A receipt envelope always has a source address
envelope.getSourceAddress().getLegacyIdentifier()
));
envelope.getSourceAddress().getLegacyIdentifier()));
} catch (DBusException e) {
e.printStackTrace();
}
} else if (content != null) {
final SignalServiceAddress sender = !envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceAddress() : content.getSender();
final SignalServiceAddress sender = !envelope.isUnidentifiedSender() && envelope.hasSource()
? envelope.getSourceAddress()
: content.getSender();
if (content.getReceiptMessage().isPresent()) {
final SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get();
if (receiptMessage.isDeliveryReceipt()) {
for (long timestamp : receiptMessage.getTimestamps()) {
try {
conn.sendMessage(new Signal.ReceiptReceived(
objectPath,
conn.sendMessage(new Signal.ReceiptReceived(objectPath,
timestamp,
sender.getLegacyIdentifier()
));
sender.getLegacyIdentifier()));
} catch (DBusException e) {
e.printStackTrace();
}
@ -61,16 +65,17 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
if (!message.isEndSession() &&
!(message.getGroupContext().isPresent() &&
message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) {
byte[] groupId = getGroupId(message);
if (!message.isEndSession() && (
groupId == null
|| message.getGroupContext().get().getGroupV1Type() == null
|| message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.DELIVER
)) {
try {
conn.sendMessage(new Signal.MessageReceived(
objectPath,
conn.sendMessage(new Signal.MessageReceived(objectPath,
message.getTimestamp(),
sender.getLegacyIdentifier(),
message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()
? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0],
groupId != null ? groupId : new byte[0],
message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m)));
} catch (DBusException e) {
@ -82,17 +87,20 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
if (sync_message.getSent().isPresent()) {
SentTranscriptMessage transcript = sync_message.getSent().get();
if (transcript.getDestination().isPresent() || transcript.getMessage().getGroupContext().isPresent()) {
if (transcript.getDestination().isPresent() || transcript.getMessage()
.getGroupContext()
.isPresent()) {
SignalServiceDataMessage message = transcript.getMessage();
byte[] groupId = getGroupId(message);
try {
conn.sendMessage(new Signal.SyncMessageReceived(
objectPath,
conn.sendMessage(new Signal.SyncMessageReceived(objectPath,
transcript.getTimestamp(),
sender.getLegacyIdentifier(),
transcript.getDestination().isPresent() ? transcript.getDestination().get().getLegacyIdentifier() : "",
message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()
? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0],
transcript.getDestination().isPresent() ? transcript.getDestination()
.get()
.getLegacyIdentifier() : "",
groupId != null ? groupId : new byte[0],
message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m)));
} catch (DBusException e) {
@ -104,6 +112,11 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
}
}
private static byte[] getGroupId(final SignalServiceDataMessage message) {
return message.getGroupContext().isPresent() ? GroupUtils.getGroupId(message.getGroupContext().get())
.serialize() : null;
}
static private List<String> getAttachments(SignalServiceDataMessage message, Manager m) {
List<String> attachments = new ArrayList<>();
if (message.getAttachments().isPresent()) {

View file

@ -35,7 +35,7 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler
result.putPOJO("error", new JsonError(exception));
}
if (envelope != null) {
result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content));
result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content, m));
}
try {
jsonProcessor.writeValue(System.out, result);

View file

@ -41,6 +41,8 @@ import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
@ -50,10 +52,10 @@ import java.io.IOException;
import java.security.Security;
import java.util.Map;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public class Main {
final static Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
installSecurityProviderWorkaround();
@ -62,7 +64,7 @@ public class Main {
System.exit(1);
}
int res = handleCommands(ns);
int res = init(ns);
System.exit(res);
}
@ -72,71 +74,81 @@ public class Main {
Security.addProvider(new BouncyCastleProvider());
}
private static int handleCommands(Namespace ns) {
public static int init(Namespace ns) {
if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
return initDbusClient(ns, ns.getBoolean("dbus_system"));
}
final String username = ns.getString("username");
if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
try {
DBusConnection.DBusBusType busType;
if (ns.getBoolean("dbus_system")) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
}
try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
Signal ts = dBusConn.getRemoteObject(
DbusConfig.SIGNAL_BUSNAME, DbusConfig.SIGNAL_OBJECTPATH,
Signal.class);
return handleCommands(ns, ts, dBusConn);
}
} catch (UnsatisfiedLinkError e) {
System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
return 1;
} catch (DBusException | IOException e) {
e.printStackTrace();
return 3;
}
final File dataPath;
String config = ns.getString("config");
if (config != null) {
dataPath = new File(config);
} else {
String dataPath = ns.getString("config");
if (isEmpty(dataPath)) {
dataPath = getDefaultDataPath();
}
dataPath = getDefaultDataPath();
}
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT);
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(
BaseConfig.USER_AGENT);
if (username == null) {
ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
return handleCommands(ns, pm);
}
if (!ServiceConfig.getCapabilities().isGv2()) {
logger.warn("WARNING: Support for new group V2 is disabled,"
+ " because the required native library dependency is missing: libzkgroup");
}
Manager manager;
if (username == null) {
ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
return handleCommands(ns, pm);
}
Manager manager;
try {
manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
} catch (Throwable e) {
logger.error("Error loading state file: {}", e.getMessage());
return 2;
}
try (Manager m = manager) {
try {
manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
} catch (Throwable e) {
System.err.println("Error loading state file: " + e.getMessage());
m.checkAccountState();
} 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 (IOException e) {
logger.error("Error while checking account: {}", e.getMessage());
return 2;
}
try (Manager m = manager) {
try {
m.checkAccountState();
} 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 (IOException e) {
System.err.println("Error while checking account: " + e.getMessage());
return 2;
}
return handleCommands(ns, m);
} catch (IOException e) {
logger.error("Cleanup failed", e);
return 3;
}
}
return handleCommands(ns, m);
} catch (IOException e) {
e.printStackTrace();
return 3;
private static int initDbusClient(final Namespace ns, final boolean systemBus) {
try {
DBusConnection.DBusBusType busType;
if (systemBus) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
}
try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME,
DbusConfig.SIGNAL_OBJECTPATH,
Signal.class);
return handleCommands(ns, ts, dBusConn);
}
} catch (DBusException | IOException e) {
logger.error("Dbus client failed", e);
return 3;
}
}
@ -199,19 +211,21 @@ public class Main {
*
* @return the data directory to be used by signal-cli.
*/
private static String getDefaultDataPath() {
String dataPath = IOUtils.getDataHomeDir() + "/signal-cli";
if (new File(dataPath).exists()) {
private static File getDefaultDataPath() {
File dataPath = new File(IOUtils.getDataHomeDir(), "signal-cli");
if (dataPath.exists()) {
return dataPath;
}
String legacySettingsPath = System.getProperty("user.home") + "/.config/signal";
if (new File(legacySettingsPath).exists()) {
File configPath = new File(System.getProperty("user.home"), ".config");
File legacySettingsPath = new File(configPath, "signal");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure";
if (new File(legacySettingsPath).exists()) {
legacySettingsPath = new File(configPath, "textsecure");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
@ -219,39 +233,7 @@ public class Main {
}
private static Namespace parseArgs(String[] args) {
ArgumentParser parser = ArgumentParsers.newFor("signal-cli")
.build()
.defaultHelp(true)
.description("Commandline interface for Signal, patched to support sending messages from stdin and outputing reactions.")
.version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
parser.addArgument("-v", "--version")
.help("Show package version.")
.action(Arguments.version());
parser.addArgument("--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("-u", "--username")
.help("Specify your phone number, that will be used for verification.");
mut.addArgument("--dbus")
.help("Make request via user dbus.")
.action(Arguments.storeTrue());
mut.addArgument("--dbus-system")
.help("Make request via system dbus.")
.action(Arguments.storeTrue());
Subparsers subparsers = parser.addSubparsers()
.title("subcommands")
.dest("command")
.description("valid subcommands")
.help("additional help");
final Map<String, Command> commands = Commands.getCommands();
for (Map.Entry<String, Command> entry : commands.entrySet()) {
Subparser subparser = subparsers.addParser(entry.getKey());
entry.getValue().attachToSubparser(subparser);
}
ArgumentParser parser = buildArgumentParser();
Namespace ns;
try {
@ -284,4 +266,34 @@ public class Main {
}
return ns;
}
private static ArgumentParser buildArgumentParser() {
ArgumentParser parser = ArgumentParsers.newFor("signal-cli")
.build()
.defaultHelp(true)
.description("Commandline interface for Signal.")
.version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
parser.addArgument("--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification.");
mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue());
mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue());
Subparsers subparsers = parser.addSubparsers()
.title("subcommands")
.dest("command")
.description("valid subcommands")
.help("additional help");
final Map<String, Command> commands = Commands.getCommands();
for (Map.Entry<String, Command> entry : commands.entrySet()) {
Subparser subparser = subparsers.addParser(entry.getKey());
entry.getValue().attachToSubparser(subparser);
}
return parser;
}
}

View file

@ -1,5 +1,7 @@
package org.asamk.signal;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.groups.GroupInfo;
@ -11,6 +13,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
@ -22,6 +26,8 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
@ -48,7 +54,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
SignalServiceAddress source = envelope.getSourceAddress();
ContactInfo sourceContact = m.getContact(source.getLegacyIdentifier());
System.out.println(String.format("Envelope from: %s (device: %d)", (sourceContact == null ? "" : "" + sourceContact.name + "") + source.getLegacyIdentifier(), envelope.getSourceDevice()));
System.out.println(String.format("Envelope from: %s (device: %d)",
(sourceContact == null ? "" : "" + sourceContact.name + "") + source.getLegacyIdentifier(),
envelope.getSourceDevice()));
if (source.getRelay().isPresent()) {
System.out.println("Relayed by: " + source.getRelay().get());
}
@ -66,18 +74,35 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (exception != null) {
if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
org.whispersystems.libsignal.UntrustedIdentityException e = (org.whispersystems.libsignal.UntrustedIdentityException) 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.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(
"The users key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
System.out.println("Use 'signal-cli -u "
+ m.getUsername()
+ " listIdentities -n "
+ e.getName()
+ "', verify the key and run 'signal-cli -u "
+ m.getUsername()
+ " trust -v \"FINGER_PRINT\" "
+ e.getName()
+ "' to mark it as trusted");
System.out.println("If you don't care about security, use 'signal-cli -u "
+ m.getUsername()
+ " trust -a "
+ e.getName()
+ "' to trust it without verification");
} else {
System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")");
System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass()
.getSimpleName() + ")");
}
}
if (content == null) {
System.out.println("Failed to decrypt message.");
} else {
ContactInfo sourceContact = m.getContact(content.getSender().getLegacyIdentifier());
System.out.println(String.format("Sender: %s (device: %d)", (sourceContact == null ? "" : "" + sourceContact.name + "") + content.getSender().getLegacyIdentifier(), content.getSenderDevice()));
System.out.println(String.format("Sender: %s (device: %d)",
(sourceContact == null ? "" : "" + sourceContact.name + "") + content.getSender()
.getLegacyIdentifier(),
content.getSenderDevice()));
if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
handleSignalServiceDataMessage(message);
@ -103,7 +128,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Received sync read messages list");
for (ReadMessage rm : syncMessage.getRead().get()) {
ContactInfo fromContact = m.getContact(rm.getSender().getLegacyIdentifier());
System.out.println("From: " + (fromContact == null ? "" : "" + fromContact.name + "") + rm.getSender().getLegacyIdentifier() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp()));
System.out.println("From: "
+ (fromContact == null ? "" : "" + fromContact.name + "")
+ rm.getSender().getLegacyIdentifier()
+ " Message timestamp: "
+ DateUtils.formatTimestamp(rm.getTimestamp()));
}
}
if (syncMessage.getRequest().isPresent()) {
@ -136,15 +165,19 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
StringBuilder toBuilder = new StringBuilder();
for (SignalServiceAddress dest : sentTranscriptMessage.getRecipients()) {
ContactInfo destContact = m.getContact(dest.getLegacyIdentifier());
toBuilder.append(destContact == null ? "" : "" + destContact.name + "").append(dest.getLegacyIdentifier()).append(" ");
toBuilder.append(destContact == null ? "" : "" + destContact.name + "")
.append(dest.getLegacyIdentifier())
.append(" ");
}
to = toBuilder.toString();
} else {
to = "Unknown";
}
System.out.println("To: " + to + " , Message timestamp: " + DateUtils.formatTimestamp(sentTranscriptMessage.getTimestamp()));
System.out.println("To: " + to + " , Message timestamp: " + DateUtils.formatTimestamp(
sentTranscriptMessage.getTimestamp()));
if (sentTranscriptMessage.getExpirationStartTimestamp() > 0) {
System.out.println("Expiration started at: " + DateUtils.formatTimestamp(sentTranscriptMessage.getExpirationStartTimestamp()));
System.out.println("Expiration started at: " + DateUtils.formatTimestamp(
sentTranscriptMessage.getExpirationStartTimestamp()));
}
SignalServiceDataMessage message = sentTranscriptMessage.getMessage();
handleSignalServiceDataMessage(message);
@ -160,15 +193,38 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (syncMessage.getVerified().isPresent()) {
System.out.println("Received sync message with verified identities:");
final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified());
String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey()));
System.out.println(" - "
+ verifiedMessage.getDestination()
+ ": "
+ verifiedMessage.getVerified());
String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(),
verifiedMessage.getIdentityKey()));
System.out.println(" " + safetyNumber);
}
if (syncMessage.getConfiguration().isPresent()) {
System.out.println("Received sync message with configuration:");
final ConfigurationMessage configurationMessage = syncMessage.getConfiguration().get();
if (configurationMessage.getReadReceipts().isPresent()) {
System.out.println(" - Read receipts: " + (configurationMessage.getReadReceipts().get() ? "enabled" : "disabled"));
System.out.println(" - Read receipts: " + (
configurationMessage.getReadReceipts().get() ? "enabled" : "disabled"
));
}
if (configurationMessage.getLinkPreviews().isPresent()) {
System.out.println(" - Link previews: " + (
configurationMessage.getLinkPreviews().get() ? "enabled" : "disabled"
));
}
if (configurationMessage.getTypingIndicators().isPresent()) {
System.out.println(" - Typing indicators: " + (
configurationMessage.getTypingIndicators().get() ? "enabled" : "disabled"
));
}
if (configurationMessage.getUnidentifiedDeliveryIndicators().isPresent()) {
System.out.println(" - Unidentified Delivery Indicators: " + (
configurationMessage.getUnidentifiedDeliveryIndicators().get()
? "enabled"
: "disabled"
));
}
}
if (syncMessage.getFetchType().isPresent()) {
@ -182,7 +238,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println(" - Timestamp:" + viewOnceOpenMessage.getTimestamp());
}
if (syncMessage.getStickerPackOperations().isPresent()) {
final List<StickerPackOperationMessage> stickerPackOperationMessages = syncMessage.getStickerPackOperations().get();
final List<StickerPackOperationMessage> stickerPackOperationMessages = syncMessage.getStickerPackOperations()
.get();
System.out.println("Received sync message with sticker pack operations:");
for (StickerPackOperationMessage m : stickerPackOperationMessages) {
System.out.println(" - " + m.getType().toString());
@ -194,6 +251,32 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
}
}
if (syncMessage.getMessageRequestResponse().isPresent()) {
final MessageRequestResponseMessage requestResponseMessage = syncMessage.getMessageRequestResponse()
.get();
System.out.println("Received message request response:");
System.out.println(" Type: " + requestResponseMessage.getType());
if (requestResponseMessage.getGroupId().isPresent()) {
System.out.println(" Group id: " + Base64.encodeBytes(requestResponseMessage.getGroupId()
.get()));
}
if (requestResponseMessage.getPerson().isPresent()) {
System.out.println(" Person: " + requestResponseMessage.getPerson()
.get()
.getLegacyIdentifier());
}
}
if (syncMessage.getKeys().isPresent()) {
final KeysMessage keysMessage = syncMessage.getKeys().get();
System.out.println("Received sync message with keys:");
if (keysMessage.getStorageService().isPresent()) {
System.out.println(" With storage key length: " + keysMessage.getStorageService()
.get()
.serialize().length);
} else {
System.out.println(" With empty storage key");
}
}
}
if (content.getCallMessage().isPresent()) {
System.out.println("Received a call message");
@ -213,7 +296,10 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (callMessage.getIceUpdateMessages().isPresent()) {
List<IceUpdateMessage> iceUpdateMessages = callMessage.getIceUpdateMessages().get();
for (IceUpdateMessage iceUpdateMessage : iceUpdateMessages) {
System.out.println("Ice update message: " + iceUpdateMessage.getId() + ", sdp: " + iceUpdateMessage.getSdp());
System.out.println("Ice update message: "
+ iceUpdateMessage.getId()
+ ", sdp: "
+ iceUpdateMessage.getSdp());
}
}
if (callMessage.getOfferMessage().isPresent()) {
@ -242,11 +328,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println(" - Action: " + typingMessage.getAction());
System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp()));
if (typingMessage.getGroupId().isPresent()) {
GroupInfo group = m.getGroup(typingMessage.getGroupId().get());
System.out.println(" - Group Info:");
final GroupId groupId = GroupId.unknownVersion(typingMessage.getGroupId().get());
System.out.println(" Id: " + groupId.toBase64());
GroupInfo group = m.getGroup(groupId);
if (group != null) {
System.out.println(" Name: " + group.name);
System.out.println(" Name: " + group.getTitle());
} else {
System.out.println(" Name: <Unknown group>");
System.out.println(" Name: <Unknown group>");
}
}
}
@ -259,38 +348,57 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
private void handleSignalServiceDataMessage(SignalServiceDataMessage message) {
System.out.println("Message timestamp: " + DateUtils.formatTimestamp(message.getTimestamp()));
if (message.isViewOnce()) {
System.out.println("=VIEW ONCE=");
}
if (message.getBody().isPresent()) {
System.out.println("Body: " + message.getBody().get());
}
if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
if (message.getGroupContext().isPresent()) {
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
System.out.println(" Name: " + groupInfo.getName().get());
} else {
GroupInfo group = m.getGroup(groupInfo.getGroupId());
final SignalServiceGroupContext groupContext = message.getGroupContext().get();
final GroupId groupId = GroupUtils.getGroupId(groupContext);
if (groupContext.getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = groupContext.getGroupV1().get();
System.out.println(" Id: " + groupId.toBase64());
if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
System.out.println(" Name: " + groupInfo.getName().get());
} else {
GroupInfo group = m.getGroup(groupId);
if (group != null) {
System.out.println(" Name: " + group.getTitle());
} else {
System.out.println(" Name: <Unknown group>");
}
}
System.out.println(" Type: " + groupInfo.getType());
if (groupInfo.getMembers().isPresent()) {
for (SignalServiceAddress member : groupInfo.getMembers().get()) {
System.out.println(" Member: " + member.getLegacyIdentifier());
}
}
if (groupInfo.getAvatar().isPresent()) {
System.out.println(" Avatar:");
printAttachment(groupInfo.getAvatar().get());
}
} else if (groupContext.getGroupV2().isPresent()) {
final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get();
System.out.println(" Id: " + groupId.toBase64());
GroupInfo group = m.getGroup(groupId);
if (group != null) {
System.out.println(" Name: " + group.name);
System.out.println(" Name: " + group.getTitle());
} else {
System.out.println(" Name: <Unknown group>");
}
}
System.out.println(" Type: " + groupInfo.getType());
if (groupInfo.getMembers().isPresent()) {
for (SignalServiceAddress member : groupInfo.getMembers().get()) {
System.out.println(" Member: " + member.getLegacyIdentifier());
}
}
if (groupInfo.getAvatar().isPresent()) {
System.out.println(" Avatar:");
printAttachment(groupInfo.getAvatar().get());
System.out.println(" Revision: " + groupInfo.getRevision());
System.out.println(" Master key length: " + groupInfo.getMasterKey().serialize().length);
System.out.println(" Has signed group change: " + groupInfo.hasSignedGroupChange());
}
}
if (message.getPreviews().isPresent()) {
final List<SignalServiceDataMessage.Preview> previews = message.getPreviews().get();
System.out.println("Previes:");
System.out.println("Previews:");
for (SignalServiceDataMessage.Preview preview : previews) {
System.out.println(" - Title: " + preview.getTitle());
System.out.println(" - Url: " + preview.getUrl());
@ -332,7 +440,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
final SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
System.out.println("Reaction:");
System.out.println(" - Emoji: " + reaction.getEmoji());
System.out.println(" - Target author: " + reaction.getTargetAuthor().getLegacyIdentifier()); // todo resolve
System.out.println(" - Target author: " + m.resolveSignalServiceAddress(reaction.getTargetAuthor())
.getLegacyIdentifier());
System.out.println(" - Target timestamp: " + reaction.getTargetSentTimestamp());
System.out.println(" - Is remove: " + reaction.isRemove());
}
@ -340,14 +449,20 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (message.getQuote().isPresent()) {
SignalServiceDataMessage.Quote quote = message.getQuote().get();
System.out.println("Quote: (" + quote.getId() + ")");
System.out.println(" Author: " + quote.getAuthor().getLegacyIdentifier());
System.out.println(" Author: " + m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier());
System.out.println(" Text: " + quote.getText());
if (quote.getMentions() != null && quote.getMentions().size() > 0) {
System.out.println(" Mentions: ");
for (SignalServiceDataMessage.Mention mention : quote.getMentions()) {
printMention(mention, m);
}
}
if (quote.getAttachments().size() > 0) {
System.out.println(" Attachments: ");
for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : quote.getAttachments()) {
System.out.println(" Filename: " + attachment.getFileName());
System.out.println(" Type: " + attachment.getContentType());
System.out.println(" Thumbnail:");
System.out.println(" - Filename: " + attachment.getFileName());
System.out.println(" Type: " + attachment.getContentType());
System.out.println(" Thumbnail:");
if (attachment.getThumbnail() != null) {
printAttachment(attachment.getThumbnail());
}
@ -355,6 +470,17 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
}
if (message.getRemoteDelete().isPresent()) {
final SignalServiceDataMessage.RemoteDelete remoteDelete = message.getRemoteDelete().get();
System.out.println("Remote delete message: timestamp = " + remoteDelete.getTargetSentTimestamp());
}
if (message.getMentions().isPresent()) {
System.out.println("Mentions: ");
for (SignalServiceDataMessage.Mention mention : message.getMentions().get()) {
printMention(mention, m);
}
}
if (message.getAttachments().isPresent()) {
System.out.println("Attachments: ");
for (SignalServiceAttachment attachment : message.getAttachments().get()) {
@ -363,13 +489,28 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
}
private void printMention(SignalServiceDataMessage.Mention mention, Manager m) {
System.out.println("- " + m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null))
.getLegacyIdentifier() + ": " + mention.getStart() + " (length: " + mention.getLength() + ")");
}
private void printAttachment(SignalServiceAttachment attachment) {
System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (
attachment.isStream() ? "Stream" : ""
) + ")");
if (attachment.isPointer()) {
final SignalServiceAttachmentPointer pointer = attachment.asPointer();
System.out.println(" Id: " + pointer.getRemoteId() + " Key length: " + pointer.getKey().length);
System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-"));
System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
System.out.println(" Filename: " + (
pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-"
));
System.out.println(" Size: " + (
pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>"
) + (
pointer.getPreview().isPresent() ? " (Preview is available: "
+ pointer.getPreview().get().length
+ " bytes)" : ""
));
System.out.println(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no"));
System.out.println(" Dimensions: " + pointer.getWidth() + "x" + pointer.getHeight());
File file = m.getAttachmentFile(pointer.getRemoteId());

View file

@ -3,9 +3,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
@ -13,12 +14,8 @@ public class BlockCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("contact")
.help("Contact number")
.nargs("*");
subparser.addArgument("-g", "--group")
.help("Group ID")
.nargs("*");
subparser.addArgument("contact").help("Contact number").nargs("*");
subparser.addArgument("-g", "--group").help("Group ID").nargs("*");
subparser.help("Block the given contacts or groups (no messages will be received)");
}
@ -40,7 +37,7 @@ public class BlockCommand implements LocalCommand {
if (ns.<String>getList("group") != null) {
for (String groupIdString : ns.<String>getList("group")) {
try {
byte[] groupId = Util.decodeGroupId(groupIdString);
GroupId groupId = Util.decodeGroupId(groupIdString);
m.setGroupBlocked(groupId, true);
} catch (GroupIdFormatException | GroupNotFoundException e) {
System.err.println(e.getMessage());

View file

@ -17,6 +17,7 @@ public class Commands {
addCommand("listDevices", new ListDevicesCommand());
addCommand("listGroups", new ListGroupsCommand());
addCommand("listIdentities", new ListIdentitiesCommand());
addCommand("joinGroup", new JoinGroupCommand());
addCommand("quitGroup", new QuitGroupCommand());
addCommand("receive", new ReceiveCommand());
addCommand("register", new RegisterCommand());

View file

@ -60,7 +60,13 @@ public class DaemonCommand implements LocalCommand {
}
boolean ignoreAttachments = ns.getBoolean("ignore_attachments");
try {
m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, ns.getBoolean("json") ? new JsonDbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH) : new DbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH));
m.receiveMessages(1,
TimeUnit.HOURS,
false,
ignoreAttachments,
ns.getBoolean("json")
? new JsonDbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH)
: new DbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH));
return 0;
} catch (IOException e) {
System.err.println("Error while receiving messages: " + e.getMessage());

View file

@ -0,0 +1,84 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.asamk.signal.manager.Manager;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
import java.io.IOException;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
public class JoinGroupCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("--uri").required(true).help("Specify the uri with the group invitation link.");
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
final GroupInviteLinkUrl linkUrl;
String uri = ns.getString("uri");
try {
linkUrl = GroupInviteLinkUrl.fromUri(uri);
} catch (GroupInviteLinkUrl.InvalidGroupLinkException e) {
System.err.println("Group link is invalid: " + e.getMessage());
return 2;
} catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
System.err.println("Group link was created with an incompatible version: " + e.getMessage());
return 2;
}
if (linkUrl == null) {
System.err.println("Link is not a signal group invitation link");
return 2;
}
try {
final Pair<GroupId, List<SendMessageResult>> results = m.joinGroup(linkUrl);
GroupId newGroupId = results.first();
if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) {
System.out.println("Requested to join group \"" + newGroupId.toBase64() + "\"");
} else {
System.out.println("Joined group \"" + newGroupId.toBase64() + "\"");
}
return handleTimestampAndSendMessageResults(0, results.second());
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
} catch (GroupPatchNotAcceptedException e) {
System.err.println("Failed to join group, maybe already a member");
return 1;
} catch (IOException e) {
e.printStackTrace();
handleIOException(e);
return 1;
} catch (Signal.Error.AttachmentInvalid e) {
System.err.println("Failed to add avatar attachment for group\": " + e.getMessage());
return 1;
} catch (DBusExecutionException e) {
System.err.println("Failed to send message: " + e.getMessage());
return 1;
} catch (GroupLinkNotActiveException e) {
System.err.println("Group link is not valid: " + e.getMessage());
return 2;
}
}
}

View file

@ -16,8 +16,7 @@ public class LinkCommand implements ProvisioningCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-n", "--name")
.help("Specify a name to describe this new device.");
subparser.addArgument("-n", "--name").help("Specify a name to describe this new device.");
}
@Override
@ -43,7 +42,11 @@ public class LinkCommand implements ProvisioningCommand {
e.printStackTrace();
return 2;
} catch (UserAlreadyExists e) {
System.err.println("The user " + e.getUsername() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again.");
System.err.println("The user "
+ e.getUsername()
+ " already exists\nDelete \""
+ e.getFileName()
+ "\" before trying again.");
return 1;
}
return 0;

View file

@ -25,7 +25,10 @@ public class ListDevicesCommand implements LocalCommand {
try {
List<DeviceInfo> devices = m.getLinkedDevices();
for (DeviceInfo d : devices) {
System.out.println("Device " + d.getId() + (d.getId() == m.getDeviceId() ? " (this device)" : "") + ":");
System.out.println("Device "
+ d.getId()
+ (d.getId() == m.getDeviceId() ? " (this device)" : "")
+ ":");
System.out.println(" Name: " + d.getName());
System.out.println(" Created: " + DateUtils.formatTimestamp(d.getCreated()));
System.out.println(" Last seen: " + DateUtils.formatTimestamp(d.getLastSeen()));

View file

@ -4,29 +4,61 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.groups.GroupInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class ListGroupsCommand implements LocalCommand {
private static void printGroup(GroupInfo group, boolean detailed, SignalServiceAddress address) {
private static void printGroup(Manager m, GroupInfo group, boolean detailed) {
if (detailed) {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s",
Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked, group.getMembersE164()));
Set<String> members = group.getMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
Set<String> pendingMembers = group.getPendingMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
Set<String> requestingMembers = group.getRequestingMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink();
System.out.println(String.format(
"Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s",
group.getGroupId().toBase64(),
group.getTitle(),
group.isMember(m.getSelfAddress()),
group.isBlocked(),
members,
pendingMembers,
requestingMembers,
groupInviteLink == null ? '-' : groupInviteLink.getUrl()));
} else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked));
group.getGroupId().toBase64(),
group.getTitle(),
group.isMember(m.getSelfAddress()),
group.isBlocked()));
}
}
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-d", "--detailed").action(Arguments.storeTrue())
.help("List members of each group");
subparser.addArgument("-d", "--detailed").action(Arguments.storeTrue()).help("List members of each group");
subparser.help("List group name and ids");
}
@ -41,7 +73,7 @@ public class ListGroupsCommand implements LocalCommand {
boolean detailed = ns.getBoolean("detailed");
for (GroupInfo group : groups) {
printGroup(group, detailed, m.getSelfAddress());
printGroup(m, group, detailed);
}
return 0;
}

View file

@ -15,14 +15,17 @@ public class ListIdentitiesCommand implements LocalCommand {
private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) {
String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey()));
System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirId.getAddress().getNumber().orNull(),
theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toString(theirId.getFingerprint()), digits));
System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s",
theirId.getAddress().getNumber().orNull(),
theirId.getTrustLevel(),
theirId.getDateAdded(),
Hex.toString(theirId.getFingerprint()),
digits));
}
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-n", "--number")
.help("Only show identity keys for the given phone number.");
subparser.addArgument("-n", "--number").help("Only show identity keys for the given phone number.");
}
@Override

View file

@ -3,29 +3,30 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotAGroupMemberException;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import java.io.IOException;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
public class QuitGroupCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-g", "--group")
.required(true)
.help("Specify the recipient group ID.");
subparser.addArgument("-g", "--group").required(true).help("Specify the recipient group ID.");
}
@Override
@ -36,14 +37,12 @@ public class QuitGroupCommand implements LocalCommand {
}
try {
m.sendQuitGroupMessage(Util.decodeGroupId(ns.getString("group")));
return 0;
final GroupId groupId = Util.decodeGroupId(ns.getString("group"));
final Pair<Long, List<SendMessageResult>> results = m.sendQuitGroupMessage(groupId);
return handleTimestampAndSendMessageResults(results.first(), results.second());
} catch (IOException e) {
handleIOException(e);
return 3;
} catch (EncapsulatedExceptions e) {
handleEncapsulatedExceptions(e);
return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;

View file

@ -63,7 +63,9 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
}
} else {
System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n",
messageReceived.getSender(), DateUtils.formatTimestamp(messageReceived.getTimestamp()), messageReceived.getMessage()));
messageReceived.getSender(),
DateUtils.formatTimestamp(messageReceived.getTimestamp()),
messageReceived.getMessage()));
if (messageReceived.getGroupId().length > 0) {
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(messageReceived.getGroupId()));
@ -78,23 +80,23 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
}
});
dbusconnection.addSigHandler(Signal.ReceiptReceived.class,
receiptReceived -> {
if (jsonProcessor != null) {
JsonMessageEnvelope envelope = new JsonMessageEnvelope(receiptReceived);
ObjectNode result = jsonProcessor.createObjectNode();
result.putPOJO("envelope", envelope);
try {
jsonProcessor.writeValue(System.out, result);
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n",
receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp())));
}
});
dbusconnection.addSigHandler(Signal.ReceiptReceived.class, receiptReceived -> {
if (jsonProcessor != null) {
JsonMessageEnvelope envelope = new JsonMessageEnvelope(receiptReceived);
ObjectNode result = jsonProcessor.createObjectNode();
result.putPOJO("envelope", envelope);
try {
jsonProcessor.writeValue(System.out, result);
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n",
receiptReceived.getSender(),
DateUtils.formatTimestamp(receiptReceived.getTimestamp())));
}
});
dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> {
if (jsonProcessor != null) {
@ -109,7 +111,10 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
}
} else {
System.out.print(String.format("Sync Envelope from: %s to: %s\nTimestamp: %s\nBody: %s\n",
syncReceived.getSource(), syncReceived.getDestination(), DateUtils.formatTimestamp(syncReceived.getTimestamp()), syncReceived.getMessage()));
syncReceived.getSource(),
syncReceived.getDestination(),
DateUtils.formatTimestamp(syncReceived.getTimestamp()),
syncReceived.getMessage()));
if (syncReceived.getGroupId().length > 0) {
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(syncReceived.getGroupId()));
@ -156,8 +161,14 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
}
boolean ignoreAttachments = ns.getBoolean("ignore_attachments");
try {
final Manager.ReceiveMessageHandler handler = ns.getBoolean("json") ? new JsonReceiveMessageHandler(m) : new ReceiveMessageHandler(m);
m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, ignoreAttachments, handler);
final Manager.ReceiveMessageHandler handler = ns.getBoolean("json")
? new JsonReceiveMessageHandler(m)
: new ReceiveMessageHandler(m);
m.receiveMessages((long) (timeout * 1000),
TimeUnit.MILLISECONDS,
returnOnTimeout,
ignoreAttachments,
handler);
return 0;
} catch (IOException e) {
System.err.println("Error while receiving messages: " + e.getMessage());

View file

@ -16,15 +16,19 @@ public class RegisterCommand implements LocalCommand {
subparser.addArgument("-v", "--voice")
.help("The verification should be done over voice, not sms.")
.action(Arguments.storeTrue());
subparser.addArgument("--captcha")
.help("The captcha token, required if registration failed with a captcha required error.");
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
try {
m.register(ns.getBoolean("voice"));
final boolean voiceVerification = ns.getBoolean("voice");
final String captcha = ns.getString("captcha");
m.register(voiceVerification, captcha);
return 0;
} catch (CaptchaRequiredException e) {
System.err.println("Captcha required for verification (" + e.getMessage() + ")");
System.err.println("Captcha invalid or required for verification (" + e.getMessage() + ")");
return 1;
} catch (IOException e) {
System.err.println("Request verify error: " + e.getMessage());

View file

@ -5,7 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
@ -22,16 +22,10 @@ public class SendCommand implements DbusCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-g", "--group")
.help("Specify the recipient group ID.");
subparser.addArgument("recipient")
.help("Specify the recipients' phone number.")
.nargs("*");
subparser.addArgument("-m", "--message")
.help("Specify the message, if missing standard input is used.");
subparser.addArgument("-a", "--attachment")
.nargs("*")
.help("Add file as attachment");
subparser.addArgument("-g", "--group").help("Specify the recipient group ID.");
subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*");
subparser.addArgument("-m", "--message").help("Specify the message, if missing standard input is used.");
subparser.addArgument("-a", "--attachment").nargs("*").help("Add file as attachment");
subparser.addArgument("-e", "--endsession")
.help("Clear session state and send end session message.")
.action(Arguments.storeTrue());
@ -44,7 +38,9 @@ public class SendCommand implements DbusCommand {
return 1;
}
if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && (ns.getBoolean("endsession") || ns.getString("group") == null)) {
if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && (
ns.getBoolean("endsession") || ns.getString("group") == null
)) {
System.err.println("No recipients given");
System.err.println("Aborting sending.");
return 1;
@ -83,7 +79,7 @@ public class SendCommand implements DbusCommand {
if (ns.getString("group") != null) {
byte[] groupId;
try {
groupId = Util.decodeGroupId(ns.getString("group"));
groupId = Util.decodeGroupId(ns.getString("group")).serialize();
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;

View file

@ -4,34 +4,34 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotAGroupMemberException;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
public class SendReactionCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.help("Send reaction to a previously received or sent message.");
subparser.addArgument("-g", "--group")
.help("Specify the recipient group ID.");
subparser.addArgument("recipient")
.help("Specify the recipients' phone number.")
.nargs("*");
subparser.addArgument("-g", "--group").help("Specify the recipient group ID.");
subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*");
subparser.addArgument("-e", "--emoji")
.required(true)
.help("Specify the emoji, should be a single unicode grapheme cluster.");
@ -42,9 +42,7 @@ public class SendReactionCommand implements LocalCommand {
.required(true)
.type(long.class)
.help("Specify the timestamp of the message to which to react.");
subparser.addArgument("-r", "--remove")
.help("Remove a reaction.")
.action(Arguments.storeTrue());
subparser.addArgument("-r", "--remove").help("Remove a reaction.").action(Arguments.storeTrue());
}
@Override
@ -66,19 +64,21 @@ public class SendReactionCommand implements LocalCommand {
long targetTimestamp = ns.getLong("target_timestamp");
try {
final Pair<Long, List<SendMessageResult>> results;
if (ns.getString("group") != null) {
byte[] groupId = Util.decodeGroupId(ns.getString("group"));
m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
GroupId groupId = Util.decodeGroupId(ns.getString("group"));
results = m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
} else {
m.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, ns.getList("recipient"));
results = m.sendMessageReaction(emoji,
isRemove,
targetAuthor,
targetTimestamp,
ns.getList("recipient"));
}
return 0;
return handleTimestampAndSendMessageResults(results.first(), results.second());
} catch (IOException e) {
handleIOException(e);
return 3;
} catch (EncapsulatedExceptions e) {
handleEncapsulatedExceptions(e);
return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;

View file

@ -16,9 +16,7 @@ public class TrustCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("number")
.help("Specify the phone number, for which to set the trust.")
.required(true);
subparser.addArgument("number").help("Specify the phone number, for which to set the trust.").required(true);
MutuallyExclusiveGroup mutTrust = subparser.addMutuallyExclusiveGroup();
mutTrust.addArgument("-a", "--trust-all-known-keys")
.help("Trust all known keys of this user, only use this for testing.")
@ -49,7 +47,8 @@ public class TrustCommand implements LocalCommand {
try {
fingerprintBytes = Hex.toByteArray(safetyNumber.toLowerCase(Locale.ROOT));
} catch (Exception e) {
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;
}
boolean res;
@ -60,7 +59,8 @@ public class TrustCommand implements LocalCommand {
return 1;
}
if (!res) {
System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct.");
System.err.println(
"Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct.");
return 1;
}
} else if (safetyNumber.length() == 60) {
@ -72,15 +72,18 @@ public class TrustCommand implements LocalCommand {
return 1;
}
if (!res) {
System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct.");
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;
}
} else {
System.err.println("Safety number has invalid format, either specify the old hex fingerprint or the new safety number");
System.err.println(
"Safety number has invalid format, either specify the old hex fingerprint or the new safety number");
return 1;
}
} else {
System.err.println("You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER");
System.err.println(
"You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER");
return 1;
}
}

View file

@ -3,9 +3,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
@ -13,12 +14,8 @@ public class UnblockCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("contact")
.help("Contact number")
.nargs("*");
subparser.addArgument("-g", "--group")
.help("Group ID")
.nargs("*");
subparser.addArgument("contact").help("Contact number").nargs("*");
subparser.addArgument("-g", "--group").help("Group ID").nargs("*");
subparser.help("Unblock the given contacts or groups (messages will be received again)");
}
@ -40,7 +37,7 @@ public class UnblockCommand implements LocalCommand {
if (ns.<String>getList("group") != null) {
for (String groupIdString : ns.<String>getList("group")) {
try {
byte[] groupId = Util.decodeGroupId(groupIdString);
GroupId groupId = Util.decodeGroupId(groupIdString);
m.setGroupBlocked(groupId, false);
} catch (GroupIdFormatException | GroupNotFoundException e) {
System.err.println(e.getMessage());

View file

@ -12,11 +12,8 @@ public class UpdateContactCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("number")
.help("Contact number");
subparser.addArgument("-n", "--name")
.required(true)
.help("New contact name");
subparser.addArgument("number").help("Contact number");
subparser.addArgument("-n", "--name").required(true).help("New contact name");
subparser.addArgument("-e", "--expiration")
.required(false)
.type(int.class)

View file

@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.util.Base64;
@ -19,15 +19,10 @@ public class UpdateGroupCommand implements DbusCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-g", "--group")
.help("Specify the recipient group ID.");
subparser.addArgument("-n", "--name")
.help("Specify the new group name.");
subparser.addArgument("-a", "--avatar")
.help("Specify a new group avatar image file");
subparser.addArgument("-m", "--member")
.nargs("*")
.help("Specify one or more members to add to the group");
subparser.addArgument("-g", "--group").help("Specify the recipient group ID.");
subparser.addArgument("-n", "--name").help("Specify the new group name.");
subparser.addArgument("-a", "--avatar").help("Specify a new group avatar image file");
subparser.addArgument("-m", "--member").nargs("*").help("Specify one or more members to add to the group");
}
@Override
@ -40,7 +35,7 @@ public class UpdateGroupCommand implements DbusCommand {
byte[] groupId = null;
if (ns.getString("group") != null) {
try {
groupId = Util.decodeGroupId(ns.getString("group"));
groupId = Util.decodeGroupId(ns.getString("group")).serialize();
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;

View file

@ -14,16 +14,11 @@ public class UpdateProfileCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup()
.required(true);
avatarOptions.addArgument("--avatar")
.help("Path to new profile avatar");
avatarOptions.addArgument("--remove-avatar")
.action(Arguments.storeTrue());
final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup().required(true);
avatarOptions.addArgument("--avatar").help("Path to new profile avatar");
avatarOptions.addArgument("--remove-avatar").action(Arguments.storeTrue());
subparser.addArgument("--name")
.required(true)
.help("New profile name");
subparser.addArgument("--name").required(true).help("New profile name");
subparser.help("Set a name and avatar image for the user profile");
}

View file

@ -6,6 +6,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.StickerPackInvalidException;
import java.io.File;
import java.io.IOException;
public class UploadStickerPackCommand implements LocalCommand {
@ -19,7 +20,7 @@ public class UploadStickerPackCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
try {
String path = ns.getString("path");
File path = new File(ns.getString("path"));
String url = m.uploadStickerPack(path);
System.out.println(url);
return 0;

View file

@ -12,10 +12,8 @@ public class VerifyCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("verificationCode")
.help("The verification code you received via sms or voice call.");
subparser.addArgument("-p", "--pin")
.help("The registration lock PIN, that was set by the user (Optional)");
subparser.addArgument("verificationCode").help("The verification code you received via sms or voice call.");
subparser.addArgument("-p", "--pin").help("The registration lock PIN, that was set by the user (Optional)");
}
@Override
@ -30,7 +28,8 @@ public class VerifyCommand implements LocalCommand {
m.verifyAccount(verificationCode, pin);
return 0;
} catch (LockedException e) {
System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: " + (e.getTimeRemaining() / 1000 / 60 / 60));
System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: "
+ (e.getTimeRemaining() / 1000 / 60 / 60));
System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN");
return 3;
} catch (IOException e) {

View file

@ -2,21 +2,23 @@ package org.asamk.signal.dbus;
import org.asamk.Signal;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotAGroupMemberException;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.util.ErrorUtils;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class DbusSignalImpl implements Signal {
@ -43,41 +45,30 @@ public class DbusSignalImpl implements Signal {
return sendMessage(message, attachments, recipients);
}
private static DBusExecutionException convertEncapsulatedExceptions(EncapsulatedExceptions e) {
if (e.getNetworkExceptions().size() + e.getUnregisteredUserExceptions().size() + e.getUntrustedIdentityExceptions().size() == 1) {
if (e.getNetworkExceptions().size() == 1) {
NetworkFailureException n = e.getNetworkExceptions().get(0);
return new Error.Failure("Network failure for \"" + n.getE164number() + "\": " + n.getMessage());
} else if (e.getUnregisteredUserExceptions().size() == 1) {
UnregisteredUserException n = e.getUnregisteredUserExceptions().get(0);
return new Error.UnregisteredUser("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage());
} else if (e.getUntrustedIdentityExceptions().size() == 1) {
UntrustedIdentityException n = e.getUntrustedIdentityExceptions().get(0);
return new Error.UntrustedIdentity("Untrusted Identity for \"" + n.getIdentifier() + "\": " + n.getMessage());
}
private static void checkSendMessageResults(
long timestamp, List<SendMessageResult> results
) throws DBusExecutionException {
List<String> errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
if (errors.size() == 0) {
return;
}
StringBuilder message = new StringBuilder();
message.append("Failed to send (some) messages:").append('\n');
for (NetworkFailureException n : e.getNetworkExceptions()) {
message.append("Network failure for \"").append(n.getE164number()).append("\": ").append(n.getMessage()).append('\n');
}
for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) {
message.append("Unregistered user \"").append(n.getE164Number()).append("\": ").append(n.getMessage()).append('\n');
}
for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) {
message.append("Untrusted Identity for \"").append(n.getIdentifier()).append("\": ").append(n.getMessage()).append('\n');
message.append(timestamp).append('\n');
message.append("Failed to send (some) messages:\n");
for (String error : errors) {
message.append(error).append('\n');
}
return new Error.Failure(message.toString());
throw new Error.Failure(message.toString());
}
@Override
public long sendMessage(final String message, final List<String> attachments, final List<String> recipients) {
try {
return m.sendMessage(message, attachments, recipients);
} catch (EncapsulatedExceptions e) {
throw convertEncapsulatedExceptions(e);
final Pair<Long, List<SendMessageResult>> results = m.sendMessage(message, attachments, recipients);
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
} catch (AttachmentInvalidException e) {
@ -90,11 +81,10 @@ public class DbusSignalImpl implements Signal {
@Override
public void sendEndSessionMessage(final List<String> recipients) {
try {
m.sendEndSessionMessage(recipients);
final Pair<Long, List<SendMessageResult>> results = m.sendEndSessionMessage(recipients);
checkSendMessageResults(results.first(), results.second());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (EncapsulatedExceptions e) {
throw convertEncapsulatedExceptions(e);
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
}
@ -103,11 +93,13 @@ public class DbusSignalImpl implements Signal {
@Override
public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
try {
return m.sendGroupMessage(message, attachments, groupId);
Pair<Long, List<SendMessageResult>> results = m.sendGroupMessage(message,
attachments,
GroupId.unknownVersion(groupId));
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (EncapsulatedExceptions e) {
throw convertEncapsulatedExceptions(e);
} catch (GroupNotFoundException | NotAGroupMemberException e) {
throw new Error.GroupNotFound(e.getMessage());
} catch (AttachmentInvalidException e) {
@ -145,7 +137,7 @@ public class DbusSignalImpl implements Signal {
@Override
public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
try {
m.setGroupBlocked(groupId, blocked);
m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked);
} catch (GroupNotFoundException e) {
throw new Error.GroupNotFound(e.getMessage());
}
@ -156,39 +148,57 @@ public class DbusSignalImpl implements Signal {
List<GroupInfo> groups = m.getGroups();
List<byte[]> ids = new ArrayList<>(groups.size());
for (GroupInfo group : groups) {
ids.add(group.groupId);
ids.add(group.getGroupId().serialize());
}
return ids;
}
@Override
public String getGroupName(final byte[] groupId) {
GroupInfo group = m.getGroup(groupId);
GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId));
if (group == null) {
return "";
} else {
return group.name;
return group.getTitle();
}
}
@Override
public List<String> getGroupMembers(final byte[] groupId) {
GroupInfo group = m.getGroup(groupId);
GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId));
if (group == null) {
return Collections.emptyList();
} else {
return new ArrayList<>(group.getMembersE164());
return group.getMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toList());
}
}
@Override
public byte[] updateGroup(final byte[] groupId, final String name, final List<String> members, final String avatar) {
public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
try {
return m.updateGroup(groupId, name, members, avatar);
if (groupId.length == 0) {
groupId = null;
}
if (name.isEmpty()) {
name = null;
}
if (members.isEmpty()) {
members = null;
}
if (avatar.isEmpty()) {
avatar = null;
}
final Pair<GroupId, List<SendMessageResult>> results = m.updateGroup(groupId == null
? null
: GroupId.unknownVersion(groupId), name, members, avatar);
checkSendMessageResults(0, results.second());
return results.first().serialize();
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (EncapsulatedExceptions e) {
throw convertEncapsulatedExceptions(e);
} catch (GroupNotFoundException | NotAGroupMemberException e) {
throw new Error.GroupNotFound(e.getMessage());
} catch (InvalidNumberException e) {

View file

@ -1,19 +1,17 @@
package org.asamk.signal.json;
import org.asamk.Signal;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
//import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
// i think this is what you have to do to get another dict in json
// but i'm not sure
class JsonReaction {
String emoji; // unicode??
String emoji; // unicode?
String targetAuthor;
long targetTimestamp;
boolean isRemove;
@ -32,50 +30,72 @@ class JsonDataMessage {
long timestamp;
String message;
int expiresInSeconds;
JsonReaction reaction;
JsonQuote quote;
List<JsonMention> mentions;
List<JsonAttachment> attachments;
JsonGroupInfo groupInfo;
JsonReaction reaction;
SignalServiceDataMessage.Quote quote;
JsonDataMessage(SignalServiceDataMessage dataMessage) {
JsonDataMessage(SignalServiceDataMessage dataMessage, Manager m) {
this.timestamp = dataMessage.getTimestamp();
if (dataMessage.getGroupContext().isPresent() && dataMessage.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = dataMessage.getGroupContext().get().getGroupV1().get();
this.groupInfo = new JsonGroupInfo(groupInfo);
if (dataMessage.getGroupContext().isPresent()) {
if (dataMessage.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = dataMessage.getGroupContext().get().getGroupV1().get();
this.groupInfo = new JsonGroupInfo(groupInfo);
} else if (dataMessage.getGroupContext().get().getGroupV2().isPresent()) {
SignalServiceGroupV2 groupInfo = dataMessage.getGroupContext().get().getGroupV2().get();
this.groupInfo = new JsonGroupInfo(groupInfo);
}
}
if (dataMessage.getBody().isPresent()) {
this.message = dataMessage.getBody().get();
}
this.expiresInSeconds = dataMessage.getExpiresInSeconds();
if (dataMessage.getAttachments().isPresent()) {
this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size());
for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) {
this.attachments.add(new JsonAttachment(attachment));
}
if (dataMessage.getReaction().isPresent()) {
this.reaction = new JsonReaction(dataMessage.getReaction().get(), m);
}
if (dataMessage.getQuote().isPresent()) {
this.quote = new JsonQuote(dataMessage.getQuote().get(), m);
}
if (dataMessage.getMentions().isPresent()) {
this.mentions = dataMessage.getMentions()
.get()
.stream()
.map(mention -> new JsonMention(mention, m))
.collect(Collectors.toList());
} else {
this.attachments = new ArrayList<>();
this.mentions = List.of();
}
if (dataMessage.getAttachments().isPresent()) {
this.attachments = dataMessage.getAttachments()
.get()
.stream()
.map(JsonAttachment::new)
.collect(Collectors.toList());
} else {
this.attachments = List.of();
}
if (dataMessage.getReaction().isPresent()) {
final SignalServiceDataMessage.Reaction reaction = dataMessage.getReaction().get();
this.reaction = new JsonReaction(reaction);
/* this.emoji = reaction.getEmoji();
// comment on this line from ReceiveMessageHandler: todo resolve
/* this.emoji = reaction.getEmoji();
this.targetAuthor = reaction.getTargetAuthor().getLegacyIdentifier();
this.targetTimestamp = reaction.getTargetSentTimestamp();
*/ } /*else {
this.reaction = null;
/*
this.emoji = "";
this.targetAuthor = "";
this.targetTimestamp = 0;
*/ // }
/*
}
if (message.getQuote().isPresent()) {
SignalServiceDataMessage.Quote quote = message.getQuote().get();
System.out.println("Quote: (" + quote.getId() + ")");
// there doesn't seem to be any fucking way to find a message's id?
// there doesn't seem to be any way to find a message's id?
System.out.println(" Author: " + quote.getAuthor().getLegacyIdentifier());
System.out.println(" Text: " + quote.getText());
}
@ -84,27 +104,24 @@ class JsonDataMessage {
}
*/
}
// very confusingly MessageReceived seems to be only made in JsonDbusReceiveMessageHandler
// and only when *sending* to dbus, so to my current understanding this never gets called
// which would suggest i'm not understanding something
public JsonDataMessage(Signal.MessageReceived messageReceived) {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
attachments = messageReceived.getAttachments()
.stream()
.map(JsonAttachment::new)
.collect(Collectors.toList());
reaction = null; // TODO Replace these 3 with the proper commands
quote = null;
mentions = null;
attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
}
// i don't understand what SyncMessages are so i'm gonna ignore them
// i don't understand what SyncMessages are so i'm going to ignore them
// i think it only matters if you have multiple devices on your end
public JsonDataMessage(Signal.SyncMessageReceived messageReceived) {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
attachments = messageReceived.getAttachments()
.stream()
.map(JsonAttachment::new)
.collect(Collectors.toList());
reaction = null; // TODO Replace these 3 with the proper commands
quote = null;
mentions = null;
attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
}
}

View file

@ -1,6 +1,8 @@
package org.asamk.signal.json;
import org.asamk.signal.manager.GroupUtils;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
@ -28,6 +30,11 @@ class JsonGroupInfo {
this.type = groupInfo.getType().toString();
}
JsonGroupInfo(SignalServiceGroupV2 groupInfo) {
this.groupId = GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64();
this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER";
}
JsonGroupInfo(byte[] groupId) {
this.groupId = Base64.encodeBytes(groupId);
}

View file

@ -0,0 +1,19 @@
package org.asamk.signal.json;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class JsonMention {
String name;
int start;
int length;
JsonMention(SignalServiceDataMessage.Mention mention, Manager m) {
this.name = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null))
.getLegacyIdentifier();
this.start = mention.getStart();
this.length = mention.getLength();
}
}

View file

@ -2,24 +2,25 @@ package org.asamk.signal.json;
import org.asamk.Signal;
//import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
public class JsonMessageEnvelope {
// gotta do something so that it actually emits valid json instead of null
// or just fix it on the python side i guess
String source;
int sourceDevice;
String relay;
long timestamp;
boolean isReceipt;
JsonDataMessage dataMessage;
JsonSyncMessage syncMessage;
JsonCallMessage callMessage;
JsonReceiptMessage receiptMessage;
// String typingAction;
public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content) {
// String typingAction;
public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content, Manager m) {
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
SignalServiceAddress source = envelope.getSourceAddress();
this.source = source.getLegacyIdentifier();
@ -27,17 +28,19 @@ public class JsonMessageEnvelope {
}
this.sourceDevice = envelope.getSourceDevice();
this.timestamp = envelope.getTimestamp();
this.isReceipt = envelope.isReceipt();
if (envelope.isReceipt()) {
this.receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp));
}
if (content != null) {
if (envelope.isUnidentifiedSender()) {
this.source = content.getSender().getLegacyIdentifier();
this.sourceDevice = content.getSenderDevice();
}
if (content.getDataMessage().isPresent()) {
this.dataMessage = new JsonDataMessage(content.getDataMessage().get());
this.dataMessage = new JsonDataMessage(content.getDataMessage().get(), m);
}
if (content.getSyncMessage().isPresent()) {
this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get());
this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get(), m);
}
if (content.getCallMessage().isPresent()) {
this.callMessage = new JsonCallMessage(content.getCallMessage().get());
@ -61,7 +64,7 @@ public class JsonMessageEnvelope {
public JsonMessageEnvelope(Signal.ReceiptReceived receiptReceived) {
source = receiptReceived.getSender();
timestamp = receiptReceived.getTimestamp();
isReceipt = true;
receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp));
}
public JsonMessageEnvelope(Signal.SyncMessageReceived messageReceived) {

View file

@ -0,0 +1,40 @@
package org.asamk.signal.json;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class JsonQuote {
long id;
String author;
String text;
List<JsonMention> mentions;
List<JsonQuotedAttachment> attachments;
JsonQuote(SignalServiceDataMessage.Quote quote, Manager m) {
this.id = quote.getId();
this.author = m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier();
this.text = quote.getText();
if (quote.getMentions() != null && quote.getMentions().size() > 0) {
this.mentions = quote.getMentions()
.stream()
.map(quotedMention -> new JsonMention(quotedMention, m))
.collect(Collectors.toList());
}
if (quote.getAttachments().size() > 0) {
this.attachments = quote.getAttachments()
.stream()
.map(JsonQuotedAttachment::new)
.collect(Collectors.toList());
} else {
this.attachments = new ArrayList<>();
}
}
}

View file

@ -0,0 +1,20 @@
package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
public class JsonQuotedAttachment {
String contentType;
String filename;
JsonAttachment thumbnail;
JsonQuotedAttachment(SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment) {
contentType = quotedAttachment.getContentType();
filename = quotedAttachment.getFileName();
if (quotedAttachment.getThumbnail() != null) {
thumbnail = new JsonAttachment(quotedAttachment.getThumbnail());
} else {
thumbnail = null;
}
}
}

View file

@ -0,0 +1,19 @@
package org.asamk.signal.json;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction;
public class JsonReaction {
String emoji;
String targetAuthor;
long targetSentTimestamp;
boolean isRemove;
JsonReaction(Reaction reaction, Manager m) {
this.emoji = reaction.getEmoji();
this.targetAuthor = m.resolveSignalServiceAddress(reaction.getTargetAuthor()).getLegacyIdentifier();
this.targetSentTimestamp = reaction.getTargetSentTimestamp();
this.isRemove = reaction.isRemove();
}
}

View file

@ -22,4 +22,17 @@ class JsonReceiptMessage {
}
this.timestamps = receiptMessage.getTimestamps();
}
private JsonReceiptMessage(
final long when, final boolean isDelivery, final boolean isRead, final List<Long> timestamps
) {
this.when = when;
this.isDelivery = isDelivery;
this.isRead = isRead;
this.timestamps = timestamps;
}
static JsonReceiptMessage deliveryReceipt(final long when, final List<Long> timestamps) {
return new JsonReceiptMessage(when, true, false, timestamps);
}
}

View file

@ -1,14 +1,15 @@
package org.asamk.signal.json;
import org.asamk.Signal;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
class JsonSyncDataMessage extends JsonDataMessage {
String destination;
JsonSyncDataMessage(SentTranscriptMessage transcriptMessage) {
super(transcriptMessage.getMessage());
JsonSyncDataMessage(SentTranscriptMessage transcriptMessage, Manager m) {
super(transcriptMessage.getMessage(), m);
if (transcriptMessage.getDestination().isPresent()) {
this.destination = transcriptMessage.getDestination().get().getLegacyIdentifier();
}

View file

@ -1,6 +1,7 @@
package org.asamk.signal.json;
import org.asamk.Signal;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -21,9 +22,9 @@ class JsonSyncMessage {
List<ReadMessage> readMessages;
JsonSyncMessageType type;
JsonSyncMessage(SignalServiceSyncMessage syncMessage) {
JsonSyncMessage(SignalServiceSyncMessage syncMessage, Manager m) {
if (syncMessage.getSent().isPresent()) {
this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get());
this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get(), m);
}
if (syncMessage.getBlockedList().isPresent()) {
this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size());

View file

@ -0,0 +1,63 @@
package org.asamk.signal.manager;
import org.whispersystems.util.Base64;
import java.util.Arrays;
public abstract class GroupId {
private final byte[] id;
public static GroupIdV1 v1(byte[] id) {
return new GroupIdV1(id);
}
public static GroupIdV2 v2(byte[] id) {
return new GroupIdV2(id);
}
public static GroupId unknownVersion(byte[] id) {
if (id.length == 16) {
return new GroupIdV1(id);
} else if (id.length == 32) {
return new GroupIdV2(id);
}
throw new AssertionError("Invalid group id of size " + id.length);
}
public static GroupId fromBase64(String id) throws GroupIdFormatException {
try {
return unknownVersion(java.util.Base64.getDecoder().decode(id));
} catch (Throwable e) {
throw new GroupIdFormatException(id, e);
}
}
public GroupId(final byte[] id) {
this.id = id;
}
public byte[] serialize() {
return id;
}
public String toBase64() {
return Base64.encodeBytes(id);
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final GroupId groupId = (GroupId) o;
return Arrays.equals(id, groupId.id);
}
@Override
public int hashCode() {
return Arrays.hashCode(id);
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager;
public class GroupIdFormatException extends Exception {
public GroupIdFormatException(String groupId, Throwable e) {
super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage(), e);
}
}

View file

@ -0,0 +1,14 @@
package org.asamk.signal.manager;
import static org.asamk.signal.manager.KeyUtils.getSecretBytes;
public class GroupIdV1 extends GroupId {
public static GroupIdV1 createRandom() {
return new GroupIdV1(getSecretBytes(16));
}
public GroupIdV1(final byte[] id) {
super(id);
}
}

View file

@ -0,0 +1,14 @@
package org.asamk.signal.manager;
import java.util.Base64;
public class GroupIdV2 extends GroupId {
public static GroupIdV2 fromBase64(String groupId) {
return new GroupIdV2(Base64.getDecoder().decode(groupId));
}
public GroupIdV2(final byte[] id) {
super(id);
}
}

View file

@ -0,0 +1,140 @@
package org.asamk.signal.manager;
import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.GroupInviteLink;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.util.Base64UrlSafe;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
public final class GroupInviteLinkUrl {
private static final String GROUP_URL_HOST = "signal.group";
private static final String GROUP_URL_PREFIX = "https://" + GROUP_URL_HOST + "/#";
private final GroupMasterKey groupMasterKey;
private final GroupLinkPassword password;
private final String url;
public static GroupInviteLinkUrl forGroup(GroupMasterKey groupMasterKey, DecryptedGroup group) {
return new GroupInviteLinkUrl(groupMasterKey,
GroupLinkPassword.fromBytes(group.getInviteLinkPassword().toByteArray()));
}
public static boolean isGroupLink(String urlString) {
return getGroupUrl(urlString) != null;
}
/**
* @return null iff not a group url.
* @throws InvalidGroupLinkException If group url, but cannot be parsed.
*/
public static GroupInviteLinkUrl fromUri(String urlString) throws InvalidGroupLinkException, UnknownGroupLinkVersionException {
URI uri = getGroupUrl(urlString);
if (uri == null) {
return null;
}
try {
if (!"/".equals(uri.getPath()) && uri.getPath().length() > 0) {
throw new InvalidGroupLinkException("No path was expected in uri");
}
String encoding = uri.getFragment();
if (encoding == null || encoding.length() == 0) {
throw new InvalidGroupLinkException("No reference was in the uri");
}
byte[] bytes = Base64UrlSafe.decodePaddingAgnostic(encoding);
GroupInviteLink groupInviteLink = GroupInviteLink.parseFrom(bytes);
switch (groupInviteLink.getContentsCase()) {
case V1CONTENTS: {
GroupInviteLink.GroupInviteLinkContentsV1 groupInviteLinkContentsV1 = groupInviteLink.getV1Contents();
GroupMasterKey groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.getGroupMasterKey()
.toByteArray());
GroupLinkPassword password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.getInviteLinkPassword()
.toByteArray());
return new GroupInviteLinkUrl(groupMasterKey, password);
}
default:
throw new UnknownGroupLinkVersionException("Url contains no known group link content");
}
} catch (InvalidInputException | IOException e) {
throw new InvalidGroupLinkException(e);
}
}
/**
* @return {@link URI} if the host name matches.
*/
private static URI getGroupUrl(String urlString) {
try {
URI url = new URI(urlString);
if (!"https".equalsIgnoreCase(url.getScheme()) && !"sgnl".equalsIgnoreCase(url.getScheme())) {
return null;
}
return GROUP_URL_HOST.equalsIgnoreCase(url.getHost()) ? url : null;
} catch (URISyntaxException e) {
return null;
}
}
private GroupInviteLinkUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
this.groupMasterKey = groupMasterKey;
this.password = password;
this.url = createUrl(groupMasterKey, password);
}
protected static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
GroupInviteLink groupInviteLink = GroupInviteLink.newBuilder()
.setV1Contents(GroupInviteLink.GroupInviteLinkContentsV1.newBuilder()
.setGroupMasterKey(ByteString.copyFrom(groupMasterKey.serialize()))
.setInviteLinkPassword(ByteString.copyFrom(password.serialize())))
.build();
String encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.toByteArray());
return GROUP_URL_PREFIX + encoding;
}
public String getUrl() {
return url;
}
public GroupMasterKey getGroupMasterKey() {
return groupMasterKey;
}
public GroupLinkPassword getPassword() {
return password;
}
public final static class InvalidGroupLinkException extends Exception {
public InvalidGroupLinkException(String message) {
super(message);
}
public InvalidGroupLinkException(Throwable cause) {
super(cause);
}
}
public final static class UnknownGroupLinkVersionException extends Exception {
public UnknownGroupLinkVersionException(String message) {
super(message);
}
}
}

View file

@ -0,0 +1,40 @@
package org.asamk.signal.manager;
import java.util.Arrays;
public final class GroupLinkPassword {
private static final int SIZE = 16;
private final byte[] bytes;
public static GroupLinkPassword createNew() {
return new GroupLinkPassword(KeyUtils.getSecretBytes(SIZE));
}
public static GroupLinkPassword fromBytes(byte[] bytes) {
return new GroupLinkPassword(bytes);
}
private GroupLinkPassword(byte[] bytes) {
this.bytes = bytes;
}
public byte[] serialize() {
return bytes.clone();
}
@Override
public boolean equals(Object other) {
if (!(other instanceof GroupLinkPassword)) {
return false;
}
return Arrays.equals(bytes, ((GroupLinkPassword) other).bytes);
}
@Override
public int hashCode() {
return Arrays.hashCode(bytes);
}
}

View file

@ -1,10 +1,8 @@
package org.asamk.signal.manager;
import org.whispersystems.util.Base64;
public class GroupNotFoundException extends Exception {
public GroupNotFoundException(byte[] groupId) {
super("Group not found: " + Base64.encodeBytes(groupId));
public GroupNotFoundException(GroupId groupId) {
super("Group not found: " + groupId.toBase64());
}
}

View file

@ -0,0 +1,68 @@
package org.asamk.signal.manager;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.storage.groups.GroupInfoV1;
import org.asamk.signal.storage.groups.GroupInfoV2;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
public class GroupUtils {
public static void setGroupContext(
final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo
) {
if (groupInfo instanceof GroupInfoV1) {
SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
.withId(groupInfo.getGroupId().serialize())
.build();
messageBuilder.asGroupMessage(group);
} else {
final GroupInfoV2 groupInfoV2 = (GroupInfoV2) groupInfo;
SignalServiceGroupV2 group = SignalServiceGroupV2.newBuilder(groupInfoV2.getMasterKey())
.withRevision(groupInfoV2.getGroup() == null ? 0 : groupInfoV2.getGroup().getRevision())
.build();
messageBuilder.asGroupMessage(group);
}
}
public static GroupId getGroupId(SignalServiceGroupContext context) {
if (context.getGroupV1().isPresent()) {
return GroupId.v1(context.getGroupV1().get().getGroupId());
} else if (context.getGroupV2().isPresent()) {
return getGroupIdV2(context.getGroupV2().get().getMasterKey());
} else {
return null;
}
}
public static GroupIdV2 getGroupIdV2(GroupSecretParams groupSecretParams) {
return GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier().serialize());
}
public static GroupIdV2 getGroupIdV2(GroupMasterKey groupMasterKey) {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
return getGroupIdV2(groupSecretParams);
}
public static GroupIdV2 getGroupIdV2(GroupIdV1 groupIdV1) {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(deriveV2MigrationMasterKey(
groupIdV1));
return getGroupIdV2(groupSecretParams);
}
private static GroupMasterKey deriveV2MigrationMasterKey(GroupIdV1 groupIdV1) {
try {
return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1.serialize(),
"GV2 Migration".getBytes(),
GroupMasterKey.SIZE));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

View file

@ -2,7 +2,6 @@ package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Arrays;
import java.util.Objects;
interface HandleAction {
@ -30,8 +29,7 @@ class SendReceiptAction implements HandleAction {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendReceiptAction that = (SendReceiptAction) o;
return timestamp == that.timestamp &&
address.equals(that.address);
return timestamp == that.timestamp && address.equals(that.address);
}
@Override
@ -94,9 +92,9 @@ class SendSyncBlockedListAction implements HandleAction {
class SendGroupInfoRequestAction implements HandleAction {
private final SignalServiceAddress address;
private final byte[] groupId;
private final GroupIdV1 groupId;
public SendGroupInfoRequestAction(final SignalServiceAddress address, final byte[] groupId) {
public SendGroupInfoRequestAction(final SignalServiceAddress address, final GroupIdV1 groupId) {
this.address = address;
this.groupId = groupId;
}
@ -110,15 +108,17 @@ class SendGroupInfoRequestAction implements HandleAction {
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o;
return address.equals(that.address) &&
Arrays.equals(groupId, that.groupId);
if (!address.equals(that.address)) return false;
return groupId.equals(that.groupId);
}
@Override
public int hashCode() {
int result = Objects.hash(address);
result = 31 * result + Arrays.hashCode(groupId);
int result = address.hashCode();
result = 31 * result + groupId.hashCode();
return result;
}
}
@ -126,9 +126,9 @@ class SendGroupInfoRequestAction implements HandleAction {
class SendGroupUpdateAction implements HandleAction {
private final SignalServiceAddress address;
private final byte[] groupId;
private final GroupIdV1 groupId;
public SendGroupUpdateAction(final SignalServiceAddress address, final byte[] groupId) {
public SendGroupUpdateAction(final SignalServiceAddress address, final GroupIdV1 groupId) {
this.address = address;
this.groupId = groupId;
}
@ -142,15 +142,17 @@ class SendGroupUpdateAction implements HandleAction {
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendGroupUpdateAction that = (SendGroupUpdateAction) o;
return address.equals(that.address) &&
Arrays.equals(groupId, that.groupId);
if (!address.equals(that.address)) return false;
return groupId.equals(that.groupId);
}
@Override
public int hashCode() {
int result = Objects.hash(address);
result = 31 * result + Arrays.hashCode(groupId);
int result = address.hashCode();
result = 31 * result + groupId.hashCode();
return result;
}
}

View file

@ -0,0 +1,18 @@
package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.push.TrustStore;
import java.io.InputStream;
class IasTrustStore implements TrustStore {
@Override
public InputStream getKeyStoreInputStream() {
return IasTrustStore.class.getResourceAsStream("ias.store");
}
@Override
public String getKeyStorePassword() {
return "whisper";
}
}

View file

@ -26,14 +26,6 @@ class KeyUtils {
return getSecret(18);
}
static byte[] createGroupId() {
return getSecretBytes(16);
}
static byte[] createUnrestrictedUnidentifiedAccess() {
return getSecretBytes(16);
}
static byte[] createStickerUploadKey() {
return getSecretBytes(32);
}
@ -43,7 +35,7 @@ class KeyUtils {
return Base64.encodeBytes(secret);
}
private static byte[] getSecretBytes(int size) {
static byte[] getSecretBytes(int size) {
byte[] secret = new byte[size];
RandomUtils.getSecureRandom().nextBytes(secret);
return secret;

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,8 @@
package org.asamk.signal.manager;
import org.whispersystems.util.Base64;
public class NotAGroupMemberException extends Exception {
public NotAGroupMemberException(byte[] groupId, String groupName) {
super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")");
public NotAGroupMemberException(GroupId groupId, String groupName) {
super("User is not a member in group: " + groupName + " (" + groupId.toBase64() + ")");
}
}

View file

@ -1,34 +1,34 @@
package org.asamk.signal.manager;
import java.io.File;
public class PathConfig {
private final String dataPath;
private final String attachmentsPath;
private final String avatarsPath;
private final File dataPath;
private final File attachmentsPath;
private final File avatarsPath;
public static PathConfig createDefault(final String settingsPath) {
return new PathConfig(
settingsPath + "/data",
settingsPath + "/attachments",
settingsPath + "/avatars"
);
public static PathConfig createDefault(final File settingsPath) {
return new PathConfig(new File(settingsPath, "data"),
new File(settingsPath, "attachments"),
new File(settingsPath, "avatars"));
}
private PathConfig(final String dataPath, final String attachmentsPath, final String avatarsPath) {
private PathConfig(final File dataPath, final File attachmentsPath, final File avatarsPath) {
this.dataPath = dataPath;
this.attachmentsPath = attachmentsPath;
this.avatarsPath = avatarsPath;
}
public String getDataPath() {
public File getDataPath() {
return dataPath;
}
public String getAttachmentsPath() {
public File getAttachmentsPath() {
return attachmentsPath;
}
public String getAvatarsPath() {
public File getAvatarsPath() {
return avatarsPath;
}
}

View file

@ -31,6 +31,7 @@ import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
@ -45,7 +46,7 @@ public class ProvisioningManager {
private final int registrationId;
private final String password;
public ProvisioningManager(String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) {
public ProvisioningManager(File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) {
this.pathConfig = PathConfig.createDefault(settingsPath);
this.serviceConfiguration = serviceConfiguration;
this.userAgent = userAgent;
@ -70,12 +71,19 @@ public class ProvisioningManager {
public String getDeviceLinkUri() throws TimeoutException, IOException {
String deviceUuid = accountManager.getNewDeviceUuid();
return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid, identityKey.getPublicKey().getPublicKey()));
return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid,
identityKey.getPublicKey().getPublicKey()));
}
public String finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
String signalingKey = KeyUtils.createSignalingKey();
SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(identityKey, signalingKey, false, true, registrationId, deviceName);
SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(
identityKey,
signalingKey,
false,
true,
registrationId,
deviceName);
String username = ret.getNumber();
// TODO do this check before actually registering
@ -96,7 +104,15 @@ public class ProvisioningManager {
}
}
try (SignalAccount account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(), username, ret.getUuid(), password, ret.getDeviceId(), ret.getIdentity(), registrationId, signalingKey, profileKey)) {
try (SignalAccount account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(),
username,
ret.getUuid(),
password,
ret.getDeviceId(),
ret.getIdentity(),
registrationId,
signalingKey,
profileKey)) {
account.save();
try (Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent)) {

View file

@ -1,7 +1,8 @@
package org.asamk.signal.manager;
import org.signal.zkgroup.ServerPublicParams;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
@ -12,8 +13,11 @@ import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -26,53 +30,86 @@ public class ServiceConfig {
final static int PREKEY_MINIMUM_COUNT = 20;
final static int PREKEY_BATCH_SIZE = 100;
final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
final static int MAX_ENVELOPE_SIZE = 0;
final static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
final static String CDS_MRENCLAVE = "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15";
private final static String URL = "https://textsecure-service.whispersystems.org";
private final static String CDN_URL = "https://cdn.signal.org";
private final static String CDN2_URL = "https://cdn2.signal.org";
private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api.directory.signal.org";
private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org";
private final static String STORAGE_URL = "https://storage.signal.org";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
private final static TrustStore IAS_TRUST_STORE = new IasTrustStore();
private final static Optional<Dns> dns = Optional.absent();
private final static String zkGroupServerPublicParamsHex = "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=";
private final static byte[] zkGroupServerPublicParams;
static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false, false);
static final AccountAttributes.Capabilities capabilities;
public static SignalServiceConfiguration createDefaultServiceConfiguration(String userAgent) {
final Interceptor userAgentInterceptor = chain ->
chain.proceed(chain.request().newBuilder()
.header("User-Agent", userAgent)
.build());
final List<Interceptor> interceptors = Collections.singletonList(userAgentInterceptor);
final byte[] zkGroupServerPublicParams;
static {
try {
zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex);
} catch (IOException e) {
throw new AssertionError(e);
}
return new SignalServiceConfiguration(
new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}),
new SignalContactDiscoveryUrl[0],
boolean zkGroupAvailable;
try {
new ServerPublicParams(zkGroupServerPublicParams);
zkGroupAvailable = true;
} catch (Throwable ignored) {
zkGroupAvailable = false;
}
capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable);
}
public static SignalServiceConfiguration createDefaultServiceConfiguration(String userAgent) {
final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
.newBuilder()
.header("User-Agent", userAgent)
.build());
final List<Interceptor> interceptors = Collections.singletonList(userAgentInterceptor);
return new SignalServiceConfiguration(new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}),
new SignalContactDiscoveryUrl[]{new SignalContactDiscoveryUrl(SIGNAL_CONTACT_DISCOVERY_URL,
TRUST_STORE)},
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
interceptors,
dns,
zkGroupServerPublicParams
);
zkGroupServerPublicParams);
}
private static Map<Integer, SignalCdnUrl[]> makeSignalCdnUrlMapFor(SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls) {
Map<Integer, SignalCdnUrl[]> result = new HashMap<>();
result.put(0, cdn0Urls);
result.put(2, cdn2Urls);
return Collections.unmodifiableMap(result);
public static AccountAttributes.Capabilities getCapabilities() {
return capabilities;
}
static KeyStore getIasKeyStore() {
try {
TrustStore contactTrustStore = IAS_TRUST_STORE;
KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(contactTrustStore.getKeyStoreInputStream(),
contactTrustStore.getKeyStorePassword().toCharArray());
return keyStore;
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
private static Map<Integer, SignalCdnUrl[]> makeSignalCdnUrlMapFor(
SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls
) {
return Map.of(0, cdn0Urls, 2, cdn2Urls);
}
private ServiceConfig() {

View file

@ -1,11 +1,13 @@
package org.asamk.signal.manager;
import java.io.File;
public class UserAlreadyExists extends Exception {
private final String username;
private final String fileName;
private final File fileName;
public UserAlreadyExists(String username, String fileName) {
public UserAlreadyExists(String username, File fileName) {
this.username = username;
this.fileName = fileName;
}
@ -14,7 +16,7 @@ public class UserAlreadyExists extends Exception {
return username;
}
public String getFileName() {
public File getFileName() {
return fileName;
}
}

View file

@ -27,11 +27,11 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
@ -81,7 +81,21 @@ class Utils {
Optional<String> caption = Optional.absent();
Optional<String> blurHash = Optional.absent();
final Optional<ResumableUploadSpec> resumableUploadSpec = Optional.absent();
return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, false, preview, 0, 0, uploadTimestamp, caption, blurHash, null, null, resumableUploadSpec);
return new SignalServiceAttachmentStream(attachmentStream,
mime,
attachmentSize,
Optional.of(attachmentFile.getName()),
false,
false,
preview,
0,
0,
uploadTimestamp,
caption,
blurHash,
null,
null,
resumableUploadSpec);
}
static StreamDetails createStreamDetailsFromFile(File file) throws IOException {
@ -96,7 +110,8 @@ class Utils {
static CertificateValidator getCertificateValidator() {
try {
ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT),
0);
return new CertificateValidator(unidentifiedSenderTrustRoot);
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
@ -107,31 +122,20 @@ class Utils {
String[] params = query.split("&");
Map<String, String> map = new HashMap<>();
for (String param : params) {
String name = null;
final String[] paramParts = param.split("=");
try {
name = URLDecoder.decode(paramParts[0], "utf-8");
} catch (UnsupportedEncodingException e) {
// Impossible
}
String value = null;
try {
value = URLDecoder.decode(paramParts[1], "utf-8");
} catch (UnsupportedEncodingException e) {
// Impossible
}
String name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8);
String value = URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8);
map.put(name, value);
}
return map;
}
static String createDeviceLinkUri(DeviceLinkInfo info) {
try {
return "tsdevice:/?uuid=" + URLEncoder.encode(info.deviceIdentifier, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()), "utf-8");
} catch (UnsupportedEncodingException e) {
// Shouldn't happen
return null;
}
return "tsdevice:/?uuid="
+ URLEncoder.encode(info.deviceIdentifier, StandardCharsets.UTF_8)
+ "&pub_key="
+ URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()),
StandardCharsets.UTF_8);
}
static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException {
@ -195,7 +199,15 @@ class Utils {
Optional<SignalServiceAddress> addressOptional = sourceUuid == null && source.isEmpty()
? Optional.absent()
: Optional.of(new SignalServiceAddress(sourceUuid, source));
return new SignalServiceEnvelope(type, addressOptional, sourceDevice, timestamp, legacyMessage, content, serverReceivedTimestamp, serverDeliveredTimestamp, uuid);
return new SignalServiceEnvelope(type,
addressOptional,
sourceDevice,
timestamp,
legacyMessage,
content,
serverReceivedTimestamp,
serverDeliveredTimestamp,
uuid);
}
}
@ -245,13 +257,18 @@ class Utils {
return outputFile;
}
static String computeSafetyNumber(SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
static String computeSafetyNumber(
SignalServiceAddress ownAddress,
IdentityKey ownIdentityKey,
SignalServiceAddress theirAddress,
IdentityKey theirIdentityKey
) {
int version;
byte[] ownId;
byte[] theirId;
if (ServiceConfig.capabilities.isUuid()
&& ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) {
if (ServiceConfig.capabilities.isUuid() && ownAddress.getUuid().isPresent() && theirAddress.getUuid()
.isPresent()) {
// Version 2: UUID user
version = 2;
ownId = UuidUtil.toByteArray(ownAddress.getUuid().get());
@ -266,7 +283,11 @@ class Utils {
theirId = theirAddress.getNumber().get().getBytes();
}
Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, ownId, ownIdentityKey, theirId, theirIdentityKey);
Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version,
ownId,
ownIdentityKey,
theirId,
theirIdentityKey);
return fingerprint.getDisplayableFingerprint().getDisplayText();
}

View file

@ -0,0 +1,11 @@
package org.asamk.signal.manager.helper;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import java.io.IOException;
public interface GroupAuthorizationProvider {
GroupsV2AuthorizationString getAuthorizationForToday(GroupSecretParams groupSecretParams) throws IOException;
}

View file

@ -0,0 +1,398 @@
package org.asamk.signal.manager.helper;
import com.google.protobuf.InvalidProtocolBufferException;
import org.asamk.signal.manager.GroupIdV2;
import org.asamk.signal.manager.GroupLinkPassword;
import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.storage.groups.GroupInfoV2;
import org.asamk.signal.storage.profiles.SignalProfile;
import org.asamk.signal.util.IOUtils;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.groups.UuidCiphertext;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class GroupHelper {
final static Logger logger = LoggerFactory.getLogger(GroupHelper.class);
private final ProfileKeyCredentialProvider profileKeyCredentialProvider;
private final ProfileProvider profileProvider;
private final SelfAddressProvider selfAddressProvider;
private final GroupsV2Operations groupsV2Operations;
private final GroupsV2Api groupsV2Api;
private final GroupAuthorizationProvider groupAuthorizationProvider;
public GroupHelper(
final ProfileKeyCredentialProvider profileKeyCredentialProvider,
final ProfileProvider profileProvider,
final SelfAddressProvider selfAddressProvider,
final GroupsV2Operations groupsV2Operations,
final GroupsV2Api groupsV2Api,
final GroupAuthorizationProvider groupAuthorizationProvider
) {
this.profileKeyCredentialProvider = profileKeyCredentialProvider;
this.profileProvider = profileProvider;
this.selfAddressProvider = selfAddressProvider;
this.groupsV2Operations = groupsV2Operations;
this.groupsV2Api = groupsV2Api;
this.groupAuthorizationProvider = groupAuthorizationProvider;
}
public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
try {
final GroupsV2AuthorizationString groupsV2AuthorizationString = groupAuthorizationProvider.getAuthorizationForToday(
groupSecretParams);
return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
return null;
}
}
public DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
GroupMasterKey groupMasterKey, GroupLinkPassword password
) throws IOException, GroupLinkNotActiveException {
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
return groupsV2Api.getGroupJoinInfo(groupSecretParams,
Optional.fromNullable(password).transform(GroupLinkPassword::serialize),
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
}
public GroupInfoV2 createGroupV2(
String name, Collection<SignalServiceAddress> members, String avatarFile
) throws IOException {
final byte[] avatarBytes = readAvatarBytes(avatarFile);
final GroupsV2Operations.NewGroup newGroup = buildNewGroupV2(name, members, avatarBytes);
if (newGroup == null) {
return null;
}
final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
final GroupsV2AuthorizationString groupAuthForToday;
final DecryptedGroup decryptedGroup;
try {
groupAuthForToday = groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams);
groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
logger.warn("Failed to create V2 group: {}", e.getMessage());
return null;
}
if (decryptedGroup == null) {
logger.warn("Failed to create V2 group, unknown error!");
return null;
}
final GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams);
final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
g.setGroup(decryptedGroup);
return g;
}
private byte[] readAvatarBytes(final String avatarFile) throws IOException {
final byte[] avatarBytes;
try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
}
return avatarBytes;
}
private GroupsV2Operations.NewGroup buildNewGroupV2(
String name, Collection<SignalServiceAddress> members, byte[] avatar
) {
final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
selfAddressProvider.getSelfAddress());
if (profileKeyCredential == null) {
logger.warn("Cannot create a V2 group as self does not have a versioned profile");
return null;
}
if (!areMembersValid(members)) return null;
GroupCandidate self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(),
Optional.fromNullable(profileKeyCredential));
Set<GroupCandidate> candidates = members.stream()
.map(member -> new GroupCandidate(member.getUuid().get(),
Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
.collect(Collectors.toSet());
final GroupSecretParams groupSecretParams = GroupSecretParams.generate();
return groupsV2Operations.createNewGroup(groupSecretParams,
name,
Optional.fromNullable(avatar),
self,
candidates,
Member.Role.DEFAULT,
0);
}
private boolean areMembersValid(final Collection<SignalServiceAddress> members) {
final Set<String> noUuidCapability = members.stream()
.filter(address -> !address.getUuid().isPresent())
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
if (noUuidCapability.size() > 0) {
logger.warn("Cannot create a V2 group as some members don't have a UUID: {}",
String.join(", ", noUuidCapability));
return false;
}
final Set<SignalProfile> noGv2Capability = members.stream()
.map(profileProvider::getProfile)
.filter(profile -> profile != null && !profile.getCapabilities().gv2)
.collect(Collectors.toSet());
if (noGv2Capability.size() > 0) {
logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
noGv2Capability.stream().map(SignalProfile::getName).collect(Collectors.joining(", ")));
return false;
}
return true;
}
public Pair<DecryptedGroup, GroupChange> updateGroupV2(
GroupInfoV2 groupInfoV2, String name, String avatarFile
) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
GroupChange.Actions.Builder change = name != null
? groupOperations.createModifyGroupTitle(name)
: GroupChange.Actions.newBuilder();
if (avatarFile != null) {
final byte[] avatarBytes = readAvatarBytes(avatarFile);
String avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes,
groupSecretParams,
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
}
final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
if (uuid.isPresent()) {
change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
}
return commitChange(groupInfoV2, change);
}
public Pair<DecryptedGroup, GroupChange> updateGroupV2(
GroupInfoV2 groupInfoV2, Set<SignalServiceAddress> newMembers
) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
if (!areMembersValid(newMembers)) {
throw new IOException("Failed to update group");
}
Set<GroupCandidate> candidates = newMembers.stream()
.map(member -> new GroupCandidate(member.getUuid().get(),
Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
.collect(Collectors.toSet());
final GroupChange.Actions.Builder change = groupOperations.createModifyGroupMembershipChange(candidates,
selfAddressProvider.getSelfAddress().getUuid().get());
final Optional<UUID> uuid = this.selfAddressProvider.getSelfAddress().getUuid();
if (uuid.isPresent()) {
change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
}
return commitChange(groupInfoV2, change);
}
public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
List<DecryptedPendingMember> pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
final UUID selfUuid = selfAddressProvider.getSelfAddress().getUuid().get();
Optional<DecryptedPendingMember> selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList,
selfUuid);
if (selfPendingMember.isPresent()) {
return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
} else {
return ejectMembers(groupInfoV2, Set.of(selfUuid));
}
}
public GroupChange joinGroup(
GroupMasterKey groupMasterKey,
GroupLinkPassword groupLinkPassword,
DecryptedGroupJoinInfo decryptedGroupJoinInfo
) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
final SignalServiceAddress selfAddress = this.selfAddressProvider.getSelfAddress();
final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
selfAddress);
if (profileKeyCredential == null) {
throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
}
boolean requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink()
== AccessControl.AccessRequired.ADMINISTRATOR;
GroupChange.Actions.Builder change = requestToJoin
? groupOperations.createGroupJoinRequest(profileKeyCredential)
: groupOperations.createGroupJoinDirect(profileKeyCredential);
change.setSourceUuid(UuidUtil.toByteString(selfAddress.getUuid().get()));
return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
}
public Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
final SignalServiceAddress selfAddress = this.selfAddressProvider.getSelfAddress();
final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
selfAddress);
if (profileKeyCredential == null) {
throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
}
final GroupChange.Actions.Builder change = groupOperations.createAcceptInviteChange(profileKeyCredential);
final Optional<UUID> uuid = selfAddress.getUuid();
if (uuid.isPresent()) {
change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
}
return commitChange(groupInfoV2, change);
}
public Pair<DecryptedGroup, GroupChange> revokeInvites(
GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
final Set<UuidCiphertext> uuidCipherTexts = pendingMembers.stream().map(member -> {
try {
return new UuidCiphertext(member.getUuidCipherText().toByteArray());
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}).collect(Collectors.toSet());
return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
}
public Pair<DecryptedGroup, GroupChange> ejectMembers(GroupInfoV2 groupInfoV2, Set<UUID> uuids) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
}
private Pair<DecryptedGroup, GroupChange> commitChange(
GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
final DecryptedGroup previousGroupState = groupInfoV2.getGroup();
final int nextRevision = previousGroupState.getRevision() + 1;
final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
final DecryptedGroupChange decryptedChange;
final DecryptedGroup decryptedGroupState;
try {
decryptedChange = groupOperations.decryptChange(changeActions,
selfAddressProvider.getSelfAddress().getUuid().get());
decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
throw new IOException(e);
}
GroupChange signedGroupChange = groupsV2Api.patchGroup(changeActions,
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
Optional.absent());
return new Pair<>(decryptedGroupState, signedGroupChange);
}
private GroupChange commitChange(
GroupSecretParams groupSecretParams,
int currentRevision,
GroupChange.Actions.Builder change,
GroupLinkPassword password
) throws IOException {
final int nextRevision = currentRevision + 1;
final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
return groupsV2Api.patchGroup(changeActions,
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
}
public DecryptedGroup getUpdatedDecryptedGroup(
DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
) {
try {
final DecryptedGroupChange decryptedGroupChange = getDecryptedGroupChange(signedGroupChange,
groupMasterKey);
if (decryptedGroupChange == null) {
return null;
}
return DecryptedGroupUtil.apply(group, decryptedGroupChange);
} catch (NotAbleToApplyGroupV2ChangeException e) {
return null;
}
}
private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
if (signedGroupChange != null) {
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(
groupMasterKey));
try {
return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
} catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
return null;
}
}
return null;
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
public interface MessagePipeProvider {
SignalServiceMessagePipe getMessagePipe(boolean unidentified);
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
public interface MessageReceiverProvider {
SignalServiceMessageReceiver getMessageReceiver();
}

View file

@ -0,0 +1,123 @@
package org.asamk.signal.manager.helper;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.internal.util.concurrent.CascadingFuture;
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public final class ProfileHelper {
private final ProfileKeyProvider profileKeyProvider;
private final UnidentifiedAccessProvider unidentifiedAccessProvider;
private final MessagePipeProvider messagePipeProvider;
private final MessageReceiverProvider messageReceiverProvider;
public ProfileHelper(
final ProfileKeyProvider profileKeyProvider,
final UnidentifiedAccessProvider unidentifiedAccessProvider,
final MessagePipeProvider messagePipeProvider,
final MessageReceiverProvider messageReceiverProvider
) {
this.profileKeyProvider = profileKeyProvider;
this.unidentifiedAccessProvider = unidentifiedAccessProvider;
this.messagePipeProvider = messagePipeProvider;
this.messageReceiverProvider = messageReceiverProvider;
}
public ProfileAndCredential retrieveProfileSync(
SignalServiceAddress recipient, SignalServiceProfile.RequestType requestType
) throws IOException {
try {
return retrieveProfile(recipient, requestType).get(10, TimeUnit.SECONDS);
} catch (ExecutionException e) {
if (e.getCause() instanceof PushNetworkException) {
throw (PushNetworkException) e.getCause();
} else if (e.getCause() instanceof NotFoundException) {
throw (NotFoundException) e.getCause();
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new PushNetworkException(e);
}
}
public ListenableFuture<ProfileAndCredential> retrieveProfile(
SignalServiceAddress address, SignalServiceProfile.RequestType requestType
) {
Optional<UnidentifiedAccess> unidentifiedAccess = getUnidentifiedAccess(address);
Optional<ProfileKey> profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(address));
if (unidentifiedAccess.isPresent()) {
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address,
profileKey,
unidentifiedAccess,
requestType),
() -> getSocketRetrievalFuture(address, profileKey, unidentifiedAccess, requestType),
() -> getPipeRetrievalFuture(address, profileKey, Optional.absent(), requestType),
() -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)),
e -> !(e instanceof NotFoundException));
} else {
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address,
profileKey,
Optional.absent(),
requestType), () -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)),
e -> !(e instanceof NotFoundException));
}
}
private ListenableFuture<ProfileAndCredential> getPipeRetrievalFuture(
SignalServiceAddress address,
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType
) throws IOException {
SignalServiceMessagePipe unidentifiedPipe = messagePipeProvider.getMessagePipe(true);
SignalServiceMessagePipe pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent()
? unidentifiedPipe
: messagePipeProvider.getMessagePipe(false);
if (pipe != null) {
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
}
throw new IOException("No pipe available!");
}
private ListenableFuture<ProfileAndCredential> getSocketRetrievalFuture(
SignalServiceAddress address,
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType
) {
SignalServiceMessageReceiver receiver = messageReceiverProvider.getMessageReceiver();
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
}
private Optional<UnidentifiedAccess> getUnidentifiedAccess(SignalServiceAddress recipient) {
Optional<UnidentifiedAccessPair> unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipient);
if (unidentifiedAccess.isPresent()) {
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
}
return Optional.absent();
}
}

View file

@ -0,0 +1,9 @@
package org.asamk.signal.manager.helper;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface ProfileKeyCredentialProvider {
ProfileKeyCredential getProfileKeyCredential(SignalServiceAddress address);
}

View file

@ -0,0 +1,9 @@
package org.asamk.signal.manager.helper;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface ProfileKeyProvider {
ProfileKey getProfileKey(SignalServiceAddress address);
}

View file

@ -0,0 +1,9 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.storage.profiles.SignalProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface ProfileProvider {
SignalProfile getProfile(SignalServiceAddress address);
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface SelfAddressProvider {
SignalServiceAddress getSelfAddress();
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.signal.zkgroup.profiles.ProfileKey;
public interface SelfProfileKeyProvider {
ProfileKey getProfileKey();
}

View file

@ -0,0 +1,105 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.storage.profiles.SignalProfile;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import static org.whispersystems.signalservice.internal.util.Util.getSecretBytes;
public class UnidentifiedAccessHelper {
private final SelfProfileKeyProvider selfProfileKeyProvider;
private final ProfileKeyProvider profileKeyProvider;
private final ProfileProvider profileProvider;
private final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider;
public UnidentifiedAccessHelper(
final SelfProfileKeyProvider selfProfileKeyProvider,
final ProfileKeyProvider profileKeyProvider,
final ProfileProvider profileProvider,
final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider
) {
this.selfProfileKeyProvider = selfProfileKeyProvider;
this.profileKeyProvider = profileKeyProvider;
this.profileProvider = profileProvider;
this.senderCertificateProvider = senderCertificateProvider;
}
public byte[] getSelfUnidentifiedAccessKey() {
return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey());
}
public byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
ProfileKey theirProfileKey = profileKeyProvider.getProfileKey(recipient);
if (theirProfileKey == null) {
return null;
}
SignalProfile targetProfile = profileProvider.getProfile(recipient);
if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) {
return null;
}
if (targetProfile.isUnrestrictedUnidentifiedAccess()) {
return createUnrestrictedUnidentifiedAccess();
}
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
}
public Optional<UnidentifiedAccessPair> getAccessForSync() {
byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
byte[] selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
return Optional.absent();
}
try {
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(selfUnidentifiedAccessKey,
selfUnidentifiedAccessCertificate),
new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)));
} catch (InvalidCertificateException e) {
return Optional.absent();
}
}
public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) {
return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
}
public Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) {
byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
byte[] selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
if (recipientUnidentifiedAccessKey == null
|| selfUnidentifiedAccessKey == null
|| selfUnidentifiedAccessCertificate == null) {
return Optional.absent();
}
try {
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(recipientUnidentifiedAccessKey,
selfUnidentifiedAccessCertificate),
new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)));
} catch (InvalidCertificateException e) {
return Optional.absent();
}
}
private static byte[] createUnrestrictedUnidentifiedAccess() {
return getSecretBytes(16);
}
}

View file

@ -0,0 +1,10 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface UnidentifiedAccessProvider {
Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress address);
}

View file

@ -0,0 +1,6 @@
package org.asamk.signal.manager.helper;
public interface UnidentifiedAccessSenderCertificateProvider {
byte[] getSenderCertificate();
}

View file

@ -10,9 +10,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.contacts.JsonContactsStore;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.storage.groups.GroupInfoV1;
import org.asamk.signal.storage.groups.JsonGroupStore;
import org.asamk.signal.storage.profiles.ProfileStore;
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
@ -20,12 +22,15 @@ import org.asamk.signal.storage.protocol.JsonSignalProtocolStore;
import org.asamk.signal.storage.protocol.RecipientStore;
import org.asamk.signal.storage.protocol.SessionInfo;
import org.asamk.signal.storage.protocol.SignalServiceAddressResolver;
import org.asamk.signal.storage.stickers.StickerStore;
import org.asamk.signal.storage.threads.LegacyJsonThreadStore;
import org.asamk.signal.storage.threads.ThreadInfo;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
@ -50,6 +55,8 @@ import java.util.stream.Collectors;
public class SignalAccount implements Closeable {
final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
private final ObjectMapper jsonProcessor = new ObjectMapper();
private final FileChannel fileChannel;
private final FileLock lock;
@ -71,6 +78,7 @@ public class SignalAccount implements Closeable {
private JsonContactsStore contactStore;
private RecipientStore recipientStore;
private ProfileStore profileStore;
private StickerStore stickerStore;
private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
this.fileChannel = fileChannel;
@ -82,12 +90,12 @@ public class SignalAccount implements Closeable {
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}
public static SignalAccount load(String dataPath, String username) throws IOException {
final String fileName = getFileName(dataPath, username);
public static SignalAccount load(File dataPath, String username) throws IOException {
final File fileName = getFileName(dataPath, username);
final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
try {
SignalAccount account = new SignalAccount(pair.first(), pair.second());
account.load();
account.load(dataPath);
return account;
} catch (Throwable e) {
pair.second().close();
@ -96,10 +104,12 @@ public class SignalAccount implements Closeable {
}
}
public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey) throws IOException {
public static SignalAccount create(
File dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey
) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
String fileName = getFileName(dataPath, username);
if (!new File(fileName).exists()) {
File fileName = getFileName(dataPath, username);
if (!fileName.exists()) {
IOUtils.createPrivateFile(fileName);
}
@ -109,19 +119,30 @@ public class SignalAccount implements Closeable {
account.username = username;
account.profileKey = profileKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.groupStore = new JsonGroupStore();
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.contactStore = new JsonContactsStore();
account.recipientStore = new RecipientStore();
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
account.registered = false;
return account;
}
public static SignalAccount createLinkedAccount(String dataPath, String username, UUID uuid, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException {
public static SignalAccount createLinkedAccount(
File dataPath,
String username,
UUID uuid,
String password,
int deviceId,
IdentityKeyPair identityKey,
int registrationId,
String signalingKey,
ProfileKey profileKey
) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
String fileName = getFileName(dataPath, username);
if (!new File(fileName).exists()) {
File fileName = getFileName(dataPath, username);
if (!fileName.exists()) {
IOUtils.createPrivateFile(fileName);
}
@ -135,29 +156,42 @@ public class SignalAccount implements Closeable {
account.deviceId = deviceId;
account.signalingKey = signalingKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.groupStore = new JsonGroupStore();
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.contactStore = new JsonContactsStore();
account.recipientStore = new RecipientStore();
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
account.registered = true;
account.isMultiDevice = true;
return account;
}
public static String getFileName(String dataPath, String username) {
return dataPath + "/" + username;
public static File getFileName(File dataPath, String username) {
return new File(dataPath, username);
}
public static boolean userExists(String dataPath, String username) {
private static File getUserPath(final File dataPath, final String username) {
return new File(dataPath, username + ".d");
}
public static File getMessageCachePath(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "msg-cache");
}
private static File getGroupCachePath(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "group-cache");
}
public static boolean userExists(File dataPath, String username) {
if (username == null) {
return false;
}
File f = new File(getFileName(dataPath, username));
File f = getFileName(dataPath, username);
return !(!f.exists() || f.isDirectory());
}
private void load() throws IOException {
private void load(File dataPath) throws IOException {
JsonNode rootNode;
synchronized (fileChannel) {
fileChannel.position(0);
@ -200,18 +234,22 @@ public class SignalAccount implements Closeable {
try {
profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()));
} catch (InvalidInputException e) {
throw new IOException("Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", e);
throw new IOException(
"Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
e);
}
}
signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"),
JsonSignalProtocolStore.class);
registered = Util.getNotNullNode(rootNode, "registered").asBoolean();
JsonNode groupStoreNode = rootNode.get("groupStore");
if (groupStoreNode != null) {
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
groupStore.groupCachePath = getGroupCachePath(dataPath, username);
}
if (groupStore == null) {
groupStore = new JsonGroupStore();
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
}
JsonNode contactStoreNode = rootNode.get("contactStore");
@ -236,9 +274,12 @@ public class SignalAccount implements Closeable {
}
for (GroupInfo group : groupStore.getGroups()) {
group.members = group.members.stream()
.map(m -> recipientStore.resolveServiceAddress(m))
.collect(Collectors.toSet());
if (group instanceof GroupInfoV1) {
GroupInfoV1 groupInfoV1 = (GroupInfoV1) group;
groupInfoV1.members = groupInfoV1.members.stream()
.map(m -> recipientStore.resolveServiceAddress(m))
.collect(Collectors.toSet());
}
}
for (SessionInfo session : signalProtocolStore.getSessions()) {
@ -258,9 +299,18 @@ public class SignalAccount implements Closeable {
profileStore = new ProfileStore();
}
JsonNode stickerStoreNode = rootNode.get("stickerStore");
if (stickerStoreNode != null) {
stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class);
}
if (stickerStore == null) {
stickerStore = new StickerStore();
}
JsonNode threadStoreNode = rootNode.get("threadStore");
if (threadStoreNode != null) {
LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.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()) {
@ -272,9 +322,9 @@ public class SignalAccount implements Closeable {
contactInfo.messageExpirationTime = thread.messageExpirationTime;
contactStore.updateContact(contactInfo);
} else {
GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id));
if (groupInfo != null) {
groupInfo.messageExpirationTime = thread.messageExpirationTime;
GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
if (groupInfo instanceof GroupInfoV1) {
((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
groupStore.updateGroup(groupInfo);
}
}
@ -305,7 +355,7 @@ public class SignalAccount implements Closeable {
.putPOJO("contactStore", contactStore)
.putPOJO("recipientStore", recipientStore)
.putPOJO("profileStore", profileStore)
;
.putPOJO("stickerStore", stickerStore);
try {
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
// Write to memory first to prevent corrupting the file in case of serialization errors
@ -319,17 +369,17 @@ public class SignalAccount implements Closeable {
}
}
} catch (Exception e) {
System.err.println(String.format("Error saving file: %s", e.getMessage()));
logger.error("Error saving file: {}", e.getMessage());
}
}
private static Pair<FileChannel, FileLock> openFileChannel(String fileName) throws IOException {
FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
FileChannel fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
FileLock lock = fileChannel.tryLock();
if (lock == null) {
System.err.println("Config file is in use by another instance, waiting…");
logger.info("Config file is in use by another instance, waiting…");
lock = fileChannel.lock();
System.err.println("Config file lock acquired.");
logger.info("Config file lock acquired.");
}
return new Pair<>(fileChannel, lock);
}
@ -370,6 +420,10 @@ public class SignalAccount implements Closeable {
return profileStore;
}
public StickerStore getStickerStore() {
return stickerStore;
}
public String getUsername() {
return username;
}

View file

@ -7,6 +7,8 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.UUID;
import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY;
public class ContactInfo {
@JsonProperty
@ -24,7 +26,7 @@ public class ContactInfo {
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
@JsonProperty
@JsonProperty(access = WRITE_ONLY)
public String profileKey;
@JsonProperty(defaultValue = "false")

View file

@ -1,123 +1,63 @@
package org.asamk.signal.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class GroupInfo {
public abstract class GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty
public final byte[] groupId;
@JsonProperty
public String name;
@JsonProperty
@JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public Set<SignalServiceAddress> members = new HashSet<>();
@JsonProperty
public String color;
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
@JsonProperty(defaultValue = "false")
public boolean blocked;
@JsonProperty
public Integer inboxPosition;
@JsonProperty(defaultValue = "false")
public boolean archived;
private long avatarId;
@JsonProperty
@JsonIgnore
private boolean active;
public abstract GroupId getGroupId();
public GroupInfo(byte[] groupId) {
this.groupId = groupId;
}
@JsonIgnore
public abstract String getTitle();
public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection<SignalServiceAddress> members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived, @JsonProperty("messageExpirationTime") int messageExpirationTime) {
this.groupId = groupId;
this.name = name;
this.members.addAll(members);
this.avatarId = avatarId;
this.color = color;
this.blocked = blocked;
this.inboxPosition = inboxPosition;
this.archived = archived;
this.messageExpirationTime = messageExpirationTime;
@JsonIgnore
public abstract GroupInviteLinkUrl getGroupInviteLink();
@JsonIgnore
public abstract Set<SignalServiceAddress> getMembers();
@JsonIgnore
public Set<SignalServiceAddress> getPendingMembers() {
return Set.of();
}
@JsonIgnore
public long getAvatarId() {
return avatarId;
public Set<SignalServiceAddress> getRequestingMembers() {
return Set.of();
}
@JsonIgnore
public Set<SignalServiceAddress> getMembers() {
return members;
}
public abstract boolean isBlocked();
@JsonIgnore
public Set<String> getMembersE164() {
Set<String> membersE164 = new HashSet<>();
for (SignalServiceAddress member : members) {
if (!member.getNumber().isPresent()) {
continue;
}
membersE164.add(member.getNumber().get());
}
return membersE164;
}
public abstract void setBlocked(boolean blocked);
@JsonIgnore
public abstract int getMessageExpirationTime();
@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;
return getMembers().stream().filter(member -> !member.matches(address)).collect(Collectors.toSet());
}
public void addMembers(Collection<SignalServiceAddress> addresses) {
for (SignalServiceAddress address : addresses) {
if (this.members.contains(address)) {
continue;
}
removeMember(address);
this.members.add(address);
}
}
public void removeMember(SignalServiceAddress address) {
this.members.removeIf(member -> member.matches(address));
@JsonIgnore
public Set<SignalServiceAddress> getMembersIncludingPendingWithout(SignalServiceAddress address) {
return Stream.concat(getMembers().stream(), getPendingMembers().stream())
.filter(member -> !member.matches(address))
.collect(Collectors.toSet());
}
@JsonIgnore
public boolean isMember(SignalServiceAddress address) {
for (SignalServiceAddress member : this.members) {
for (SignalServiceAddress member : getMembers()) {
if (member.matches(address)) {
return true;
}
@ -125,61 +65,13 @@ public class GroupInfo {
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());
}
@JsonIgnore
public boolean isPendingMember(SignalServiceAddress address) {
for (SignalServiceAddress member : getPendingMembers()) {
if (member.matches(address)) {
return true;
}
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;
}
return false;
}
}

View file

@ -0,0 +1,212 @@
package org.asamk.signal.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdV1;
import org.asamk.signal.manager.GroupIdV2;
import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.asamk.signal.manager.GroupUtils;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class GroupInfoV1 extends GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
private final GroupIdV1 groupId;
private GroupIdV2 expectedV2Id;
@JsonProperty
public String name;
@JsonProperty
@JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public Set<SignalServiceAddress> members = new HashSet<>();
@JsonProperty
public String color;
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
@JsonProperty(defaultValue = "false")
public boolean blocked;
@JsonProperty
public Integer inboxPosition;
@JsonProperty(defaultValue = "false")
public boolean archived;
public GroupInfoV1(GroupIdV1 groupId) {
this.groupId = groupId;
}
public GroupInfoV1(
@JsonProperty("groupId") byte[] groupId,
@JsonProperty("expectedV2Id") byte[] expectedV2Id,
@JsonProperty("name") String name,
@JsonProperty("members") Collection<SignalServiceAddress> members,
@JsonProperty("avatarId") long _ignored_avatarId,
@JsonProperty("color") String color,
@JsonProperty("blocked") boolean blocked,
@JsonProperty("inboxPosition") Integer inboxPosition,
@JsonProperty("archived") boolean archived,
@JsonProperty("messageExpirationTime") int messageExpirationTime,
@JsonProperty("active") boolean _ignored_active
) {
this.groupId = GroupId.v1(groupId);
this.expectedV2Id = GroupId.v2(expectedV2Id);
this.name = name;
this.members.addAll(members);
this.color = color;
this.blocked = blocked;
this.inboxPosition = inboxPosition;
this.archived = archived;
this.messageExpirationTime = messageExpirationTime;
}
@Override
@JsonIgnore
public GroupIdV1 getGroupId() {
return groupId;
}
@JsonProperty("groupId")
private byte[] getGroupIdJackson() {
return groupId.serialize();
}
@JsonIgnore
public GroupIdV2 getExpectedV2Id() {
if (expectedV2Id == null) {
expectedV2Id = GroupUtils.getGroupIdV2(groupId);
}
return expectedV2Id;
}
@JsonProperty("expectedV2Id")
private byte[] getExpectedV2IdJackson() {
return expectedV2Id.serialize();
}
@Override
public String getTitle() {
return name;
}
@Override
public GroupInviteLinkUrl getGroupInviteLink() {
return null;
}
@JsonIgnore
public Set<SignalServiceAddress> getMembers() {
return members;
}
@Override
public boolean isBlocked() {
return blocked;
}
@Override
public void setBlocked(final boolean blocked) {
this.blocked = blocked;
}
@Override
public int getMessageExpirationTime() {
return messageExpirationTime;
}
public void addMembers(Collection<SignalServiceAddress> addresses) {
for (SignalServiceAddress address : addresses) {
if (this.members.contains(address)) {
continue;
}
removeMember(address);
this.members.add(address);
}
}
public void removeMember(SignalServiceAddress address) {
this.members.removeIf(member -> member.matches(address));
}
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

@ -0,0 +1,115 @@
package org.asamk.signal.storage.groups;
import org.asamk.signal.manager.GroupIdV2;
import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
public class GroupInfoV2 extends GroupInfo {
private final GroupIdV2 groupId;
private final GroupMasterKey masterKey;
private boolean blocked;
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
this.groupId = groupId;
this.masterKey = masterKey;
}
@Override
public GroupIdV2 getGroupId() {
return groupId;
}
public GroupMasterKey getMasterKey() {
return masterKey;
}
public void setGroup(final DecryptedGroup group) {
this.group = group;
}
public DecryptedGroup getGroup() {
return group;
}
@Override
public String getTitle() {
if (this.group == null) {
return null;
}
return this.group.getTitle();
}
@Override
public GroupInviteLinkUrl getGroupInviteLink() {
if (this.group == null || this.group.getInviteLinkPassword() == null || (
this.group.getAccessControl().getAddFromInviteLink() != AccessControl.AccessRequired.ANY
&& this.group.getAccessControl().getAddFromInviteLink()
!= AccessControl.AccessRequired.ADMINISTRATOR
)) {
return null;
}
return GroupInviteLinkUrl.forGroup(masterKey, group);
}
@Override
public Set<SignalServiceAddress> getMembers() {
if (this.group == null) {
return Collections.emptySet();
}
return group.getMembersList()
.stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
.collect(Collectors.toSet());
}
@Override
public Set<SignalServiceAddress> getPendingMembers() {
if (this.group == null) {
return Collections.emptySet();
}
return group.getPendingMembersList()
.stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
.collect(Collectors.toSet());
}
@Override
public Set<SignalServiceAddress> getRequestingMembers() {
if (this.group == null) {
return Collections.emptySet();
}
return group.getRequestingMembersList()
.stream()
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
.collect(Collectors.toSet());
}
@Override
public boolean isBlocked() {
return blocked;
}
@Override
public void setBlocked(final boolean blocked) {
this.blocked = blocked;
}
@Override
public int getMessageExpirationTime() {
return this.group != null && this.group.hasDisappearingMessagesTimer()
? this.group.getDisappearingMessagesTimer().getDuration()
: 0;
}
}

View file

@ -12,58 +12,191 @@ import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdV1;
import org.asamk.signal.manager.GroupIdV2;
import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.IOUtils;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.util.Base64;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonGroupStore {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
public static List<GroupInfo> groupsWithLegacyAvatarId = new ArrayList<>();
private static final ObjectMapper jsonProcessor = new ObjectMapper();
public File groupCachePath;
@JsonProperty("groups")
@JsonSerialize(using = JsonGroupStore.MapToListSerializer.class)
@JsonDeserialize(using = JsonGroupStore.GroupsDeserializer.class)
private Map<String, GroupInfo> groups = new HashMap<>();
@JsonSerialize(using = GroupsSerializer.class)
@JsonDeserialize(using = GroupsDeserializer.class)
private final Map<GroupId, GroupInfo> groups = new HashMap<>();
private JsonGroupStore() {
}
public JsonGroupStore(final File groupCachePath) {
this.groupCachePath = groupCachePath;
}
public void updateGroup(GroupInfo group) {
groups.put(Base64.encodeBytes(group.groupId), group);
}
public GroupInfo getGroup(byte[] groupId) {
return groups.get(Base64.encodeBytes(groupId));
}
public List<GroupInfo> getGroups() {
return new ArrayList<>(groups.values());
}
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());
groups.put(group.getGroupId(), group);
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
try {
IOUtils.createPrivateDirectories(groupCachePath);
try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.getGroupId()))) {
((GroupInfoV2) group).getGroup().writeTo(stream);
}
final File groupFileLegacy = getGroupFileLegacy(group.getGroupId());
if (groupFileLegacy.exists()) {
groupFileLegacy.delete();
}
} catch (IOException e) {
logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
}
}
}
private static class GroupsDeserializer extends JsonDeserializer<Map<String, GroupInfo>> {
public void deleteGroup(GroupId groupId) {
groups.remove(groupId);
}
public GroupInfo getGroup(GroupId groupId) {
GroupInfo group = groups.get(groupId);
if (group == null) {
if (groupId instanceof GroupIdV1) {
group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId));
} else if (groupId instanceof GroupIdV2) {
group = getGroupV1ByV2Id((GroupIdV2) groupId);
}
}
loadDecryptedGroup(group);
return group;
}
private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) {
for (GroupInfo g : groups.values()) {
if (g instanceof GroupInfoV1) {
final GroupInfoV1 gv1 = (GroupInfoV1) g;
if (groupIdV2.equals(gv1.getExpectedV2Id())) {
return gv1;
}
}
}
return null;
}
private void loadDecryptedGroup(final GroupInfo group) {
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
File groupFile = getGroupFile(group.getGroupId());
if (!groupFile.exists()) {
groupFile = getGroupFileLegacy(group.getGroupId());
}
if (!groupFile.exists()) {
return;
}
try (FileInputStream stream = new FileInputStream(groupFile)) {
((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream));
} catch (IOException ignored) {
}
}
}
private File getGroupFileLegacy(final GroupId groupId) {
return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
}
private File getGroupFile(final GroupId groupId) {
return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
}
public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
GroupInfo group = getGroup(groupId);
if (group instanceof GroupInfoV1) {
return (GroupInfoV1) group;
}
if (group == null) {
return new GroupInfoV1(groupId);
}
return null;
}
public List<GroupInfo> getGroups() {
final Collection<GroupInfo> groups = this.groups.values();
for (GroupInfo group : groups) {
loadDecryptedGroup(group);
}
return new ArrayList<>(groups);
}
private static class GroupsSerializer extends JsonSerializer<Map<String, GroupInfo>> {
@Override
public Map<String, GroupInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
Map<String, GroupInfo> groups = new HashMap<>();
public void serialize(
final Map<String, GroupInfo> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
final Collection<GroupInfo> groups = value.values();
jgen.writeStartArray(groups.size());
for (GroupInfo group : groups) {
if (group instanceof GroupInfoV1) {
jgen.writeObject(group);
} else if (group instanceof GroupInfoV2) {
final GroupInfoV2 groupV2 = (GroupInfoV2) group;
jgen.writeStartObject();
jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
jgen.writeStringField("masterKey", Base64.encodeBytes(groupV2.getMasterKey().serialize()));
jgen.writeBooleanField("blocked", groupV2.isBlocked());
jgen.writeEndObject();
} else {
throw new AssertionError("Unknown group version");
}
}
jgen.writeEndArray();
}
}
private static class GroupsDeserializer extends JsonDeserializer<Map<GroupId, GroupInfo>> {
@Override
public Map<GroupId, GroupInfo> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
Map<GroupId, GroupInfo> groups = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
GroupInfo g = jsonProcessor.treeToValue(n, GroupInfo.class);
// Check if a legacy avatarId exists
if (g.getAvatarId() != 0) {
groupsWithLegacyAvatarId.add(g);
GroupInfo g;
if (n.has("masterKey")) {
// a v2 group
GroupIdV2 groupId = GroupIdV2.fromBase64(n.get("groupId").asText());
try {
GroupMasterKey masterKey = new GroupMasterKey(Base64.decode(n.get("masterKey").asText()));
g = new GroupInfoV2(groupId, masterKey);
} catch (InvalidInputException e) {
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
}
g.setBlocked(n.get("blocked").asBoolean(false));
} else {
GroupInfoV1 gv1 = jsonProcessor.treeToValue(n, GroupInfoV1.class);
g = gv1;
}
groups.put(Base64.encodeBytes(g.groupId), g);
groups.put(g.getGroupId(), g);
}
return groups;

View file

@ -14,13 +14,13 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
@ -33,7 +33,7 @@ public class ProfileStore {
@JsonSerialize(using = ProfileStoreSerializer.class)
private final List<SignalProfileEntry> profiles = new ArrayList<>();
public SignalProfileEntry getProfile(SignalServiceAddress serviceAddress) {
public SignalProfileEntry getProfileEntry(SignalServiceAddress serviceAddress) {
for (SignalProfileEntry entry : profiles) {
if (entry.getServiceAddress().matches(serviceAddress)) {
return entry;
@ -42,8 +42,27 @@ public class ProfileStore {
return null;
}
public void updateProfile(SignalServiceAddress serviceAddress, ProfileKey profileKey, long now, SignalProfile profile) {
SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile);
public ProfileKey getProfileKey(SignalServiceAddress serviceAddress) {
for (SignalProfileEntry entry : profiles) {
if (entry.getServiceAddress().matches(serviceAddress)) {
return entry.getProfileKey();
}
}
return null;
}
public void updateProfile(
SignalServiceAddress serviceAddress,
ProfileKey profileKey,
long now,
SignalProfile profile,
ProfileKeyCredential profileKeyCredential
) {
SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress,
profileKey,
now,
profile,
profileKeyCredential);
for (int i = 0; i < profiles.size(); i++) {
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
profiles.set(i, newEntry);
@ -54,31 +73,55 @@ public class ProfileStore {
profiles.add(newEntry);
}
public void storeProfileKey(SignalServiceAddress serviceAddress, ProfileKey profileKey) {
SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress, profileKey, 0, null, null);
for (int i = 0; i < profiles.size(); i++) {
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
if (!profiles.get(i).getProfileKey().equals(profileKey)) {
profiles.set(i, newEntry);
}
return;
}
}
profiles.add(newEntry);
}
public static class ProfileStoreDeserializer extends JsonDeserializer<List<SignalProfileEntry>> {
@Override
public List<SignalProfileEntry> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
public List<SignalProfileEntry> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
List<SignalProfileEntry> addresses = new ArrayList<>();
if (node.isArray()) {
for (JsonNode entry : node) {
String name = entry.hasNonNull("name")
? entry.get("name").asText()
: null;
UUID uuid = entry.hasNonNull("uuid")
? UuidUtil.parseOrNull(entry.get("uuid").asText())
: null;
String name = entry.hasNonNull("name") ? entry.get("name").asText() : null;
UUID uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null;
final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, name);
ProfileKey profileKey = null;
try {
profileKey = new ProfileKey(Base64.decode(entry.get("profileKey").asText()));
} catch (InvalidInputException ignored) {
}
ProfileKeyCredential profileKeyCredential = null;
if (entry.hasNonNull("profileKeyCredential")) {
try {
profileKeyCredential = new ProfileKeyCredential(Base64.decode(entry.get(
"profileKeyCredential").asText()));
} catch (Throwable ignored) {
}
}
long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();
SignalProfile profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class);
addresses.add(new SignalProfileEntry(serviceAddress, profileKey, lastUpdateTimestamp, profile));
addresses.add(new SignalProfileEntry(serviceAddress,
profileKey,
lastUpdateTimestamp,
profile,
profileKeyCredential));
}
}
@ -89,7 +132,9 @@ public class ProfileStore {
public static class ProfileStoreSerializer extends JsonSerializer<List<SignalProfileEntry>> {
@Override
public void serialize(List<SignalProfileEntry> profiles, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
public void serialize(
List<SignalProfileEntry> profiles, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartArray();
for (SignalProfileEntry profileEntry : profiles) {
final SignalServiceAddress address = profileEntry.getServiceAddress();
@ -103,6 +148,10 @@ public class ProfileStore {
json.writeStringField("profileKey", Base64.encodeBytes(profileEntry.getProfileKey().serialize()));
json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp());
json.writeObjectField("profile", profileEntry.getProfile());
if (profileEntry.getProfileKeyCredential() != null) {
json.writeStringField("profileKeyCredential",
Base64.encodeBytes(profileEntry.getProfileKeyCredential().serialize()));
}
json.writeEndObject();
}
json.writeEndArray();

View file

@ -1,5 +1,6 @@
package org.asamk.signal.storage.profiles;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
@ -23,18 +24,34 @@ public class SignalProfile {
private final boolean unrestrictedUnidentifiedAccess;
@JsonProperty
private final SignalServiceProfile.Capabilities capabilities;
private final Capabilities capabilities;
public SignalProfile(final String identityKey, final String name, final File avatarFile, final String unidentifiedAccess, final boolean unrestrictedUnidentifiedAccess, final SignalServiceProfile.Capabilities capabilities) {
public SignalProfile(
final String identityKey,
final String name,
final File avatarFile,
final String unidentifiedAccess,
final boolean unrestrictedUnidentifiedAccess,
final SignalServiceProfile.Capabilities capabilities
) {
this.identityKey = identityKey;
this.name = name;
this.avatarFile = avatarFile;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = capabilities;
this.capabilities = new Capabilities();
this.capabilities.storage = capabilities.isStorage();
this.capabilities.gv1Migration = capabilities.isGv1Migration();
this.capabilities.gv2 = capabilities.isGv2();
}
public SignalProfile(@JsonProperty("identityKey") final String identityKey, @JsonProperty("name") final String name, @JsonProperty("unidentifiedAccess") final String unidentifiedAccess, @JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess, @JsonProperty("capabilities") final SignalServiceProfile.Capabilities capabilities) {
public SignalProfile(
@JsonProperty("identityKey") final String identityKey,
@JsonProperty("name") final String name,
@JsonProperty("unidentifiedAccess") final String unidentifiedAccess,
@JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess,
@JsonProperty("capabilities") final Capabilities capabilities
) {
this.identityKey = identityKey;
this.name = name;
this.avatarFile = null;
@ -63,19 +80,43 @@ public class SignalProfile {
return unrestrictedUnidentifiedAccess;
}
public SignalServiceProfile.Capabilities getCapabilities() {
public Capabilities getCapabilities() {
return capabilities;
}
@Override
public String toString() {
return "SignalProfile{" +
"identityKey='" + identityKey + '\'' +
", name='" + name + '\'' +
", avatarFile=" + avatarFile +
", unidentifiedAccess='" + unidentifiedAccess + '\'' +
", unrestrictedUnidentifiedAccess=" + unrestrictedUnidentifiedAccess +
", capabilities=" + capabilities +
'}';
return "SignalProfile{"
+ "identityKey='"
+ identityKey
+ '\''
+ ", name='"
+ name
+ '\''
+ ", avatarFile="
+ avatarFile
+ ", unidentifiedAccess='"
+ unidentifiedAccess
+ '\''
+ ", unrestrictedUnidentifiedAccess="
+ unrestrictedUnidentifiedAccess
+ ", capabilities="
+ capabilities
+ '}';
}
public static class Capabilities {
@JsonIgnore
public boolean uuid;
@JsonProperty
public boolean gv2;
@JsonProperty
public boolean storage;
@JsonProperty
public boolean gv1Migration;
}
}

View file

@ -1,6 +1,7 @@
package org.asamk.signal.storage.profiles;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SignalProfileEntry {
@ -13,11 +14,22 @@ public class SignalProfileEntry {
private final SignalProfile profile;
public SignalProfileEntry(final SignalServiceAddress serviceAddress, final ProfileKey profileKey, final long lastUpdateTimestamp, final SignalProfile profile) {
private final ProfileKeyCredential profileKeyCredential;
private boolean requestPending;
public SignalProfileEntry(
final SignalServiceAddress serviceAddress,
final ProfileKey profileKey,
final long lastUpdateTimestamp,
final SignalProfile profile,
final ProfileKeyCredential profileKeyCredential
) {
this.serviceAddress = serviceAddress;
this.profileKey = profileKey;
this.lastUpdateTimestamp = lastUpdateTimestamp;
this.profile = profile;
this.profileKeyCredential = profileKeyCredential;
}
public SignalServiceAddress getServiceAddress() {
@ -35,4 +47,16 @@ public class SignalProfileEntry {
public SignalProfile getProfile() {
return profile;
}
public ProfileKeyCredential getProfileKeyCredential() {
return profileKeyCredential;
}
public boolean isRequestPending() {
return requestPending;
}
public void setRequestPending(final boolean requestPending) {
this.requestPending = requestPending;
}
}

View file

@ -10,6 +10,8 @@ import com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@ -27,6 +29,8 @@ import java.util.UUID;
public class JsonIdentityKeyStore implements IdentityKeyStore {
final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class);
private final List<Identity> identities = new ArrayList<>();
private final IdentityKeyPair identityKeyPair;
@ -63,7 +67,10 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return saveIdentity(resolveSignalServiceAddress(address.getName()), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null);
return saveIdentity(resolveSignalServiceAddress(address.getName()),
identityKey,
TrustLevel.TRUSTED_UNVERIFIED,
null);
}
/**
@ -75,7 +82,9 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
* @param trustLevel Level of trust: untrusted, trusted, trusted and verified
* @param added Added timestamp, if null and the key is newly added, the current time is used.
*/
public boolean saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added) {
public boolean saveIdentity(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added
) {
for (Identity id : identities) {
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue;
@ -99,7 +108,9 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
* @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) {
public void setIdentityTrustLevel(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel
) {
for (Identity id : identities) {
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue;
@ -178,7 +189,9 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<JsonIdentityKeyStore> {
@Override
public JsonIdentityKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
public JsonIdentityKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
try {
@ -190,28 +203,27 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
JsonNode trustedKeysNode = node.get("trustedKeys");
if (trustedKeysNode.isArray()) {
for (JsonNode trustedKey : trustedKeysNode) {
String trustedKeyName = trustedKey.hasNonNull("name")
? trustedKey.get("name").asText()
: null;
String trustedKeyName = trustedKey.hasNonNull("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;
UUID uuid = trustedKey.hasNonNull("uuid") ? UuidUtil.parseOrNull(trustedKey.get("uuid")
.asText()) : null;
final SignalServiceAddress serviceAddress = uuid == null
? Util.getSignalServiceAddressFromIdentifier(trustedKeyName)
: new SignalServiceAddress(uuid, trustedKeyName);
try {
IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0);
TrustLevel trustLevel = trustedKey.has("trustLevel") ? TrustLevel.fromInt(trustedKey.get("trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
Date added = trustedKey.has("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp").asLong()) : new Date();
TrustLevel trustLevel = trustedKey.has("trustLevel") ? TrustLevel.fromInt(trustedKey.get(
"trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
Date added = trustedKey.has("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp")
.asLong()) : new Date();
keyStore.saveIdentity(serviceAddress, id, trustLevel, added);
} catch (InvalidKeyException | IOException e) {
System.out.println(String.format("Error while decoding key for: %s", trustedKeyName));
logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
}
}
}
@ -226,10 +238,13 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
public static class JsonIdentityKeyStoreSerializer extends JsonSerializer<JsonIdentityKeyStore> {
@Override
public void serialize(JsonIdentityKeyStore jsonIdentityKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
public void serialize(
JsonIdentityKeyStore jsonIdentityKeyStore, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartObject();
json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId());
json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
json.writeStringField("identityKey",
Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
json.writeArrayFieldStart("trustedKeys");
for (Identity trustedKey : jsonIdentityKeyStore.identities) {
json.writeStartObject();
@ -279,8 +294,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
}
boolean isTrusted() {
return trustLevel == TrustLevel.TRUSTED_UNVERIFIED ||
trustLevel == TrustLevel.TRUSTED_VERIFIED;
return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED;
}
public IdentityKey getIdentityKey() {

View file

@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
@ -19,6 +21,8 @@ import java.util.Map;
class JsonPreKeyStore implements PreKeyStore {
final static Logger logger = LoggerFactory.getLogger(JsonPreKeyStore.class);
private final Map<Integer, byte[]> store = new HashMap<>();
public JsonPreKeyStore() {
@ -60,7 +64,9 @@ class JsonPreKeyStore implements PreKeyStore {
public static class JsonPreKeyStoreDeserializer extends JsonDeserializer<JsonPreKeyStore> {
@Override
public JsonPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
public JsonPreKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Map<Integer, byte[]> preKeyMap = new HashMap<>();
@ -70,7 +76,7 @@ class JsonPreKeyStore implements PreKeyStore {
try {
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
} catch (IOException e) {
System.err.println(String.format("Error while decoding prekey for: %s", preKeyId));
logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage());
}
}
}
@ -86,7 +92,9 @@ class JsonPreKeyStore implements PreKeyStore {
public static class JsonPreKeyStoreSerializer extends JsonSerializer<JsonPreKeyStore> {
@Override
public void serialize(JsonPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
public void serialize(
JsonPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartArray();
for (Map.Entry<Integer, byte[]> preKey : jsonPreKeyStore.store.entrySet()) {
json.writeStartObject();

View file

@ -9,6 +9,8 @@ import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
@ -24,6 +26,8 @@ import java.util.UUID;
class JsonSessionStore implements SessionStore {
final static Logger logger = LoggerFactory.getLogger(JsonSessionStore.class);
private final List<SessionInfo> sessions = new ArrayList<>();
private SignalServiceAddressResolver resolver;
@ -51,7 +55,7 @@ class JsonSessionStore implements SessionStore {
try {
return new SessionRecord(info.sessionRecord);
} catch (IOException e) {
System.err.println("Failed to load session, resetting session: " + e);
logger.warn("Failed to load session, resetting session: {}", e.getMessage());
final SessionRecord sessionRecord = new SessionRecord();
info.sessionRecord = sessionRecord.serialize();
return sessionRecord;
@ -126,24 +130,22 @@ class JsonSessionStore implements SessionStore {
public static class JsonSessionStoreDeserializer extends JsonDeserializer<JsonSessionStore> {
@Override
public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
public JsonSessionStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
JsonSessionStore sessionStore = new JsonSessionStore();
if (node.isArray()) {
for (JsonNode session : node) {
String sessionName = session.hasNonNull("name")
? session.get("name").asText()
: null;
String sessionName = session.hasNonNull("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;
UUID uuid = session.hasNonNull("uuid") ? UuidUtil.parseOrNull(session.get("uuid").asText()) : null;
final SignalServiceAddress serviceAddress = uuid == null
? Util.getSignalServiceAddressFromIdentifier(sessionName)
: new SignalServiceAddress(uuid, sessionName);
@ -153,7 +155,7 @@ class JsonSessionStore implements SessionStore {
SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record));
sessionStore.sessions.add(sessionInfo);
} catch (IOException e) {
System.err.println(String.format("Error while decoding session for: %s", sessionName));
logger.warn("Error while decoding session for {}: {}", sessionName, e.getMessage());
}
}
}
@ -165,7 +167,9 @@ class JsonSessionStore implements SessionStore {
public static class JsonSessionStoreSerializer extends JsonSerializer<JsonSessionStore> {
@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();
for (SessionInfo sessionInfo : jsonSessionStore.sessions) {
json.writeStartObject();

View file

@ -42,7 +42,12 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
public JsonSignalProtocolStore() {
}
public JsonSignalProtocolStore(JsonPreKeyStore preKeyStore, JsonSessionStore sessionStore, JsonSignedPreKeyStore signedPreKeyStore, JsonIdentityKeyStore identityKeyStore) {
public JsonSignalProtocolStore(
JsonPreKeyStore preKeyStore,
JsonSessionStore sessionStore,
JsonSignedPreKeyStore signedPreKeyStore,
JsonIdentityKeyStore identityKeyStore
) {
this.preKeyStore = preKeyStore;
this.sessionStore = sessionStore;
this.signedPreKeyStore = signedPreKeyStore;
@ -80,7 +85,9 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
identityKeyStore.saveIdentity(serviceAddress, identityKey, trustLevel, null);
}
public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
public void setIdentityTrustLevel(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel
) {
identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel);
}

View file

@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
@ -21,6 +23,8 @@ import java.util.Map;
class JsonSignedPreKeyStore implements SignedPreKeyStore {
final static Logger logger = LoggerFactory.getLogger(JsonSignedPreKeyStore.class);
private final Map<Integer, byte[]> store = new HashMap<>();
public JsonSignedPreKeyStore() {
@ -77,7 +81,9 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore {
public static class JsonSignedPreKeyStoreDeserializer extends JsonDeserializer<JsonSignedPreKeyStore> {
@Override
public JsonSignedPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
public JsonSignedPreKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Map<Integer, byte[]> preKeyMap = new HashMap<>();
@ -87,7 +93,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore {
try {
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
} catch (IOException e) {
System.err.println(String.format("Error while decoding prekey for: %s", preKeyId));
logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage());
}
}
}
@ -103,7 +109,9 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore {
public static class JsonSignedPreKeyStoreSerializer extends JsonSerializer<JsonSignedPreKeyStore> {
@Override
public void serialize(JsonSignedPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
public void serialize(
JsonSignedPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartArray();
for (Map.Entry<Integer, byte[]> signedPreKey : jsonPreKeyStore.store.entrySet()) {
json.writeStartObject();

View file

@ -49,7 +49,9 @@ public class RecipientStore {
public static class RecipientStoreDeserializer extends JsonDeserializer<Set<SignalServiceAddress>> {
@Override
public Set<SignalServiceAddress> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
public Set<SignalServiceAddress> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Set<SignalServiceAddress> addresses = new HashSet<>();
@ -70,7 +72,9 @@ public class RecipientStore {
public static class RecipientStoreSerializer extends JsonSerializer<Set<SignalServiceAddress>> {
@Override
public void serialize(Set<SignalServiceAddress> addresses, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
public void serialize(
Set<SignalServiceAddress> addresses, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartArray();
for (SignalServiceAddress address : addresses) {
json.writeStartObject();

View file

@ -0,0 +1,35 @@
package org.asamk.signal.storage.stickers;
public class Sticker {
private final byte[] packId;
private final byte[] packKey;
private boolean installed;
public Sticker(final byte[] packId, final byte[] packKey) {
this.packId = packId;
this.packKey = packKey;
}
public Sticker(final byte[] packId, final byte[] packKey, final boolean installed) {
this.packId = packId;
this.packKey = packKey;
this.installed = installed;
}
public byte[] getPackId() {
return packId;
}
public byte[] getPackKey() {
return packKey;
}
public boolean isInstalled() {
return installed;
}
public void setInstalled(final boolean installed) {
this.installed = installed;
}
}

View file

@ -0,0 +1,74 @@
package org.asamk.signal.storage.stickers;
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.util.Base64;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class StickerStore {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonSerialize(using = StickersSerializer.class)
@JsonDeserialize(using = StickersDeserializer.class)
private final Map<byte[], Sticker> stickers = new HashMap<>();
public Sticker getSticker(byte[] packId) {
return stickers.get(packId);
}
public void updateSticker(Sticker sticker) {
stickers.put(sticker.getPackId(), sticker);
}
private static class StickersSerializer extends JsonSerializer<Map<byte[], Sticker>> {
@Override
public void serialize(
final Map<byte[], Sticker> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
final Collection<Sticker> stickers = value.values();
jgen.writeStartArray(stickers.size());
for (Sticker sticker : stickers) {
jgen.writeStartObject();
jgen.writeStringField("packId", Base64.encodeBytes(sticker.getPackId()));
jgen.writeStringField("packKey", Base64.encodeBytes(sticker.getPackKey()));
jgen.writeBooleanField("installed", sticker.isInstalled());
jgen.writeEndObject();
}
jgen.writeEndArray();
}
}
private static class StickersDeserializer extends JsonDeserializer<Map<byte[], Sticker>> {
@Override
public Map<byte[], Sticker> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
Map<byte[], Sticker> stickers = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
byte[] packId = Base64.decode(n.get("packId").asText());
byte[] packKey = Base64.decode(n.get("packKey").asText());
boolean installed = n.get("installed").asBoolean(false);
stickers.put(packId, new Sticker(packId, packKey, installed));
}
return stickers;
}
}
}

View file

@ -34,7 +34,9 @@ public class LegacyJsonThreadStore {
private static class MapToListSerializer extends JsonSerializer<Map<?, ?>> {
@Override
public void serialize(final Map<?, ?> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
public void serialize(
final Map<?, ?> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
jgen.writeObject(value.values());
}
}
@ -42,7 +44,9 @@ public class LegacyJsonThreadStore {
private static class ThreadsDeserializer extends JsonDeserializer<Map<String, ThreadInfo>> {
@Override
public Map<String, ThreadInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
public Map<String, ThreadInfo> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
Map<String, ThreadInfo> threads = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {

View file

@ -1,14 +1,14 @@
package org.asamk.signal.util;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.NotAGroupMemberException;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ErrorUtils {
@ -18,20 +18,42 @@ public class ErrorUtils {
public static void handleAssertionError(AssertionError e) {
System.err.println("Failed to send/receive message (Assertion): " + e.getMessage());
e.printStackTrace();
System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
System.err.println(
"If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
}
public static void handleEncapsulatedExceptions(EncapsulatedExceptions e) {
public static int handleTimestampAndSendMessageResults(long timestamp, List<SendMessageResult> results) {
if (timestamp != 0) {
System.out.println(timestamp);
}
List<String> errors = getErrorMessagesFromSendMessageResults(results);
return handleSendMessageResultErrors(errors);
}
public static List<String> getErrorMessagesFromSendMessageResults(List<SendMessageResult> results) {
List<String> errors = new ArrayList<>();
for (SendMessageResult result : results) {
if (result.isNetworkFailure()) {
errors.add(String.format("Network failure for \"%s\"", result.getAddress().getLegacyIdentifier()));
} else if (result.isUnregisteredFailure()) {
errors.add(String.format("Unregistered user \"%s\"", result.getAddress().getLegacyIdentifier()));
} else if (result.getIdentityFailure() != null) {
errors.add(String.format("Untrusted Identity for \"%s\"", result.getAddress().getLegacyIdentifier()));
}
}
return errors;
}
private static int handleSendMessageResultErrors(List<String> errors) {
if (errors.size() == 0) {
return 0;
}
System.err.println("Failed to send (some) messages:");
for (NetworkFailureException n : e.getNetworkExceptions()) {
System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage());
}
for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) {
System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage());
}
for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) {
System.err.println("Untrusted Identity for \"" + n.getIdentifier() + "\": " + n.getMessage());
for (String error : errors) {
System.err.println(error);
}
return 3;
}
public static void handleIOException(IOException e) {

View file

@ -1,10 +0,0 @@
package org.asamk.signal.util;
import java.io.IOException;
public class GroupIdFormatException extends Exception {
public GroupIdFormatException(String groupId, IOException e) {
super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage());
}
}

View file

@ -2,9 +2,7 @@ package org.asamk.signal.util;
public class Hex {
private final static char[] HEX_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
private final static char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private Hex() {
}

View file

@ -46,8 +46,7 @@ public class IOUtils {
return baos.toByteArray();
}
public static void createPrivateDirectories(String directoryPath) throws IOException {
final File file = new File(directoryPath);
public static void createPrivateDirectories(File file) throws IOException {
if (file.exists()) {
return;
}
@ -61,8 +60,8 @@ public class IOUtils {
}
}
public static void createPrivateFile(String path) throws IOException {
final Path file = new File(path).toPath();
public static void createPrivateFile(File path) throws IOException {
final Path file = path.toPath();
try {
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE);
Files.createFile(file, PosixFilePermissions.asFileAttribute(perms));
@ -71,13 +70,13 @@ public class IOUtils {
}
}
public static String getDataHomeDir() {
public static File getDataHomeDir() {
String dataHome = System.getenv("XDG_DATA_HOME");
if (dataHome != null) {
return dataHome;
return new File(dataHome);
}
return System.getProperty("user.home") + "/.local/share";
return new File(new File(System.getProperty("user.home"), ".local"), "share");
}
public static void copyStreamToFile(InputStream input, File outputFile) throws IOException {

View file

@ -16,10 +16,13 @@ public class SecurityProvider extends Provider {
// Workaround for BKS truststore
put("KeyStore.BKS", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.Std.class.getCanonicalName());
put("KeyStore.BKS-V1", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.Version1.class.getCanonicalName());
put("KeyStore.BouncyCastle", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.BouncyCastleStore.class.getCanonicalName());
put("KeyStore.BKS-V1",
org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.Version1.class.getCanonicalName());
put("KeyStore.BouncyCastle",
org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.BouncyCastleStore.class.getCanonicalName());
put("KeyFactory.X.509", org.bouncycastle.jcajce.provider.asymmetric.x509.KeyFactory.class.getCanonicalName());
put("CertificateFactory.X.509", org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory.class.getCanonicalName());
put("CertificateFactory.X.509",
org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory.class.getCanonicalName());
}
public static class DefaultRandom extends SecureRandomSpi {

View file

@ -2,13 +2,13 @@ package org.asamk.signal.util;
import com.fasterxml.jackson.databind.JsonNode;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdFormatException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.io.InvalidObjectException;
public class Util {
@ -41,18 +41,15 @@ public class Util {
public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
JsonNode node = parent.get(name);
if (node == null) {
throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ",
name));
}
return node;
}
public static byte[] decodeGroupId(String groupId) throws GroupIdFormatException {
try {
return Base64.decode(groupId);
} catch (IOException e) {
throw new GroupIdFormatException(groupId, e);
}
public static GroupId decodeGroupId(String groupId) throws GroupIdFormatException {
return GroupId.fromBase64(groupId);
}
public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException {