Merge remote-tracking branch 'AsamK/master' into master

This commit is contained in:
user-invalid 2020-09-20 17:32:45 +02:00
commit 7aa31842dd
91 changed files with 3917 additions and 1594 deletions

View file

@ -1,33 +1,33 @@
package org.asamk;
import org.asamk.signal.AttachmentInvalidException;
import org.asamk.signal.GroupNotFoundException;
import org.freedesktop.dbus.DBusInterface;
import org.freedesktop.dbus.DBusSignal;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.freedesktop.dbus.interfaces.DBusInterface;
import org.freedesktop.dbus.messages.DBusSignal;
import java.io.IOException;
import java.util.List;
/**
* DBus interface for the org.asamk.Signal service.
* Including emitted Signals and returned Errors.
*/
public interface Signal extends DBusInterface {
void sendMessage(String message, List<String> attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
long sendMessage(String message, List<String> attachments, String recipient) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
void sendMessage(String message, List<String> attachments, List<String> recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException;
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 IOException, EncapsulatedExceptions;
void sendEndSessionMessage(List<String> recipients) throws Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
void sendGroupMessage(String message, List<String> attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException;
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 InvalidNumberException;
String getContactName(String number) throws Error.InvalidNumber;
void setContactName(String number, String name) throws InvalidNumberException;
void setContactName(String number, String name) throws Error.InvalidNumber;
void setContactBlocked(String number, boolean blocked) throws InvalidNumberException;
void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber;
void setGroupBlocked(byte[] groupId, boolean blocked) throws GroupNotFoundException;
void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound;
List<byte[]> getGroupIds();
@ -35,17 +35,17 @@ public interface Signal extends DBusInterface {
List<String> getGroupMembers(byte[] groupId);
byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException;
byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.UnregisteredUser, Error.UntrustedIdentity;
boolean isRegistered();
class MessageReceived extends DBusSignal {
private long timestamp;
private String sender;
private byte[] groupId;
private String message;
private List<String> attachments;
private final long timestamp;
private final String sender;
private final byte[] groupId;
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 {
super(objectpath, timestamp, sender, groupId, message, attachments);
@ -79,8 +79,8 @@ public interface Signal extends DBusInterface {
class ReceiptReceived extends DBusSignal {
private long timestamp;
private String sender;
private final long timestamp;
private final String sender;
public ReceiptReceived(String objectpath, long timestamp, String sender) throws DBusException {
super(objectpath, timestamp, sender);
@ -96,4 +96,93 @@ public interface Signal extends DBusInterface {
return sender;
}
}
class SyncMessageReceived extends DBusSignal {
private final long timestamp;
private final String source;
private final String destination;
private final byte[] groupId;
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 {
super(objectpath, timestamp, source, destination, groupId, message, attachments);
this.timestamp = timestamp;
this.source = source;
this.destination = destination;
this.groupId = groupId;
this.message = message;
this.attachments = attachments;
}
public long getTimestamp() {
return timestamp;
}
public String getSource() {
return source;
}
public String getDestination() {
return destination;
}
public byte[] getGroupId() {
return groupId;
}
public String getMessage() {
return message;
}
public List<String> getAttachments() {
return attachments;
}
}
interface Error {
class AttachmentInvalid extends DBusExecutionException {
public AttachmentInvalid(final String message) {
super(message);
}
}
class Failure extends DBusExecutionException {
public Failure(final String message) {
super(message);
}
}
class GroupNotFound extends DBusExecutionException {
public GroupNotFound(final String message) {
super(message);
}
}
class InvalidNumber extends DBusExecutionException {
public InvalidNumber(final String message) {
super(message);
}
}
class UnregisteredUser extends DBusExecutionException {
public UnregisteredUser(final String message) {
super(message);
}
}
class UntrustedIdentity extends DBusExecutionException {
public UntrustedIdentity(final String message) {
super(message);
}
}
}
}

View file

@ -0,0 +1,12 @@
package org.asamk.signal;
public class BaseConfig {
public final static String PROJECT_NAME = BaseConfig.class.getPackage().getImplementationTitle();
public final static String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion();
final static String USER_AGENT = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + " " + PROJECT_VERSION;
private BaseConfig() {
}
}

View file

@ -1,7 +1,7 @@
package org.asamk.signal;
import org.asamk.signal.manager.Manager;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;

View file

@ -1,35 +0,0 @@
package org.asamk.signal;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import java.util.ArrayList;
import java.util.List;
class JsonDataMessage {
long timestamp;
String message;
int expiresInSeconds;
List<JsonAttachment> attachments;
JsonGroupInfo groupInfo;
JsonDataMessage(SignalServiceDataMessage dataMessage) {
this.timestamp = dataMessage.getTimestamp();
if (dataMessage.getGroupInfo().isPresent()) {
this.groupInfo = new JsonGroupInfo(dataMessage.getGroupInfo().get());
}
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));
}
} else {
this.attachments = new ArrayList<>();
}
}
}

View file

@ -2,13 +2,16 @@ package org.asamk.signal;
import org.asamk.Signal;
import org.asamk.signal.manager.Manager;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
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.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import java.util.ArrayList;
import java.util.List;
@ -28,7 +31,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, final String objectPath, Manager m) {
if (envelope.isReceipt()) {
try {
conn.sendSignal(new Signal.ReceiptReceived(
conn.sendMessage(new Signal.ReceiptReceived(
objectPath,
envelope.getTimestamp(),
!envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceE164().get() : content.getSender().getNumber().get()
@ -36,36 +39,81 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
} catch (DBusException e) {
e.printStackTrace();
}
} else if (content != null && content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
if (!message.isEndSession() &&
!(message.getGroupInfo().isPresent() &&
message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) {
List<String> attachments = new ArrayList<>();
if (message.getAttachments().isPresent()) {
for (SignalServiceAttachment attachment : message.getAttachments().get()) {
if (attachment.isPointer()) {
attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath());
} else if (content != null) {
if (content.getReceiptMessage().isPresent()) {
final SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get();
if (receiptMessage.isDeliveryReceipt()) {
final String sender = !envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceE164().get() : content.getSender().getNumber().get();
for (long timestamp : receiptMessage.getTimestamps()) {
try {
conn.sendMessage(new Signal.ReceiptReceived(
objectPath,
timestamp,
sender
));
} catch (DBusException e) {
e.printStackTrace();
}
}
}
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
try {
conn.sendSignal(new Signal.MessageReceived(
objectPath,
message.getTimestamp(),
envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(),
message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0],
message.getBody().isPresent() ? message.getBody().get() : "",
attachments));
} catch (DBusException e) {
e.printStackTrace();
if (!message.isEndSession() &&
!(message.getGroupContext().isPresent() &&
message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) {
try {
conn.sendMessage(new Signal.MessageReceived(
objectPath,
message.getTimestamp(),
envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(),
message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()
? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0],
message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m)));
} catch (DBusException e) {
e.printStackTrace();
}
}
} else if (content.getSyncMessage().isPresent()) {
SignalServiceSyncMessage sync_message = content.getSyncMessage().get();
if (sync_message.getSent().isPresent()) {
SentTranscriptMessage transcript = sync_message.getSent().get();
if (!envelope.isUnidentifiedSender() && envelope.hasSource() && (transcript.getDestination().isPresent() || transcript.getMessage().getGroupContext().isPresent())) {
SignalServiceDataMessage message = transcript.getMessage();
try {
conn.sendMessage(new Signal.SyncMessageReceived(
objectPath,
transcript.getTimestamp(),
envelope.getSourceAddress().getNumber().get(),
transcript.getDestination().isPresent() ? transcript.getDestination().get().getNumber().get() : "",
message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()
? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0],
message.getBody().isPresent() ? message.getBody().get() : "",
JsonDbusReceiveMessageHandler.getAttachments(message, m)));
} catch (DBusException e) {
e.printStackTrace();
}
}
}
}
}
}
static private List<String> getAttachments(SignalServiceDataMessage message, Manager m) {
List<String> attachments = new ArrayList<>();
if (message.getAttachments().isPresent()) {
for (SignalServiceAttachment attachment : message.getAttachments().get()) {
if (attachment.isPointer()) {
attachments.add(m.getAttachmentFile(attachment.asPointer().getRemoteId()).getAbsolutePath());
}
}
}
return attachments;
}
@Override
public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) {
super.handleMessage(envelope, content, exception);

View file

@ -1,10 +0,0 @@
package org.asamk.signal;
class JsonError {
String message;
JsonError(Throwable exception) {
this.message = exception.getMessage();
}
}

View file

@ -5,9 +5,10 @@ import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.asamk.signal.json.JsonError;
import org.asamk.signal.json.JsonMessageEnvelope;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
@ -23,7 +24,6 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler
this.m = m;
this.jsonProcessor = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect
jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}

View file

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

View file

@ -31,16 +31,22 @@ import org.asamk.signal.commands.Commands;
import org.asamk.signal.commands.DbusCommand;
import org.asamk.signal.commands.ExtendedDbusCommand;
import org.asamk.signal.commands.LocalCommand;
import org.asamk.signal.manager.BaseConfig;
import org.asamk.signal.commands.ProvisioningCommand;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.ServiceConfig;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import java.io.File;
import java.io.IOException;
import java.security.Security;
import java.util.Map;
@ -68,82 +74,124 @@ public class Main {
private static int handleCommands(Namespace ns) {
final String username = ns.getString("username");
Manager m;
Signal ts;
DBusConnection dBusConn = null;
try {
if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
try {
m = null;
int busType;
if (ns.getBoolean("dbus_system")) {
busType = DBusConnection.SYSTEM;
} else {
busType = DBusConnection.SESSION;
}
dBusConn = DBusConnection.getConnection(busType);
ts = dBusConn.getRemoteObject(
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);
} catch (UnsatisfiedLinkError e) {
System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
return 1;
} catch (DBusException e) {
e.printStackTrace();
if (dBusConn != null) {
dBusConn.disconnect();
}
return 3;
}
} else {
String dataPath = ns.getString("config");
if (isEmpty(dataPath)) {
dataPath = getDefaultDataPath();
}
m = new Manager(username, dataPath);
ts = m;
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;
}
} else {
String dataPath = ns.getString("config");
if (isEmpty(dataPath)) {
dataPath = getDefaultDataPath();
}
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT);
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) {
System.err.println("Error loading state file: " + e.getMessage());
return 2;
}
try (Manager m = manager) {
try {
m.init();
} catch (Exception 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) {
System.err.println("Error while checking account: " + e.getMessage());
return 2;
}
}
String commandKey = ns.getString("command");
final Map<String, Command> commands = Commands.getCommands();
if (commands.containsKey(commandKey)) {
Command command = commands.get(commandKey);
if (dBusConn != null) {
if (command instanceof ExtendedDbusCommand) {
return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, ts);
} else {
System.err.println(commandKey + " is not yet implemented via dbus");
return 1;
}
} else {
if (command instanceof LocalCommand) {
return ((LocalCommand) command).handleCommand(ns, m);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, ts);
} else {
System.err.println(commandKey + " is only works via dbus");
return 1;
}
}
}
return 0;
} finally {
if (dBusConn != null) {
dBusConn.disconnect();
return handleCommands(ns, m);
} catch (IOException e) {
e.printStackTrace();
return 3;
}
}
}
private static int handleCommands(Namespace ns, Signal ts, DBusConnection dBusConn) {
String commandKey = ns.getString("command");
final Map<String, Command> commands = Commands.getCommands();
if (commands.containsKey(commandKey)) {
Command command = commands.get(commandKey);
if (command instanceof ExtendedDbusCommand) {
return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, ts);
} else {
System.err.println(commandKey + " is not yet implemented via dbus");
return 1;
}
}
return 0;
}
private static int handleCommands(Namespace ns, ProvisioningManager pm) {
String commandKey = ns.getString("command");
final Map<String, Command> commands = Commands.getCommands();
if (commands.containsKey(commandKey)) {
Command command = commands.get(commandKey);
if (command instanceof ProvisioningCommand) {
return ((ProvisioningCommand) command).handleCommand(ns, pm);
} else {
System.err.println(commandKey + " only works with a username");
return 1;
}
}
return 0;
}
private static int handleCommands(Namespace ns, Manager m) {
String commandKey = ns.getString("command");
final Map<String, Command> commands = Commands.getCommands();
if (commands.containsKey(commandKey)) {
Command command = commands.get(commandKey);
if (command instanceof LocalCommand) {
return ((LocalCommand) command).handleCommand(ns, m);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m));
} else if (command instanceof ExtendedDbusCommand) {
System.err.println(commandKey + " only works via dbus");
}
return 1;
}
return 0;
}
/**
* Uses $XDG_DATA_HOME/signal-cli if it exists, or if none of the legacy directories exist:
* - $HOME/.config/signal

View file

@ -76,7 +76,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (content == null) {
System.out.println("Failed to decrypt message.");
} else {
System.out.println(String.format("Sender: %s (device: %d)", content.getSender().getNumber().get(), content.getSenderDevice()));
ContactInfo sourceContact = m.getContact(content.getSender().getNumber().get());
System.out.println(String.format("Sender: %s (device: %d)", (sourceContact == null ? "" : "" + sourceContact.name + "") + content.getSender().getNumber().get(), content.getSenderDevice()));
if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
handleSignalServiceDataMessage(message);
@ -102,7 +103,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Received sync read messages list");
for (ReadMessage rm : syncMessage.getRead().get()) {
ContactInfo fromContact = m.getContact(rm.getSender().getNumber().get());
System.out.println("From: " + (fromContact == null ? "" : "" + fromContact.name + "") + rm.getSender().getNumber() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp()));
System.out.println("From: " + (fromContact == null ? "" : "" + fromContact.name + "") + rm.getSender().getNumber().get() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp()));
}
}
if (syncMessage.getRequest().isPresent()) {
@ -113,6 +114,15 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (syncMessage.getRequest().get().isGroupsRequest()) {
System.out.println(" - groups request");
}
if (syncMessage.getRequest().get().isBlockedListRequest()) {
System.out.println(" - blocked list request");
}
if (syncMessage.getRequest().get().isConfigurationRequest()) {
System.out.println(" - configuration request");
}
if (syncMessage.getRequest().get().isKeysRequest()) {
System.out.println(" - keys request");
}
}
if (syncMessage.getSent().isPresent()) {
System.out.println("Received sync sent message");
@ -122,6 +132,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
String dest = sentTranscriptMessage.getDestination().get().getNumber().get();
ContactInfo destContact = m.getContact(dest);
to = (destContact == null ? "" : "" + destContact.name + "") + dest;
} else if (sentTranscriptMessage.getRecipients().size() > 0) {
StringBuilder toBuilder = new StringBuilder();
for (SignalServiceAddress dest : sentTranscriptMessage.getRecipients()) {
ContactInfo destContact = m.getContact(dest.getNumber().get());
toBuilder.append(destContact == null ? "" : "" + destContact.name + "").append(dest.getNumber().get()).append(" ");
}
to = toBuilder.toString();
} else {
to = "Unknown";
}
@ -137,14 +154,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Blocked numbers:");
final BlockedListMessage blockedList = syncMessage.getBlockedList().get();
for (SignalServiceAddress address : blockedList.getAddresses()) {
System.out.println(" - " + address.getNumber());
System.out.println(" - " + address.getNumber().get());
}
}
if (syncMessage.getVerified().isPresent()) {
System.out.println("Received sync message with verified identities:");
final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified());
String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey()));
String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey()));
System.out.println(" " + safetyNumber);
}
if (syncMessage.getConfiguration().isPresent()) {
@ -161,7 +178,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (syncMessage.getViewOnceOpen().isPresent()) {
final ViewOnceOpenMessage viewOnceOpenMessage = syncMessage.getViewOnceOpen().get();
System.out.println("Received sync message with view once open message:");
System.out.println(" - Sender:" + viewOnceOpenMessage.getSender().getNumber());
System.out.println(" - Sender:" + viewOnceOpenMessage.getSender().getNumber().get());
System.out.println(" - Timestamp:" + viewOnceOpenMessage.getTimestamp());
}
if (syncMessage.getStickerPackOperations().isPresent()) {
@ -183,7 +200,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
SignalServiceCallMessage callMessage = content.getCallMessage().get();
if (callMessage.getAnswerMessage().isPresent()) {
AnswerMessage answerMessage = callMessage.getAnswerMessage().get();
System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getDescription());
System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getSdp());
}
if (callMessage.getBusyMessage().isPresent()) {
BusyMessage busyMessage = callMessage.getBusyMessage().get();
@ -201,7 +218,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
if (callMessage.getOfferMessage().isPresent()) {
OfferMessage offerMessage = callMessage.getOfferMessage().get();
System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getDescription());
System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getSdp());
}
}
if (content.getReceiptMessage().isPresent()) {
@ -246,8 +263,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (message.getBody().isPresent()) {
System.out.println("Body: " + message.getBody().get());
}
if (message.getGroupInfo().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupInfo().get();
if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
@ -311,10 +328,19 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Profile key update, key length:" + message.getProfileKey().get().length);
}
if (message.getReaction().isPresent()) {
final SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
System.out.println("Reaction:");
System.out.println(" - Emoji: " + reaction.getEmoji());
System.out.println(" - Target author: " + reaction.getTargetAuthor().getNumber().get());
System.out.println(" - Target timestamp: " + reaction.getTargetSentTimestamp());
System.out.println(" - Is remove: " + reaction.isRemove());
}
if (message.getQuote().isPresent()) {
SignalServiceDataMessage.Quote quote = message.getQuote().get();
System.out.println("Quote: (" + quote.getId() + ")");
System.out.println(" Author: " + quote.getAuthor().getNumber());
System.out.println(" Author: " + quote.getAuthor().getNumber().get());
System.out.println(" Text: " + quote.getText());
if (quote.getAttachments().size() > 0) {
System.out.println(" Attachments: ");
@ -341,12 +367,12 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
if (attachment.isPointer()) {
final SignalServiceAttachmentPointer pointer = attachment.asPointer();
System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length);
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(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no"));
System.out.println(" Dimensions: " + pointer.getWidth() + "x" + pointer.getHeight());
File file = m.getAttachmentFile(pointer.getId());
File file = m.getAttachmentFile(pointer.getRemoteId());
if (file.exists()) {
System.out.println(" Stored plaintext in: " + file);
}

View file

@ -2,9 +2,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.manager.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;

View file

@ -22,6 +22,7 @@ public class Commands {
addCommand("removeDevice", new RemoveDeviceCommand());
addCommand("removePin", new RemovePinCommand());
addCommand("send", new SendCommand());
addCommand("sendReaction", new SendReactionCommand());
addCommand("sendContacts", new SendContactsCommand());
addCommand("updateContact", new UpdateContactCommand());
addCommand("setPin", new SetPinCommand());
@ -32,6 +33,7 @@ public class Commands {
addCommand("updateGroup", new UpdateGroupCommand());
addCommand("updateProfile", new UpdateProfileCommand());
addCommand("verify", new VerifyCommand());
addCommand("uploadStickerPack", new UploadStickerPackCommand());
}
public static Map<String, Command> getCommands() {

View file

@ -6,8 +6,9 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.DbusReceiveMessageHandler;
import org.asamk.signal.JsonDbusReceiveMessageHandler;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import java.io.IOException;
@ -41,15 +42,15 @@ public class DaemonCommand implements LocalCommand {
DBusConnection conn = null;
try {
try {
int busType;
DBusConnection.DBusBusType busType;
String busName;
if (ns.getBoolean("system")) {
busType = DBusConnection.SYSTEM;
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.SESSION;
busType = DBusConnection.DBusBusType.SESSION;
}
conn = DBusConnection.getConnection(busType);
conn.exportObject(SIGNAL_OBJECTPATH, m);
conn.exportObject(SIGNAL_OBJECTPATH, new DbusSignalImpl(m));
busName = ns.getString("busname");
if (busName == null) {
conn.requestBusName(SIGNAL_BUSNAME);

View file

@ -3,7 +3,7 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.Signal;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.connections.impl.DBusConnection;
public interface ExtendedDbusCommand extends Command {

View file

@ -3,8 +3,8 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.UserAlreadyExists;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.UserAlreadyExists;
import org.whispersystems.libsignal.InvalidKeyException;
import java.io.IOException;
@ -12,7 +12,7 @@ import java.util.concurrent.TimeoutException;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
public class LinkCommand implements LocalCommand {
public class LinkCommand implements ProvisioningCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
@ -21,15 +21,15 @@ public class LinkCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
public int handleCommand(final Namespace ns, final ProvisioningManager m) {
String deviceName = ns.getString("name");
if (deviceName == null) {
deviceName = "cli";
}
try {
System.out.println(m.getDeviceLinkUri());
m.finishDeviceLink(deviceName);
System.out.println("Associated with: " + m.getUsername());
String username = m.finishDeviceLink(deviceName);
System.out.println("Associated with: " + username);
} catch (TimeoutException e) {
System.err.println("Link request timed out, please try again.");
return 3;

View file

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

View file

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

View file

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

View file

@ -0,0 +1,10 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.signal.manager.ProvisioningManager;
public interface ProvisioningCommand extends Command {
int handleCommand(Namespace ns, ProvisioningManager m);
}

View file

@ -3,10 +3,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.NotAGroupMemberException;
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;

View file

@ -1,5 +1,11 @@
package org.asamk.signal.commands;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
@ -7,10 +13,10 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.JsonReceiveMessageHandler;
import org.asamk.signal.ReceiveMessageHandler;
import org.asamk.signal.json.JsonMessageEnvelope;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.DateUtils;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.DBusSigHandler;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.util.Base64;
@ -35,49 +41,102 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
}
public int handleCommand(final Namespace ns, final Signal signal, DBusConnection dbusconnection) {
if (dbusconnection != null) {
try {
dbusconnection.addSigHandler(Signal.MessageReceived.class, new DBusSigHandler<Signal.MessageReceived>() {
@Override
public void handle(Signal.MessageReceived s) {
System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n",
s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()), s.getMessage()));
if (s.getGroupId().length > 0) {
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(s.getGroupId()));
}
if (s.getAttachments().size() > 0) {
System.out.println("Attachments: ");
for (String attachment : s.getAttachments()) {
System.out.println("- Stored plaintext in: " + attachment);
}
}
final ObjectMapper jsonProcessor;
if (ns.getBoolean("json")) {
jsonProcessor = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
} else {
jsonProcessor = null;
}
try {
dbusconnection.addSigHandler(Signal.MessageReceived.class, messageReceived -> {
if (jsonProcessor != null) {
JsonMessageEnvelope envelope = new JsonMessageEnvelope(messageReceived);
ObjectNode result = jsonProcessor.createObjectNode();
result.putPOJO("envelope", envelope);
try {
jsonProcessor.writeValue(System.out, result);
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
});
dbusconnection.addSigHandler(Signal.ReceiptReceived.class, new DBusSigHandler<Signal.ReceiptReceived>() {
@Override
public void handle(Signal.ReceiptReceived s) {
System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n",
s.getSender(), DateUtils.formatTimestamp(s.getTimestamp())));
} else {
System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n",
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()));
}
});
} catch (UnsatisfiedLinkError e) {
System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
return 1;
} catch (DBusException e) {
e.printStackTrace();
return 1;
}
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
return 0;
if (messageReceived.getAttachments().size() > 0) {
System.out.println("Attachments: ");
for (String attachment : messageReceived.getAttachments()) {
System.out.println("- Stored plaintext in: " + attachment);
}
}
System.out.println();
}
});
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) {
JsonMessageEnvelope envelope = new JsonMessageEnvelope(syncReceived);
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("Sync Envelope from: %s to: %s\nTimestamp: %s\nBody: %s\n",
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()));
}
if (syncReceived.getAttachments().size() > 0) {
System.out.println("Attachments: ");
for (String attachment : syncReceived.getAttachments()) {
System.out.println("- Stored plaintext in: " + attachment);
}
}
System.out.println();
}
});
} catch (UnsatisfiedLinkError e) {
System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
return 1;
} catch (DBusException e) {
e.printStackTrace();
return 1;
}
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
return 0;
}
}
return 0;
}
@Override

View file

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

View file

@ -21,7 +21,7 @@ public class RemovePinCommand implements LocalCommand {
return 1;
}
try {
m.setRegistrationLockPin(Optional.<String>absent());
m.setRegistrationLockPin(Optional.absent());
return 0;
} catch (IOException e) {
System.err.println("Remove pin error: " + e.getMessage());

View file

@ -5,14 +5,10 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.AttachmentInvalidException;
import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.NotAGroupMemberException;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import java.io.IOException;
import java.nio.charset.Charset;
@ -20,12 +16,7 @@ import java.util.ArrayList;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.util.ErrorUtils.handleDBusExecutionException;
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;
public class SendCommand implements DbusCommand {
@ -61,19 +52,13 @@ public class SendCommand implements DbusCommand {
if (ns.getBoolean("endsession")) {
try {
signal.sendEndSessionMessage(ns.<String>getList("recipient"));
signal.sendEndSessionMessage(ns.getList("recipient"));
return 0;
} catch (IOException e) {
handleIOException(e);
return 3;
} catch (EncapsulatedExceptions e) {
handleEncapsulatedExceptions(e);
return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
} catch (DBusExecutionException e) {
handleDBusExecutionException(e);
System.err.println("Failed to send message: " + e.getMessage());
return 1;
}
}
@ -89,42 +74,42 @@ public class SendCommand implements DbusCommand {
}
}
List<String> attachments = ns.getList("attachment");
if (attachments == null) {
attachments = new ArrayList<>();
}
try {
List<String> attachments = ns.getList("attachment");
if (attachments == null) {
attachments = new ArrayList<>();
}
if (ns.getString("group") != null) {
byte[] groupId = Util.decodeGroupId(ns.getString("group"));
signal.sendGroupMessage(messageText, attachments, groupId);
} else {
signal.sendMessage(messageText, attachments, ns.<String>getList("recipient"));
byte[] groupId;
try {
groupId = Util.decodeGroupId(ns.getString("group"));
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;
}
long timestamp = signal.sendGroupMessage(messageText, attachments, groupId);
System.out.println(timestamp);
return 0;
}
return 0;
} catch (IOException e) {
handleIOException(e);
return 3;
} catch (EncapsulatedExceptions e) {
handleEncapsulatedExceptions(e);
return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
} catch (GroupNotFoundException e) {
handleGroupNotFoundException(e);
} catch (DBusExecutionException e) {
System.err.println("Failed to send message: " + e.getMessage());
return 1;
} catch (NotAGroupMemberException e) {
handleNotAGroupMemberException(e);
return 1;
} catch (AttachmentInvalidException e) {
System.err.println("Failed to add attachment: " + e.getMessage());
System.err.println("Aborting sending.");
}
try {
long timestamp = signal.sendMessage(messageText, attachments, ns.getList("recipient"));
System.out.println(timestamp);
return 0;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
} catch (DBusExecutionException e) {
handleDBusExecutionException(e);
return 1;
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
System.err.println("Failed to send message: " + e.getMessage());
return 1;
}
}

View file

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

View file

@ -0,0 +1,99 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
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.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
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;
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("-e", "--emoji")
.required(true)
.help("Specify the emoji, should be a single unicode grapheme cluster.");
subparser.addArgument("-a", "--target-author")
.required(true)
.help("Specify the number of the author of the message to which to react.");
subparser.addArgument("-t", "--target-timestamp")
.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());
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && ns.getString("group") == null) {
System.err.println("No recipients given");
System.err.println("Aborting sending.");
return 1;
}
String emoji = ns.getString("emoji");
boolean isRemove = ns.getBoolean("remove");
String targetAuthor = ns.getString("target_author");
long targetTimestamp = ns.getLong("target_timestamp");
try {
if (ns.getString("group") != null) {
byte[] groupId = Util.decodeGroupId(ns.getString("group"));
m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
} else {
m.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, ns.getList("recipient"));
}
return 0;
} catch (IOException e) {
handleIOException(e);
return 3;
} catch (EncapsulatedExceptions e) {
handleEncapsulatedExceptions(e);
return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
} catch (GroupNotFoundException e) {
handleGroupNotFoundException(e);
return 1;
} catch (NotAGroupMemberException e) {
handleNotAGroupMemberException(e);
return 1;
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;
} catch (InvalidNumberException e) {
handleInvalidNumberException(e);
return 1;
}
}
}

View file

@ -6,7 +6,9 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.ErrorUtils;
import org.asamk.signal.util.Hex;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.Locale;
@ -21,8 +23,8 @@ public class TrustCommand implements LocalCommand {
mutTrust.addArgument("-a", "--trust-all-known-keys")
.help("Trust all known keys of this user, only use this for testing.")
.action(Arguments.storeTrue());
mutTrust.addArgument("-v", "--verified-fingerprint")
.help("Specify the fingerprint of the key, only use this option if you have verified the fingerprint.");
mutTrust.addArgument("-v", "--verified-safety-number", "--verified-fingerprint")
.help("Specify the safety number of the key, only use this option if you have verified the safety number.");
}
@Override
@ -39,34 +41,46 @@ public class TrustCommand implements LocalCommand {
return 1;
}
} else {
String fingerprint = ns.getString("verified_fingerprint");
if (fingerprint != null) {
fingerprint = fingerprint.replaceAll(" ", "");
if (fingerprint.length() == 66) {
String safetyNumber = ns.getString("verified_safety_number");
if (safetyNumber != null) {
safetyNumber = safetyNumber.replaceAll(" ", "");
if (safetyNumber.length() == 66) {
byte[] fingerprintBytes;
try {
fingerprintBytes = Hex.toByteArray(fingerprint.toLowerCase(Locale.ROOT));
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.");
return 1;
}
boolean res = m.trustIdentityVerified(number, fingerprintBytes);
boolean res;
try {
res = m.trustIdentityVerified(number, fingerprintBytes);
} catch (InvalidNumberException e) {
ErrorUtils.handleInvalidNumberException(e);
return 1;
}
if (!res) {
System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct.");
return 1;
}
} else if (fingerprint.length() == 60) {
boolean res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint);
} else if (safetyNumber.length() == 60) {
boolean res;
try {
res = m.trustIdentityVerifiedSafetyNumber(number, safetyNumber);
} catch (InvalidNumberException e) {
ErrorUtils.handleInvalidNumberException(e);
return 1;
}
if (!res) {
System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct.");
return 1;
}
} else {
System.err.println("Fingerprint 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 you have verified with -v FINGERPRINT");
System.err.println("You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER");
return 1;
}
}

View file

@ -2,9 +2,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.manager.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;

View file

@ -6,6 +6,8 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
public class UpdateContactCommand implements LocalCommand {
@Override
@ -15,6 +17,10 @@ public class UpdateContactCommand implements LocalCommand {
subparser.addArgument("-n", "--name")
.required(true)
.help("New contact name");
subparser.addArgument("-e", "--expiration")
.required(false)
.type(int.class)
.help("Set expiration time of messages (seconds)");
subparser.help("Update the details of a given contact");
}
@ -30,8 +36,17 @@ public class UpdateContactCommand implements LocalCommand {
try {
m.setContactName(number, name);
Integer expiration = ns.getInt("expiration");
if (expiration != null) {
m.setExpirationTimer(number, expiration);
}
} catch (InvalidNumberException e) {
System.out.println("Invalid contact number: " + e.getMessage());
System.err.println("Invalid contact number: " + e.getMessage());
return 1;
} catch (IOException e) {
System.err.println("Update contact error: " + e.getMessage());
return 3;
}
return 0;

View file

@ -4,23 +4,16 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.AttachmentInvalidException;
import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.NotAGroupMemberException;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
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;
public class UpdateGroupCommand implements DbusCommand {
@ -44,49 +37,48 @@ public class UpdateGroupCommand implements DbusCommand {
return 1;
}
try {
byte[] groupId = null;
if (ns.getString("group") != null) {
byte[] groupId = null;
if (ns.getString("group") != null) {
try {
groupId = Util.decodeGroupId(ns.getString("group"));
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;
}
if (groupId == null) {
groupId = new byte[0];
}
String groupName = ns.getString("name");
if (groupName == null) {
groupName = "";
}
List<String> groupMembers = ns.getList("member");
if (groupMembers == null) {
groupMembers = new ArrayList<>();
}
String groupAvatar = ns.getString("avatar");
if (groupAvatar == null) {
groupAvatar = "";
}
}
if (groupId == null) {
groupId = new byte[0];
}
String groupName = ns.getString("name");
if (groupName == null) {
groupName = "";
}
List<String> groupMembers = ns.getList("member");
if (groupMembers == null) {
groupMembers = new ArrayList<>();
}
String groupAvatar = ns.getString("avatar");
if (groupAvatar == null) {
groupAvatar = "";
}
try {
byte[] newGroupId = signal.updateGroup(groupId, groupName, groupMembers, groupAvatar);
if (groupId.length != newGroupId.length) {
System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\"");
}
return 0;
} catch (IOException e) {
handleIOException(e);
return 3;
} catch (AttachmentInvalidException e) {
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
} catch (Signal.Error.AttachmentInvalid e) {
System.err.println("Failed to add avatar attachment for group\": " + e.getMessage());
System.err.println("Aborting sending.");
return 1;
} catch (GroupNotFoundException e) {
handleGroupNotFoundException(e);
return 1;
} catch (NotAGroupMemberException e) {
handleNotAGroupMemberException(e);
return 1;
} catch (EncapsulatedExceptions e) {
handleEncapsulatedExceptions(e);
return 3;
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
} catch (DBusExecutionException e) {
System.err.println("Failed to send message: " + e.getMessage());
return 1;
}
}

View file

@ -14,16 +14,18 @@ public class UpdateProfileCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup();
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.help("Set a name and/or avatar image for the user profile");
subparser.help("Set a name and avatar image for the user profile");
}
@Override
@ -34,38 +36,15 @@ public class UpdateProfileCommand implements LocalCommand {
}
String name = ns.getString("name");
if (name != null) {
try {
m.setProfileName(name);
} catch (IOException e) {
System.err.println("UpdateAccount error: " + e.getMessage());
return 3;
}
}
String avatarPath = ns.getString("avatar");
if (avatarPath != null) {
File avatarFile = new File(avatarPath);
try {
m.setProfileAvatar(avatarFile);
} catch (IOException e) {
System.err.println("UpdateAccount error: " + e.getMessage());
return 3;
}
}
boolean removeAvatar = ns.getBoolean("remove_avatar");
if (removeAvatar) {
try {
m.removeProfileAvatar();
} catch (IOException e) {
System.err.println("UpdateAccount error: " + e.getMessage());
return 3;
}
try {
File avatarFile = removeAvatar ? null : new File(avatarPath);
m.setProfile(name, avatarFile);
} catch (IOException e) {
System.err.println("UpdateAccount error: " + e.getMessage());
return 3;
}
return 0;

View file

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

View file

@ -20,10 +20,6 @@ public class VerifyCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.userHasKeys()) {
System.err.println("User has no keys, first call register.");
return 1;
}
if (m.isRegistered()) {
System.err.println("User registration is already verified");
return 1;

View file

@ -0,0 +1,205 @@
package org.asamk.signal.dbus;
import org.asamk.Signal;
import org.asamk.signal.manager.AttachmentInvalidException;
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.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.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DbusSignalImpl implements Signal {
private final Manager m;
public DbusSignalImpl(final Manager m) {
this.m = m;
}
@Override
public boolean isRemote() {
return false;
}
@Override
public String getObjectPath() {
return null;
}
@Override
public long sendMessage(final String message, final List<String> attachments, final String recipient) {
List<String> recipients = new ArrayList<>(1);
recipients.add(recipient);
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());
}
}
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');
}
return 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);
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
} catch (AttachmentInvalidException e) {
throw new Error.AttachmentInvalid(e.getMessage());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
}
@Override
public void sendEndSessionMessage(final List<String> recipients) {
try {
m.sendEndSessionMessage(recipients);
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (EncapsulatedExceptions e) {
throw convertEncapsulatedExceptions(e);
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
}
}
@Override
public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
try {
return m.sendGroupMessage(message, attachments, groupId);
} 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) {
throw new Error.AttachmentInvalid(e.getMessage());
}
}
@Override
public String getContactName(final String number) {
try {
return m.getContactName(number);
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
}
}
@Override
public void setContactName(final String number, final String name) {
try {
m.setContactName(number, name);
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
}
}
@Override
public void setContactBlocked(final String number, final boolean blocked) {
try {
m.setContactBlocked(number, blocked);
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
}
}
@Override
public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
try {
m.setGroupBlocked(groupId, blocked);
} catch (GroupNotFoundException e) {
throw new Error.GroupNotFound(e.getMessage());
}
}
@Override
public List<byte[]> getGroupIds() {
List<GroupInfo> groups = m.getGroups();
List<byte[]> ids = new ArrayList<>(groups.size());
for (GroupInfo group : groups) {
ids.add(group.groupId);
}
return ids;
}
@Override
public String getGroupName(final byte[] groupId) {
GroupInfo group = m.getGroup(groupId);
if (group == null) {
return "";
} else {
return group.name;
}
}
@Override
public List<String> getGroupMembers(final byte[] groupId) {
GroupInfo group = m.getGroup(groupId);
if (group == null) {
return Collections.emptyList();
} else {
return new ArrayList<>(group.getMembersE164());
}
}
@Override
public byte[] updateGroup(final byte[] groupId, final String name, final List<String> members, final String avatar) {
try {
return m.updateGroup(groupId, name, members, avatar);
} 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) {
throw new Error.InvalidNumber(e.getMessage());
} catch (AttachmentInvalidException e) {
throw new Error.AttachmentInvalid(e.getMessage());
}
}
@Override
public boolean isRegistered() {
return true;
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
@ -15,7 +15,7 @@ class JsonAttachment {
final SignalServiceAttachmentPointer pointer = attachment.asPointer();
if (attachment.isPointer()) {
this.id = String.valueOf(pointer.getId());
this.id = String.valueOf(pointer.getRemoteId());
if (pointer.getFileName().isPresent()) {
this.filename = pointer.getFileName().get();
}
@ -24,4 +24,8 @@ class JsonAttachment {
}
}
}
JsonAttachment(String filename) {
this.filename = filename;
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;

View file

@ -0,0 +1,59 @@
package org.asamk.signal.json;
import org.asamk.Signal;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
class JsonDataMessage {
long timestamp;
String message;
int expiresInSeconds;
List<JsonAttachment> attachments;
JsonGroupInfo groupInfo;
JsonDataMessage(SignalServiceDataMessage dataMessage) {
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.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));
}
} else {
this.attachments = new ArrayList<>();
}
}
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());
}
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());
}
}

View file

@ -0,0 +1,10 @@
package org.asamk.signal.json;
public class JsonError {
String message;
public JsonError(Throwable exception) {
this.message = exception.getMessage();
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -27,4 +27,8 @@ class JsonGroupInfo {
}
this.type = groupInfo.getType().toString();
}
JsonGroupInfo(byte[] groupId) {
this.groupId = Base64.encodeBytes(groupId);
}
}

View file

@ -1,10 +1,11 @@
package org.asamk.signal;
package org.asamk.signal.json;
import org.asamk.Signal;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
class JsonMessageEnvelope {
public class JsonMessageEnvelope {
String source;
int sourceDevice;
@ -44,4 +45,22 @@ class JsonMessageEnvelope {
}
}
}
public JsonMessageEnvelope(Signal.MessageReceived messageReceived) {
source = messageReceived.getSender();
timestamp = messageReceived.getTimestamp();
dataMessage = new JsonDataMessage(messageReceived);
}
public JsonMessageEnvelope(Signal.ReceiptReceived receiptReceived) {
source = receiptReceived.getSender();
timestamp = receiptReceived.getTimestamp();
isReceipt = true;
}
public JsonMessageEnvelope(Signal.SyncMessageReceived messageReceived) {
source = messageReceived.getSource();
timestamp = messageReceived.getTimestamp();
syncMessage = new JsonSyncMessage(messageReceived);
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.json;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;

View file

@ -1,5 +1,6 @@
package org.asamk.signal;
package org.asamk.signal.json;
import org.asamk.Signal;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
class JsonSyncDataMessage extends JsonDataMessage {
@ -12,4 +13,9 @@ class JsonSyncDataMessage extends JsonDataMessage {
this.destination = transcriptMessage.getDestination().get().getNumber().get();
}
}
JsonSyncDataMessage(Signal.SyncMessageReceived messageReceived) {
super(messageReceived);
destination = messageReceived.getDestination();
}
}

View file

@ -0,0 +1,50 @@
package org.asamk.signal.json;
import org.asamk.Signal;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList;
import java.util.List;
enum JsonSyncMessageType {
CONTACTS_SYNC,
GROUPS_SYNC,
REQUEST_SYNC
}
class JsonSyncMessage {
JsonSyncDataMessage sentMessage;
List<String> blockedNumbers;
List<ReadMessage> readMessages;
JsonSyncMessageType type;
JsonSyncMessage(SignalServiceSyncMessage syncMessage) {
if (syncMessage.getSent().isPresent()) {
this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get());
}
if (syncMessage.getBlockedList().isPresent()) {
this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size());
for (SignalServiceAddress address : syncMessage.getBlockedList().get().getAddresses()) {
this.blockedNumbers.add(address.getNumber().get());
}
}
if (syncMessage.getRead().isPresent()) {
this.readMessages = syncMessage.getRead().get();
}
if (syncMessage.getContacts().isPresent()) {
this.type = JsonSyncMessageType.CONTACTS_SYNC;
} else if (syncMessage.getGroups().isPresent()) {
this.type = JsonSyncMessageType.GROUPS_SYNC;
} else if (syncMessage.getRequest().isPresent()) {
this.type = JsonSyncMessageType.REQUEST_SYNC;
}
}
JsonSyncMessage(Signal.SyncMessageReceived messageReceived) {
sentMessage = new JsonSyncDataMessage(messageReceived);
}
}

View file

@ -1,8 +1,6 @@
package org.asamk.signal;
package org.asamk.signal.manager;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
public class AttachmentInvalidException extends DBusExecutionException {
public class AttachmentInvalidException extends Exception {
public AttachmentInvalidException(String message) {
super(message);

View file

@ -1,32 +0,0 @@
package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
public class BaseConfig {
public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION;
final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF";
final static int PREKEY_MINIMUM_COUNT = 20;
final static int PREKEY_BATCH_SIZE = 100;
final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
private final static String URL = "https://textsecure-service.whispersystems.org";
private final static String CDN_URL = "https://cdn.signal.org";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration(
new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
new SignalContactDiscoveryUrl[0]
);
private BaseConfig() {
}
}

View file

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

View file

@ -0,0 +1,156 @@
package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Arrays;
import java.util.Objects;
interface HandleAction {
void execute(Manager m) throws Throwable;
}
class SendReceiptAction implements HandleAction {
private final SignalServiceAddress address;
private final long timestamp;
public SendReceiptAction(final SignalServiceAddress address, final long timestamp) {
this.address = address;
this.timestamp = timestamp;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendReceipt(address, timestamp);
}
@Override
public boolean equals(final Object o) {
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);
}
@Override
public int hashCode() {
return Objects.hash(address, timestamp);
}
}
class SendSyncContactsAction implements HandleAction {
private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction();
private SendSyncContactsAction() {
}
public static SendSyncContactsAction create() {
return INSTANCE;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendContacts();
}
}
class SendSyncGroupsAction implements HandleAction {
private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction();
private SendSyncGroupsAction() {
}
public static SendSyncGroupsAction create() {
return INSTANCE;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendGroups();
}
}
class SendSyncBlockedListAction implements HandleAction {
private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction();
private SendSyncBlockedListAction() {
}
public static SendSyncBlockedListAction create() {
return INSTANCE;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendBlockedList();
}
}
class SendGroupInfoRequestAction implements HandleAction {
private final SignalServiceAddress address;
private final byte[] groupId;
public SendGroupInfoRequestAction(final SignalServiceAddress address, final byte[] groupId) {
this.address = address;
this.groupId = groupId;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendGroupInfoRequest(groupId, address);
}
@Override
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);
}
@Override
public int hashCode() {
int result = Objects.hash(address);
result = 31 * result + Arrays.hashCode(groupId);
return result;
}
}
class SendGroupUpdateAction implements HandleAction {
private final SignalServiceAddress address;
private final byte[] groupId;
public SendGroupUpdateAction(final SignalServiceAddress address, final byte[] groupId) {
this.address = address;
this.groupId = groupId;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendUpdateGroupMessage(groupId, address);
}
@Override
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);
}
@Override
public int hashCode() {
int result = Objects.hash(address);
result = 31 * result + Arrays.hashCode(groupId);
return result;
}
}

View file

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

View file

@ -1,6 +1,8 @@
package org.asamk.signal.manager;
import org.asamk.signal.util.RandomUtils;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.util.Base64;
class KeyUtils {
@ -12,8 +14,12 @@ class KeyUtils {
return getSecret(52);
}
static byte[] createProfileKey() {
return getSecretBytes(32);
static ProfileKey createProfileKey() {
try {
return new ProfileKey(getSecretBytes(32));
} catch (InvalidInputException e) {
throw new AssertionError("Profile key is guaranteed to be 32 bytes here");
}
}
static String createPassword() {
@ -24,6 +30,14 @@ class KeyUtils {
return getSecretBytes(16);
}
static byte[] createUnrestrictedUnidentifiedAccess() {
return getSecretBytes(16);
}
static byte[] createStickerUploadKey() {
return getSecretBytes(32);
}
private static String getSecret(int size) {
byte[] secret = getSecretBytes(size);
return Base64.encodeBytes(secret);

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -0,0 +1,117 @@
/*
Copyright (C) 2015-2020 AsamK and contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.asamk.signal.manager;
import org.asamk.signal.storage.SignalAccount;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ProvisioningManager {
private final PathConfig pathConfig;
private final SignalServiceConfiguration serviceConfiguration;
private final String userAgent;
private final SignalServiceAccountManager accountManager;
private final IdentityKeyPair identityKey;
private final int registrationId;
private final String password;
public ProvisioningManager(String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) {
this.pathConfig = PathConfig.createDefault(settingsPath);
this.serviceConfiguration = serviceConfiguration;
this.userAgent = userAgent;
identityKey = KeyHelper.generateIdentityKeyPair();
registrationId = KeyHelper.generateRegistrationId(false);
password = KeyUtils.createPassword();
final SleepTimer timer = new UptimeSleepTimer();
GroupsV2Operations groupsV2Operations;
try {
groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration));
} catch (Throwable ignored) {
groupsV2Operations = null;
}
accountManager = new SignalServiceAccountManager(serviceConfiguration,
new DynamicCredentialsProvider(null, null, password, null, SignalServiceAddress.DEFAULT_DEVICE_ID),
userAgent,
groupsV2Operations,
timer);
}
public String getDeviceLinkUri() throws TimeoutException, IOException {
String deviceUuid = accountManager.getNewDeviceUuid();
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);
String username = ret.getNumber();
// TODO do this check before actually registering
if (SignalAccount.userExists(pathConfig.getDataPath(), username)) {
throw new UserAlreadyExists(username, SignalAccount.getFileName(pathConfig.getDataPath(), username));
}
// Create new account with the synced identity
byte[] profileKeyBytes = ret.getProfileKey();
ProfileKey profileKey;
if (profileKeyBytes == null) {
profileKey = KeyUtils.createProfileKey();
} else {
try {
profileKey = new ProfileKey(profileKeyBytes);
} catch (InvalidInputException e) {
throw new IOException("Received invalid profileKey", e);
}
}
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)) {
m.refreshPreKeys();
m.requestSyncGroups();
m.requestSyncContacts();
m.requestSyncBlocked();
m.requestSyncConfiguration();
m.saveAccount();
}
}
return username;
}
}

View file

@ -0,0 +1,80 @@
package org.asamk.signal.manager;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.Dns;
import okhttp3.Interceptor;
public class ServiceConfig {
final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF";
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 long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
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_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 Optional<Dns> dns = Optional.absent();
private final static String zkGroupServerPublicParamsHex = "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=";
static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false, false);
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;
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],
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
interceptors,
dns,
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);
}
private ServiceConfig() {
}
}

View file

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

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.manager;
public class UserAlreadyExists extends Exception {

View file

@ -1,6 +1,5 @@
package org.asamk.signal.manager;
import org.asamk.signal.AttachmentInvalidException;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
@ -13,9 +12,9 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import org.whispersystems.util.Base64;
import java.io.BufferedInputStream;
@ -35,12 +34,10 @@ import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
@ -61,7 +58,7 @@ class Utils {
return signalServiceAttachments;
}
private static String getFileMimeType(File file) throws IOException {
static String getFileMimeType(File file, String defaultMimeType) throws IOException {
String mime = Files.probeContentType(file.toPath());
if (mime == null) {
try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) {
@ -69,7 +66,7 @@ class Utils {
}
}
if (mime == null) {
mime = "application/octet-stream";
return defaultMimeType;
}
return mime;
}
@ -77,12 +74,14 @@ class Utils {
static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
InputStream attachmentStream = new FileInputStream(attachmentFile);
final long attachmentSize = attachmentFile.length();
final String mime = getFileMimeType(attachmentFile);
// TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option
final String mime = getFileMimeType(attachmentFile, "application/octet-stream");
// TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option
final long uploadTimestamp = System.currentTimeMillis();
Optional<byte[]> preview = Optional.absent();
Optional<String> caption = Optional.absent();
Optional<String> blurHash = Optional.absent();
return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, blurHash, null);
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);
}
static StreamDetails createStreamDetailsFromFile(File file) throws IOException {
@ -97,7 +96,7 @@ class Utils {
static CertificateValidator getCertificateValidator() {
try {
ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.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);
@ -149,38 +148,19 @@ class Utils {
return new DeviceLinkInfo(deviceIdentifier, deviceKey);
}
static Set<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients, String localNumber) {
Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
for (String recipient : recipients) {
try {
recipientsTS.add(getPushAddress(recipient, localNumber));
} catch (InvalidNumberException e) {
System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
System.err.println("Aborting sending.");
return null;
}
}
return recipientsTS;
}
static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException {
return PhoneNumberFormatter.formatNumber(number, localNumber);
}
private static SignalServiceAddress getPushAddress(String number, String localNumber) throws InvalidNumberException {
String e164number = canonicalizeNumber(number, localNumber);
return new SignalServiceAddress(null, e164number);
}
static SignalServiceEnvelope loadEnvelope(File file) throws IOException {
try (FileInputStream f = new FileInputStream(file)) {
DataInputStream in = new DataInputStream(f);
int version = in.readInt();
if (version > 2) {
if (version > 4) {
return null;
}
int type = in.readInt();
String source = in.readUTF();
UUID sourceUuid = null;
if (version >= 3) {
sourceUuid = UuidUtil.parseOrNull(in.readUTF());
}
int sourceDevice = in.readInt();
if (version == 1) {
// read legacy relay field
@ -199,25 +179,33 @@ class Utils {
legacyMessage = new byte[legacyMessageLen];
in.readFully(legacyMessage);
}
long serverTimestamp = 0;
long serverReceivedTimestamp = 0;
String uuid = null;
if (version == 2) {
serverTimestamp = in.readLong();
if (version >= 2) {
serverReceivedTimestamp = in.readLong();
uuid = in.readUTF();
if ("".equals(uuid)) {
uuid = null;
}
}
return new SignalServiceEnvelope(type, Optional.of(new SignalServiceAddress(null, source)), sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid);
long serverDeliveredTimestamp = 0;
if (version >= 4) {
serverDeliveredTimestamp = in.readLong();
}
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);
}
}
static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
try (FileOutputStream f = new FileOutputStream(file)) {
try (DataOutputStream out = new DataOutputStream(f)) {
out.writeInt(2); // version
out.writeInt(4); // version
out.writeInt(envelope.getType());
out.writeUTF(envelope.getSourceE164().get());
out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : "");
out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : "");
out.writeInt(envelope.getSourceDevice());
out.writeLong(envelope.getTimestamp());
if (envelope.hasContent()) {
@ -232,9 +220,10 @@ class Utils {
} else {
out.writeInt(0);
}
out.writeLong(envelope.getServerTimestamp());
out.writeLong(envelope.getServerReceivedTimestamp());
String uuid = envelope.getUuid();
out.writeUTF(uuid == null ? "" : uuid);
out.writeLong(envelope.getServerDeliveredTimestamp());
}
}
}
@ -256,10 +245,28 @@ class Utils {
return outputFile;
}
static String computeSafetyNumber(String ownUsername, IdentityKey ownIdentityKey, String theirUsername, IdentityKey theirIdentityKey) {
// Version 1: E164 user
// Version 2: UUID user
Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(1, ownUsername.getBytes(), ownIdentityKey, theirUsername.getBytes(), theirIdentityKey);
static String computeSafetyNumber(SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
int version;
byte[] ownId;
byte[] theirId;
if (ServiceConfig.capabilities.isUuid()
&& ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) {
// Version 2: UUID user
version = 2;
ownId = UuidUtil.toByteArray(ownAddress.getUuid().get());
theirId = UuidUtil.toByteArray(theirAddress.getUuid().get());
} else {
// Version 1: E164 user
version = 1;
if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) {
return "INVALID ID";
}
ownId = ownAddress.getNumber().get().getBytes();
theirId = theirAddress.getNumber().get().getBytes();
}
Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, ownId, ownIdentityKey, theirId, theirIdentityKey);
return fingerprint.getDisplayableFingerprint().getDisplayText();
}

View file

@ -10,39 +10,57 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.contacts.JsonContactsStore;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.storage.groups.JsonGroupStore;
import org.asamk.signal.storage.profiles.ProfileStore;
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
import org.asamk.signal.storage.protocol.JsonSignalProtocolStore;
import org.asamk.signal.storage.threads.JsonThreadStore;
import org.asamk.signal.storage.protocol.RecipientStore;
import org.asamk.signal.storage.protocol.SessionInfo;
import org.asamk.signal.storage.protocol.SignalServiceAddressResolver;
import org.asamk.signal.storage.threads.LegacyJsonThreadStore;
import org.asamk.signal.storage.threads.ThreadInfo;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Collection;
import java.util.UUID;
import java.util.stream.Collectors;
public class SignalAccount {
public class SignalAccount implements Closeable {
private final ObjectMapper jsonProcessor = new ObjectMapper();
private FileChannel fileChannel;
private FileLock lock;
private final FileChannel fileChannel;
private final FileLock lock;
private String username;
private UUID uuid;
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
private boolean isMultiDevice = false;
private String password;
private String registrationLockPin;
private String signalingKey;
private byte[] profileKey;
private ProfileKey profileKey;
private int preKeyIdOffset;
private int nextSignedPreKeyId;
@ -51,72 +69,82 @@ public class SignalAccount {
private JsonSignalProtocolStore signalProtocolStore;
private JsonGroupStore groupStore;
private JsonContactsStore contactStore;
private JsonThreadStore threadStore;
private RecipientStore recipientStore;
private ProfileStore profileStore;
private SignalAccount() {
private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
this.fileChannel = fileChannel;
this.lock = lock;
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}
public static SignalAccount load(String dataPath, String username) throws IOException {
SignalAccount account = new SignalAccount();
IOUtils.createPrivateDirectories(dataPath);
account.openFileChannel(getFileName(dataPath, username));
account.load();
return account;
final String fileName = getFileName(dataPath, username);
final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
try {
SignalAccount account = new SignalAccount(pair.first(), pair.second());
account.load();
return account;
} catch (Throwable e) {
pair.second().close();
pair.first().close();
throw e;
}
}
public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, byte[] profileKey) throws IOException {
public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
String fileName = getFileName(dataPath, username);
if (!new File(fileName).exists()) {
IOUtils.createPrivateFile(fileName);
}
SignalAccount account = new SignalAccount();
account.openFileChannel(getFileName(dataPath, username));
final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
SignalAccount account = new SignalAccount(pair.first(), pair.second());
account.username = username;
account.profileKey = profileKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.groupStore = new JsonGroupStore();
account.threadStore = new JsonThreadStore();
account.contactStore = new JsonContactsStore();
account.recipientStore = new RecipientStore();
account.profileStore = new ProfileStore();
account.registered = false;
return account;
}
public static SignalAccount createLinkedAccount(String dataPath, String username, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, byte[] profileKey) throws IOException {
public static SignalAccount createLinkedAccount(String dataPath, String username, UUID uuid, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
String fileName = getFileName(dataPath, username);
if (!new File(fileName).exists()) {
IOUtils.createPrivateFile(fileName);
}
SignalAccount account = new SignalAccount();
account.openFileChannel(getFileName(dataPath, username));
final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
SignalAccount account = new SignalAccount(pair.first(), pair.second());
account.username = username;
account.uuid = uuid;
account.password = password;
account.profileKey = profileKey;
account.deviceId = deviceId;
account.signalingKey = signalingKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.groupStore = new JsonGroupStore();
account.threadStore = new JsonThreadStore();
account.contactStore = new JsonContactsStore();
account.recipientStore = new RecipientStore();
account.profileStore = new ProfileStore();
account.registered = true;
account.isMultiDevice = true;
return account;
}
public static SignalAccount createTemporaryAccount(IdentityKeyPair identityKey, int registrationId) {
SignalAccount account = new SignalAccount();
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.registered = false;
return account;
}
public static String getFileName(String dataPath, String username) {
return dataPath + "/" + username;
}
@ -136,6 +164,14 @@ public class SignalAccount {
rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
}
JsonNode uuidNode = rootNode.get("uuid");
if (uuidNode != null && !uuidNode.isNull()) {
try {
uuid = UUID.fromString(uuidNode.asText());
} catch (IllegalArgumentException e) {
throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e);
}
}
JsonNode node = rootNode.get("deviceId");
if (node != null) {
deviceId = node.asInt();
@ -161,7 +197,11 @@ public class SignalAccount {
nextSignedPreKeyId = 0;
}
if (rootNode.has("profileKey")) {
profileKey = Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText());
try {
profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()));
} catch (InvalidInputException e) {
throw new IOException("Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", e);
}
}
signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
@ -181,12 +221,66 @@ public class SignalAccount {
if (contactStore == null) {
contactStore = new JsonContactsStore();
}
JsonNode recipientStoreNode = rootNode.get("recipientStore");
if (recipientStoreNode != null) {
recipientStore = jsonProcessor.convertValue(recipientStoreNode, RecipientStore.class);
}
if (recipientStore == null) {
recipientStore = new RecipientStore();
recipientStore.resolveServiceAddress(getSelfAddress());
for (ContactInfo contact : contactStore.getContacts()) {
recipientStore.resolveServiceAddress(contact.getAddress());
}
for (GroupInfo group : groupStore.getGroups()) {
group.members = group.members.stream()
.map(m -> recipientStore.resolveServiceAddress(m))
.collect(Collectors.toSet());
}
for (SessionInfo session : signalProtocolStore.getSessions()) {
session.address = recipientStore.resolveServiceAddress(session.address);
}
for (JsonIdentityKeyStore.Identity identity : signalProtocolStore.getIdentities()) {
identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
}
}
JsonNode profileStoreNode = rootNode.get("profileStore");
if (profileStoreNode != null) {
profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
}
if (profileStore == null) {
profileStore = new ProfileStore();
}
JsonNode threadStoreNode = rootNode.get("threadStore");
if (threadStoreNode != null) {
threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class);
}
if (threadStore == null) {
threadStore = new JsonThreadStore();
LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
// Migrate thread info to group and contact store
for (ThreadInfo thread : threadStore.getThreads()) {
if (thread.id == null || thread.id.isEmpty()) {
continue;
}
try {
ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
if (contactInfo != null) {
contactInfo.messageExpirationTime = thread.messageExpirationTime;
contactStore.updateContact(contactInfo);
} else {
GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id));
if (groupInfo != null) {
groupInfo.messageExpirationTime = thread.messageExpirationTime;
groupStore.updateGroup(groupInfo);
}
}
} catch (Exception ignored) {
}
}
}
}
@ -196,6 +290,7 @@ public class SignalAccount {
}
ObjectNode rootNode = jsonProcessor.createObjectNode();
rootNode.put("username", username)
.put("uuid", uuid == null ? null : uuid.toString())
.put("deviceId", deviceId)
.put("isMultiDevice", isMultiDevice)
.put("password", password)
@ -203,40 +298,44 @@ public class SignalAccount {
.put("signalingKey", signalingKey)
.put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey", Base64.encodeBytes(profileKey))
.put("profileKey", Base64.encodeBytes(profileKey.serialize()))
.put("registered", registered)
.putPOJO("axolotlStore", signalProtocolStore)
.putPOJO("groupStore", groupStore)
.putPOJO("contactStore", contactStore)
.putPOJO("threadStore", threadStore)
.putPOJO("recipientStore", recipientStore)
.putPOJO("profileStore", profileStore)
;
try {
synchronized (fileChannel) {
fileChannel.position(0);
jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode);
fileChannel.truncate(fileChannel.position());
fileChannel.force(false);
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
// Write to memory first to prevent corrupting the file in case of serialization errors
jsonProcessor.writeValue(output, rootNode);
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
synchronized (fileChannel) {
fileChannel.position(0);
input.transferTo(Channels.newOutputStream(fileChannel));
fileChannel.truncate(fileChannel.position());
fileChannel.force(false);
}
}
} catch (Exception e) {
System.err.println(String.format("Error saving file: %s", e.getMessage()));
}
}
private void openFileChannel(String fileName) throws IOException {
if (fileChannel != null) {
return;
}
if (!new File(fileName).exists()) {
IOUtils.createPrivateFile(fileName);
}
fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
lock = fileChannel.tryLock();
private static Pair<FileChannel, FileLock> openFileChannel(String fileName) throws IOException {
FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
FileLock lock = fileChannel.tryLock();
if (lock == null) {
System.err.println("Config file is in use by another instance, waiting…");
lock = fileChannel.lock();
System.err.println("Config file lock acquired.");
}
return new Pair<>(fileChannel, lock);
}
public void setResolver(final SignalServiceAddressResolver resolver) {
signalProtocolStore.setResolver(resolver);
}
public void addPreKeys(Collection<PreKeyRecord> records) {
@ -263,16 +362,28 @@ public class SignalAccount {
return contactStore;
}
public JsonThreadStore getThreadStore() {
return threadStore;
public RecipientStore getRecipientStore() {
return recipientStore;
}
public ProfileStore getProfileStore() {
return profileStore;
}
public String getUsername() {
return username;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(final UUID uuid) {
this.uuid = uuid;
}
public SignalServiceAddress getSelfAddress() {
return new SignalServiceAddress(null, username);
return new SignalServiceAddress(uuid, username);
}
public int getDeviceId() {
@ -291,6 +402,10 @@ public class SignalAccount {
return registrationLockPin;
}
public String getRegistrationLock() {
return null; // TODO implement KBS
}
public void setRegistrationLockPin(final String registrationLockPin) {
this.registrationLockPin = registrationLockPin;
}
@ -303,11 +418,11 @@ public class SignalAccount {
this.signalingKey = signalingKey;
}
public byte[] getProfileKey() {
public ProfileKey getProfileKey() {
return profileKey;
}
public void setProfileKey(final byte[] profileKey) {
public void setProfileKey(final ProfileKey profileKey) {
this.profileKey = profileKey;
}
@ -334,4 +449,15 @@ public class SignalAccount {
public void setMultiDevice(final boolean multiDevice) {
isMultiDevice = multiDevice;
}
@Override
public void close() throws IOException {
synchronized (fileChannel) {
try {
lock.close();
} catch (ClosedChannelException ignored) {
}
fileChannel.close();
}
}
}

View file

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

View file

@ -1,41 +1,46 @@
package org.asamk.signal.storage.contacts;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonContactsStore {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty("contacts")
@JsonSerialize(using = JsonContactsStore.MapToListSerializer.class)
@JsonDeserialize(using = ContactsDeserializer.class)
private Map<String, ContactInfo> contacts = new HashMap<>();
private List<ContactInfo> contacts = new ArrayList<>();
public void updateContact(ContactInfo contact) {
contacts.put(contact.number, contact);
final SignalServiceAddress contactAddress = contact.getAddress();
for (int i = 0; i < contacts.size(); i++) {
if (contacts.get(i).getAddress().matches(contactAddress)) {
contacts.set(i, contact);
return;
}
}
contacts.add(contact);
}
public ContactInfo getContact(String number) {
return contacts.get(number);
public ContactInfo getContact(SignalServiceAddress address) {
for (ContactInfo contact : contacts) {
if (contact.getAddress().matches(address)) {
if (contact.uuid == null) {
contact.uuid = address.getUuid().orNull();
} else if (contact.number == null) {
contact.number = address.getNumber().orNull();
}
return contact;
}
}
return null;
}
public List<ContactInfo> getContacts() {
return new ArrayList<>(contacts.values());
return new ArrayList<>(contacts);
}
/**
@ -44,27 +49,4 @@ public class JsonContactsStore {
public void clear() {
contacts.clear();
}
private static class MapToListSerializer extends JsonSerializer<Map<?, ?>> {
@Override
public void serialize(final Map<?, ?> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
jgen.writeObject(value.values());
}
}
private static class ContactsDeserializer extends JsonDeserializer<Map<String, ContactInfo>> {
@Override
public Map<String, ContactInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
Map<String, ContactInfo> contacts = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
ContactInfo c = jsonProcessor.treeToValue(n, ContactInfo.class);
contacts.put(c.number, c);
}
return contacts;
}
}
}

View file

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

View file

@ -0,0 +1,111 @@
package org.asamk.signal.storage.profiles;
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.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
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;
public class ProfileStore {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty("profiles")
@JsonDeserialize(using = ProfileStoreDeserializer.class)
@JsonSerialize(using = ProfileStoreSerializer.class)
private final List<SignalProfileEntry> profiles = new ArrayList<>();
public SignalProfileEntry getProfile(SignalServiceAddress serviceAddress) {
for (SignalProfileEntry entry : profiles) {
if (entry.getServiceAddress().matches(serviceAddress)) {
return entry;
}
}
return null;
}
public void updateProfile(SignalServiceAddress serviceAddress, ProfileKey profileKey, long now, SignalProfile profile) {
SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile);
for (int i = 0; i < profiles.size(); i++) {
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
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 {
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;
final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, name);
ProfileKey profileKey = null;
try {
profileKey = new ProfileKey(Base64.decode(entry.get("profileKey").asText()));
} catch (InvalidInputException ignored) {
}
long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();
SignalProfile profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class);
addresses.add(new SignalProfileEntry(serviceAddress, profileKey, lastUpdateTimestamp, profile));
}
}
return addresses;
}
}
public static class ProfileStoreSerializer extends JsonSerializer<List<SignalProfileEntry>> {
@Override
public void serialize(List<SignalProfileEntry> profiles, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
json.writeStartArray();
for (SignalProfileEntry profileEntry : profiles) {
final SignalServiceAddress address = profileEntry.getServiceAddress();
json.writeStartObject();
if (address.getNumber().isPresent()) {
json.writeStringField("name", address.getNumber().get());
}
if (address.getUuid().isPresent()) {
json.writeStringField("uuid", address.getUuid().get().toString());
}
json.writeStringField("profileKey", Base64.encodeBytes(profileEntry.getProfileKey().serialize()));
json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp());
json.writeObjectField("profile", profileEntry.getProfile());
json.writeEndObject();
}
json.writeEndArray();
}
}
}

View file

@ -0,0 +1,81 @@
package org.asamk.signal.storage.profiles;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.io.File;
public class SignalProfile {
@JsonProperty
private final String identityKey;
@JsonProperty
private final String name;
private final File avatarFile;
@JsonProperty
private final String unidentifiedAccess;
@JsonProperty
private final boolean unrestrictedUnidentifiedAccess;
@JsonProperty
private 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;
}
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) {
this.identityKey = identityKey;
this.name = name;
this.avatarFile = null;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = capabilities;
}
public String getIdentityKey() {
return identityKey;
}
public String getName() {
return name;
}
public File getAvatarFile() {
return avatarFile;
}
public String getUnidentifiedAccess() {
return unidentifiedAccess;
}
public boolean isUnrestrictedUnidentifiedAccess() {
return unrestrictedUnidentifiedAccess;
}
public SignalServiceProfile.Capabilities getCapabilities() {
return capabilities;
}
@Override
public String toString() {
return "SignalProfile{" +
"identityKey='" + identityKey + '\'' +
", name='" + name + '\'' +
", avatarFile=" + avatarFile +
", unidentifiedAccess='" + unidentifiedAccess + '\'' +
", unrestrictedUnidentifiedAccess=" + unrestrictedUnidentifiedAccess +
", capabilities=" + capabilities +
'}';
}
}

View file

@ -0,0 +1,38 @@
package org.asamk.signal.storage.profiles;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SignalProfileEntry {
private final SignalServiceAddress serviceAddress;
private final ProfileKey profileKey;
private final long lastUpdateTimestamp;
private final SignalProfile profile;
public SignalProfileEntry(final SignalServiceAddress serviceAddress, final ProfileKey profileKey, final long lastUpdateTimestamp, final SignalProfile profile) {
this.serviceAddress = serviceAddress;
this.profileKey = profileKey;
this.lastUpdateTimestamp = lastUpdateTimestamp;
this.profile = profile;
}
public SignalServiceAddress getServiceAddress() {
return serviceAddress;
}
public ProfileKey getProfileKey() {
return profileKey;
}
public long getLastUpdateTimestamp() {
return lastUpdateTimestamp;
}
public SignalProfile getProfile() {
return profile;
}
}

View file

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

View file

@ -70,7 +70,7 @@ class JsonPreKeyStore implements PreKeyStore {
try {
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
} catch (IOException e) {
System.out.println(String.format("Error while decoding prekey for: %s", preKeyId));
System.err.println(String.format("Error while decoding prekey for: %s", preKeyId));
}
}
}

View file

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

View file

@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.TrustLevel;
import org.asamk.signal.manager.TrustLevel;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyIdException;
@ -13,9 +13,9 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
import java.util.Map;
public class JsonSignalProtocolStore implements SignalProtocolStore {
@ -26,7 +26,7 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
@JsonProperty("sessionStore")
@JsonDeserialize(using = JsonSessionStore.JsonSessionStoreDeserializer.class)
@JsonSerialize(using = JsonSessionStore.JsonPreKeyStoreSerializer.class)
@JsonSerialize(using = JsonSessionStore.JsonSessionStoreSerializer.class)
private JsonSessionStore sessionStore;
@JsonProperty("signedPreKeyStore")
@ -56,6 +56,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId);
}
public void setResolver(final SignalServiceAddressResolver resolver) {
sessionStore.setResolver(resolver);
identityKeyStore.setResolver(resolver);
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyStore.getIdentityKeyPair();
@ -71,16 +76,20 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
return identityKeyStore.saveIdentity(address, identityKey);
}
public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) {
identityKeyStore.saveIdentity(name, identityKey, trustLevel, null);
public void saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
identityKeyStore.saveIdentity(serviceAddress, identityKey, trustLevel, null);
}
public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel);
}
public List<JsonIdentityKeyStore.Identity> getIdentities() {
return identityKeyStore.getIdentities();
}
public List<JsonIdentityKeyStore.Identity> getIdentities(String name) {
return identityKeyStore.getIdentities(name);
public List<JsonIdentityKeyStore.Identity> getIdentities(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentities(serviceAddress);
}
@Override
@ -93,6 +102,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
return identityKeyStore.getIdentity(address);
}
public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentity(serviceAddress);
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
return preKeyStore.loadPreKey(preKeyId);
@ -118,6 +131,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
return sessionStore.loadSession(address);
}
public List<SessionInfo> getSessions() {
return sessionStore.getSessions();
}
@Override
public List<Integer> getSubDeviceSessions(String name) {
return sessionStore.getSubDeviceSessions(name);
@ -143,6 +160,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
sessionStore.deleteAllSessions(name);
}
public void deleteAllSessions(SignalServiceAddress serviceAddress) {
sessionStore.deleteAllSessions(serviceAddress);
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
return signedPreKeyStore.loadSignedPreKey(signedPreKeyId);

View file

@ -87,7 +87,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore {
try {
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
} catch (IOException e) {
System.out.println(String.format("Error while decoding prekey for: %s", preKeyId));
System.err.println(String.format("Error while decoding prekey for: %s", preKeyId));
}
}
}

View file

@ -0,0 +1,84 @@
package org.asamk.signal.storage.protocol;
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.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class RecipientStore {
@JsonProperty("recipientStore")
@JsonDeserialize(using = RecipientStoreDeserializer.class)
@JsonSerialize(using = RecipientStoreSerializer.class)
private final Set<SignalServiceAddress> addresses = new HashSet<>();
public SignalServiceAddress resolveServiceAddress(SignalServiceAddress serviceAddress) {
if (addresses.contains(serviceAddress)) {
// If the Set already contains the exact address with UUID and Number,
// we can just return it here.
return serviceAddress;
}
for (SignalServiceAddress address : addresses) {
if (address.matches(serviceAddress)) {
return address;
}
}
if (serviceAddress.getNumber().isPresent() && serviceAddress.getUuid().isPresent()) {
addresses.add(serviceAddress);
}
return serviceAddress;
}
public static class RecipientStoreDeserializer extends JsonDeserializer<Set<SignalServiceAddress>> {
@Override
public Set<SignalServiceAddress> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Set<SignalServiceAddress> addresses = new HashSet<>();
if (node.isArray()) {
for (JsonNode recipient : node) {
String recipientName = recipient.get("name").asText();
UUID uuid = UuidUtil.parseOrThrow(recipient.get("uuid").asText());
final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, recipientName);
addresses.add(serviceAddress);
}
}
return addresses;
}
}
public static class RecipientStoreSerializer extends JsonSerializer<Set<SignalServiceAddress>> {
@Override
public void serialize(Set<SignalServiceAddress> addresses, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
json.writeStartArray();
for (SignalServiceAddress address : addresses) {
json.writeStartObject();
json.writeStringField("name", address.getNumber().get());
json.writeStringField("uuid", address.getUuid().get().toString());
json.writeEndObject();
}
json.writeEndArray();
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,13 +1,12 @@
package org.asamk.signal.util;
import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.NotAGroupMemberException;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
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.util.InvalidNumberException;
import java.io.IOException;
@ -50,13 +49,13 @@ public class ErrorUtils {
System.err.println("Aborting sending.");
}
public static void handleDBusExecutionException(DBusExecutionException e) {
System.err.println("Cannot connect to dbus: " + e.getMessage());
System.err.println("Aborting.");
}
public static void handleGroupIdFormatException(GroupIdFormatException e) {
System.err.println(e.getMessage());
System.err.println("Aborting sending.");
}
public static void handleInvalidNumberException(InvalidNumberException e) {
System.err.println("Failed to parse recipient: " + e.getMessage());
System.err.println("Aborting sending.");
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal;
package org.asamk.signal.util;
import java.io.IOException;

View file

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

View file

@ -1,8 +1,13 @@
package org.asamk.signal.util;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.file.Files;
@ -35,6 +40,12 @@ public class IOUtils {
return output.toString();
}
public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.copy(in, baos);
return baos.toByteArray();
}
public static void createPrivateDirectories(String directoryPath) throws IOException {
final File file = new File(directoryPath);
if (file.exists()) {
@ -68,4 +79,19 @@ public class IOUtils {
return System.getProperty("user.home") + "/.local/share";
}
public static void copyStreamToFile(InputStream input, File outputFile) throws IOException {
copyStreamToFile(input, outputFile, 8192);
}
public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException {
try (OutputStream output = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[bufferSize];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
}
}
}

View file

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

View file

@ -11,15 +11,15 @@ public class SecurityProvider extends Provider {
private static final String info = "Security Provider v1.0";
public SecurityProvider() {
super(PROVIDER_NAME, 1.0, info);
super(PROVIDER_NAME, "1.0", info);
put("SecureRandom.DEFAULT", DefaultRandom.class.getName());
// Workaround for BKS truststore
put("KeyStore.BKS", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Std");
put("KeyStore.BKS-V1", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Version1");
put("KeyStore.BouncyCastle", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$BouncyCastleStore");
put("KeyFactory.X.509", "org.bouncycastle.jcajce.provider.asymmetric.x509.KeyFactory");
put("CertificateFactory.X.509", "org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory");
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("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());
}
public static class DefaultRandom extends SecureRandomSpi {

View file

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