Merge remote-tracking branch 'asamk/master'

This commit is contained in:
Julius Bünger 2021-01-12 14:02:08 +01:00
commit 14b6d9aed1
108 changed files with 2882 additions and 1573 deletions

View file

@ -1,9 +1,26 @@
# Changelog
## [Unreleased]
### Fixed
- Disable registration lock before removing the PIN
- Fix PIN hash version to match the official clients.
If you had previously set a PIN you need to set it again to be able to unlock the registration lock later.
## [0.7.2] - 2020-12-31
### Added
- Implement new registration lock PIN with `setPin` and `removePin` (with KBS)
- Include quotes, mentions and reactions in json output (Thanks @Atomic-Bean)
### Fixed
- Retrieve avatars for v2 groups
- Download attachment thumbnail for quoted attachments
## [0.7.1] - 2020-12-21
### Added
- Accept group invitation with `updateGroup -g GROUP_ID`
- Decline group invitation with `quitGroup -g GROUP_ID`
- Join group via invitation link `joinGroup --uri https://signal.group/#...`
### Fixed
- Include group ids for v2 groups in json output

View file

@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_11
mainClassName = 'org.asamk.signal.Main'
version = '0.7.0'
version = '0.7.2'
compileJava.options.encoding = 'UTF-8'
@ -18,10 +18,10 @@ repositories {
dependencies {
implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_15'
implementation 'org.bouncycastle:bcprov-jdk15on:1.67'
implementation 'org.bouncycastle:bcprov-jdk15on:1.68'
implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1'
implementation 'com.github.hypfvieh:dbus-java:3.2.4'
implementation 'org.slf4j:slf4j-nop:1.7.30'
implementation 'org.slf4j:slf4j-simple:1.7.30'
}
jar {

View file

@ -124,6 +124,15 @@ Only works, if this is the master device.
Specify the device you want to remove.
Use listDevices to see the deviceIds.
=== getUserStatus
Uses a list of phone numbers to determine the statuses of those users. Shows if they are registered on the Signal Servers or not.
[NUMBER [NUMBER ...]]::
One or more numbers to check.
*--json*::
Output the statuses as an array of json objects.
=== send
Send a message to another user or group.
@ -178,6 +187,13 @@ Dont download attachments of received messages.
*--json*::
Output received messages in json format, one object per line.
=== joinGroup
Join a group via an invitation link.
*--uri*::
The invitation link URI (starts with `https://signal.group/#`)
=== updateGroup
Create or update a group.

View file

@ -1,8 +1,8 @@
package org.asamk.signal;
import org.asamk.Signal;
import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.groups.GroupUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
@ -65,7 +65,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
byte[] groupId = getGroupId(m, message);
byte[] groupId = getGroupId(message);
if (!message.isEndSession() && (
groupId == null
|| message.getGroupContext().get().getGroupV1Type() == null
@ -91,7 +91,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
.getGroupContext()
.isPresent()) {
SignalServiceDataMessage message = transcript.getMessage();
byte[] groupId = getGroupId(m, message);
byte[] groupId = getGroupId(message);
try {
conn.sendMessage(new Signal.SyncMessageReceived(objectPath,
@ -112,20 +112,9 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
}
}
private static byte[] getGroupId(final Manager m, final SignalServiceDataMessage message) {
byte[] groupId;
if (message.getGroupContext().isPresent()) {
if (message.getGroupContext().get().getGroupV1().isPresent()) {
groupId = message.getGroupContext().get().getGroupV1().get().getGroupId();
} else if (message.getGroupContext().get().getGroupV2().isPresent()) {
groupId = GroupUtils.getGroupId(message.getGroupContext().get().getGroupV2().get().getMasterKey());
} else {
groupId = null;
}
} else {
groupId = null;
}
return groupId;
private static byte[] getGroupId(final SignalServiceDataMessage message) {
return message.getGroupContext().isPresent() ? GroupUtils.getGroupId(message.getGroupContext().get())
.serialize() : null;
}
static private List<String> getAttachments(SignalServiceDataMessage message, Manager m) {

View file

@ -3,7 +3,6 @@ package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
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.node.ObjectNode;
@ -23,8 +22,7 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler
public JsonReceiveMessageHandler(Manager m) {
this.m = m;
this.jsonProcessor = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}
@ -35,7 +33,7 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler
result.putPOJO("error", new JsonError(exception));
}
if (envelope != null) {
result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content));
result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content, m));
}
try {
jsonProcessor.writeValue(System.out, result);

View file

@ -1,5 +1,5 @@
/*
Copyright (C) 2015-2020 AsamK and contributors
Copyright (C) 2015-2021 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
@ -32,16 +32,20 @@ import org.asamk.signal.commands.DbusCommand;
import org.asamk.signal.commands.ExtendedDbusCommand;
import org.asamk.signal.commands.LocalCommand;
import org.asamk.signal.commands.ProvisioningCommand;
import org.asamk.signal.commands.RegistrationCommand;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotRegisteredException;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.RegistrationManager;
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.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
@ -50,10 +54,10 @@ import java.io.IOException;
import java.security.Security;
import java.util.Map;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public class Main {
final static Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
installSecurityProviderWorkaround();
@ -62,7 +66,7 @@ public class Main {
System.exit(1);
}
int res = handleCommands(ns);
int res = init(ns);
System.exit(res);
}
@ -72,125 +76,148 @@ public class Main {
Security.addProvider(new BouncyCastleProvider());
}
private static int handleCommands(Namespace ns) {
final String username = ns.getString("username");
public static int init(Namespace ns) {
Command command = getCommand(ns);
if (command == null) {
logger.error("Command not implemented!");
return 3;
}
if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
try {
DBusConnection.DBusBusType busType;
if (ns.getBoolean("dbus_system")) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
}
try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME,
DbusConfig.SIGNAL_OBJECTPATH,
Signal.class);
return initDbusClient(command, ns, ns.getBoolean("dbus_system"));
}
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();
final String username = ns.getString("username");
final File dataPath;
String config = ns.getString("config");
if (config != null) {
dataPath = new File(config);
} else {
dataPath = getDefaultDataPath();
}
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(
BaseConfig.USER_AGENT);
if (!ServiceConfig.getCapabilities().isGv2()) {
logger.warn("WARNING: Support for new group V2 is disabled,"
+ " because the required native library dependency is missing: libzkgroup");
}
if (username == null) {
ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
return handleCommand(command, ns, pm);
}
if (command instanceof RegistrationCommand) {
final RegistrationManager manager;
try {
manager = RegistrationManager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
} catch (Throwable e) {
logger.error("Error loading or creating state file: {}", e.getMessage());
return 2;
}
try (RegistrationManager m = manager) {
return handleCommand(command, ns, m);
} catch (Exception e) {
logger.error("Cleanup failed", e);
return 3;
}
} else {
String dataPath = ns.getString("config");
if (isEmpty(dataPath)) {
dataPath = getDefaultDataPath();
}
}
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(
BaseConfig.USER_AGENT);
Manager manager;
try {
manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
} catch (NotRegisteredException e) {
System.err.println("User is not registered.");
return 1;
} catch (Throwable e) {
logger.error("Error loading state file: {}", e.getMessage());
return 2;
}
if (username == null) {
ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
return handleCommands(ns, pm);
}
Manager manager;
try (Manager m = manager) {
try {
manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
} catch (Throwable e) {
System.err.println("Error loading state file: " + e.getMessage());
m.checkAccountState();
} catch (IOException e) {
logger.error("Error while checking account: {}", e.getMessage());
return 2;
}
try (Manager m = manager) {
try {
m.checkAccountState();
} catch (AuthorizationFailedException e) {
if (!"register".equals(ns.getString("command"))) {
// Register command should still be possible, if current authorization fails
System.err.println("Authorization failed, was the number registered elsewhere?");
return 2;
}
} catch (IOException e) {
System.err.println("Error while checking account: " + e.getMessage());
return 2;
}
return handleCommands(ns, m);
} catch (IOException e) {
e.printStackTrace();
return 3;
}
return handleCommand(command, ns, m);
} catch (IOException e) {
logger.error("Cleanup failed", e);
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);
private static int initDbusClient(final Command command, final Namespace ns, final boolean systemBus) {
try {
DBusConnection.DBusBusType busType;
if (systemBus) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
System.err.println(commandKey + " is not yet implemented via dbus");
return 1;
busType = DBusConnection.DBusBusType.SESSION;
}
try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME,
DbusConfig.SIGNAL_OBJECTPATH,
Signal.class);
return handleCommand(command, ns, ts, dBusConn);
}
} catch (DBusException | IOException e) {
logger.error("Dbus client failed", e);
return 3;
}
return 0;
}
private static int handleCommands(Namespace ns, ProvisioningManager pm) {
private static Command getCommand(Namespace ns) {
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;
}
if (!commands.containsKey(commandKey)) {
return null;
}
return 0;
return commands.get(commandKey);
}
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");
}
private static int handleCommand(Command command, Namespace ns, Signal ts, DBusConnection dBusConn) {
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("Command is not yet implemented via dbus");
return 1;
}
}
private static int handleCommand(Command command, Namespace ns, ProvisioningManager pm) {
if (command instanceof ProvisioningCommand) {
return ((ProvisioningCommand) command).handleCommand(ns, pm);
} else {
System.err.println("Command only works with a username");
return 1;
}
}
private static int handleCommand(Command command, Namespace ns, RegistrationManager m) {
if (command instanceof RegistrationCommand) {
return ((RegistrationCommand) command).handleCommand(ns, m);
}
return 1;
}
private static int handleCommand(Command command, Namespace ns, Manager m) {
if (command instanceof LocalCommand) {
return ((LocalCommand) command).handleCommand(ns, m);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m));
} else {
System.err.println("Command only works via dbus");
return 1;
}
return 0;
}
/**
@ -200,19 +227,21 @@ public class Main {
*
* @return the data directory to be used by signal-cli.
*/
private static String getDefaultDataPath() {
String dataPath = IOUtils.getDataHomeDir() + "/signal-cli";
if (new File(dataPath).exists()) {
private static File getDefaultDataPath() {
File dataPath = new File(IOUtils.getDataHomeDir(), "signal-cli");
if (dataPath.exists()) {
return dataPath;
}
String legacySettingsPath = System.getProperty("user.home") + "/.config/signal";
if (new File(legacySettingsPath).exists()) {
File configPath = new File(System.getProperty("user.home"), ".config");
File legacySettingsPath = new File(configPath, "signal");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure";
if (new File(legacySettingsPath).exists()) {
legacySettingsPath = new File(configPath, "textsecure");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
@ -220,34 +249,7 @@ public class Main {
}
private static Namespace parseArgs(String[] args) {
ArgumentParser parser = ArgumentParsers.newFor("signal-cli")
.build()
.defaultHelp(true)
.description("Commandline interface for Signal.")
.version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
parser.addArgument("--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
parser.addArgument("-n", "--busname")
.help("Name of the DBus.");
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification.");
mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue());
mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue());
Subparsers subparsers = parser.addSubparsers()
.title("subcommands")
.dest("command")
.description("valid subcommands")
.help("additional help");
final Map<String, Command> commands = Commands.getCommands();
for (Map.Entry<String, Command> entry : commands.entrySet()) {
Subparser subparser = subparsers.addParser(entry.getKey());
entry.getValue().attachToSubparser(subparser);
}
ArgumentParser parser = buildArgumentParser();
Namespace ns;
try {
@ -280,4 +282,36 @@ public class Main {
}
return ns;
}
private static ArgumentParser buildArgumentParser() {
ArgumentParser parser = ArgumentParsers.newFor("signal-cli")
.build()
.defaultHelp(true)
.description("Commandline interface for Signal.")
.version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
parser.addArgument("--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
parser.addArgument("-n", "--busname")
.help("Name of the DBus.");
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification.");
mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue());
mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue());
Subparsers subparsers = parser.addSubparsers()
.title("subcommands")
.dest("command")
.description("valid subcommands")
.help("additional help");
final Map<String, Command> commands = Commands.getCommands();
for (Map.Entry<String, Command> entry : commands.entrySet()) {
Subparser subparser = subparsers.addParser(entry.getKey());
entry.getValue().attachToSubparser(subparser);
}
return parser;
}
}

View file

@ -1,9 +1,10 @@
package org.asamk.signal;
import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.storage.contacts.ContactInfo;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.util.DateUtils;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
@ -328,8 +329,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp()));
if (typingMessage.getGroupId().isPresent()) {
System.out.println(" - Group Info:");
System.out.println(" Id: " + Base64.encodeBytes(typingMessage.getGroupId().get()));
GroupInfo group = m.getGroup(typingMessage.getGroupId().get());
final GroupId groupId = GroupId.unknownVersion(typingMessage.getGroupId().get());
System.out.println(" Id: " + groupId.toBase64());
GroupInfo group = m.getGroup(groupId);
if (group != null) {
System.out.println(" Name: " + group.getTitle());
} else {
@ -356,13 +358,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (message.getGroupContext().isPresent()) {
System.out.println("Group info:");
final SignalServiceGroupContext groupContext = message.getGroupContext().get();
final GroupId groupId = GroupUtils.getGroupId(groupContext);
if (groupContext.getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = groupContext.getGroupV1().get();
System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
System.out.println(" Id: " + groupId.toBase64());
if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
System.out.println(" Name: " + groupInfo.getName().get());
} else {
GroupInfo group = m.getGroup(groupInfo.getGroupId());
GroupInfo group = m.getGroup(groupId);
if (group != null) {
System.out.println(" Name: " + group.getTitle());
} else {
@ -381,8 +384,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
} else if (groupContext.getGroupV2().isPresent()) {
final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get();
byte[] groupId = GroupUtils.getGroupId(groupInfo.getMasterKey());
System.out.println(" Id: " + Base64.encodeBytes(groupId));
System.out.println(" Id: " + groupId.toBase64());
GroupInfo group = m.getGroup(groupId);
if (group != null) {
System.out.println(" Name: " + group.getTitle());
@ -447,14 +449,20 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
if (message.getQuote().isPresent()) {
SignalServiceDataMessage.Quote quote = message.getQuote().get();
System.out.println("Quote: (" + quote.getId() + ")");
System.out.println(" Author: " + quote.getAuthor().getLegacyIdentifier());
System.out.println(" Author: " + m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier());
System.out.println(" Text: " + quote.getText());
if (quote.getMentions() != null && quote.getMentions().size() > 0) {
System.out.println(" Mentions: ");
for (SignalServiceDataMessage.Mention mention : quote.getMentions()) {
printMention(mention, m);
}
}
if (quote.getAttachments().size() > 0) {
System.out.println(" Attachments: ");
for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : quote.getAttachments()) {
System.out.println(" Filename: " + attachment.getFileName());
System.out.println(" Type: " + attachment.getContentType());
System.out.println(" Thumbnail:");
System.out.println(" - Filename: " + attachment.getFileName());
System.out.println(" Type: " + attachment.getContentType());
System.out.println(" Thumbnail:");
if (attachment.getThumbnail() != null) {
printAttachment(attachment.getThumbnail());
}
@ -467,16 +475,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
System.out.println("Remote delete message: timestamp = " + remoteDelete.getTargetSentTimestamp());
}
if (message.getMentions().isPresent()) {
final List<SignalServiceDataMessage.Mention> mentions = message.getMentions().get();
System.out.println("Mentions: ");
for (SignalServiceDataMessage.Mention mention : mentions) {
System.out.println("- "
+ mention.getUuid()
+ ": "
+ mention.getStart()
+ " (length: "
+ mention.getLength()
+ ")");
for (SignalServiceDataMessage.Mention mention : message.getMentions().get()) {
printMention(mention, m);
}
}
@ -488,6 +489,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
}
}
private void printMention(SignalServiceDataMessage.Mention mention, Manager m) {
System.out.println("- " + m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null))
.getLegacyIdentifier() + ": " + mention.getStart() + " (length: " + mention.getLength() + ")");
}
private void printAttachment(SignalServiceAttachment attachment) {
System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (
attachment.isStream() ? "Stream" : ""

View file

@ -23,10 +23,6 @@ public class AddDeviceCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
try {
m.addDeviceLink(new URI(ns.getString("uri")));
return 0;

View file

@ -3,9 +3,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
@ -20,11 +21,6 @@ public class BlockCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
for (String contact_number : ns.<String>getList("contact")) {
try {
m.setContactBlocked(contact_number, true);
@ -36,7 +32,7 @@ public class BlockCommand implements LocalCommand {
if (ns.<String>getList("group") != null) {
for (String groupIdString : ns.<String>getList("group")) {
try {
byte[] groupId = Util.decodeGroupId(groupIdString);
GroupId groupId = Util.decodeGroupId(groupIdString);
m.setGroupBlocked(groupId, true);
} catch (GroupIdFormatException | GroupNotFoundException e) {
System.err.println(e.getMessage());

View file

@ -11,11 +11,13 @@ public class Commands {
addCommand("addDevice", new AddDeviceCommand());
addCommand("block", new BlockCommand());
addCommand("daemon", new DaemonCommand());
addCommand("getUserStatus", new GetUserStatusCommand());
addCommand("link", new LinkCommand());
addCommand("listContacts", new ListContactsCommand());
addCommand("listDevices", new ListDevicesCommand());
addCommand("listGroups", new ListGroupsCommand());
addCommand("listIdentities", new ListIdentitiesCommand());
addCommand("joinGroup", new JoinGroupCommand());
addCommand("quitGroup", new QuitGroupCommand());
addCommand("receive", new ReceiveCommand());
addCommand("register", new RegisterCommand());

View file

@ -35,10 +35,6 @@ public class DaemonCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
DBusConnection conn = null;
try {
try {

View file

@ -0,0 +1,77 @@
package org.asamk.signal.commands;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GetUserStatusCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("number").help("Phone number").nargs("+");
subparser.help("Check if the specified phone number/s have been registered");
subparser.addArgument("--json")
.help("Output received messages in json format, one json object per line.")
.action(Arguments.storeTrue());
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
// Setup the json object mapper
ObjectMapper jsonProcessor = new ObjectMapper();
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
// Get a map of registration statuses
Map<String, Boolean> registered;
try {
registered = m.areUsersRegistered(new HashSet<>(ns.getList("number")));
} catch (IOException e) {
System.err.println("Unable to check if users are registered");
return 1;
}
// Output
if (ns.getBoolean("json")) {
List<JsonIsRegistered> objects = registered.entrySet()
.stream()
.map(entry -> new JsonIsRegistered(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
try {
jsonProcessor.writeValue(System.out, objects);
System.out.println();
} catch (IOException e) {
System.err.println(e.getMessage());
}
} else {
for (Map.Entry<String, Boolean> entry : registered.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
return 0;
}
private static final class JsonIsRegistered {
public String name;
public boolean isRegistered;
public JsonIsRegistered(String name, boolean isRegistered) {
this.name = name;
this.isRegistered = isRegistered;
}
}
}

View file

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

View file

@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.manager.storage.contacts.ContactInfo;
import java.util.List;
@ -16,10 +16,6 @@ public class ListContactsCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
List<ContactInfo> contacts = m.getContacts();
for (ContactInfo c : contacts) {
System.out.println(String.format("Number: %s Name: %s Blocked: %b", c.number, c.name, c.blocked));

View file

@ -18,10 +18,6 @@ public class ListDevicesCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
try {
List<DeviceInfo> devices = m.getLinkedDevices();
for (DeviceInfo d : devices) {

View file

@ -5,9 +5,9 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
import java.util.List;
import java.util.Set;
@ -35,18 +35,21 @@ public class ListGroupsCommand implements LocalCommand {
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink();
System.out.println(String.format(
"Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s",
Base64.encodeBytes(group.groupId),
"Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s",
group.getGroupId().toBase64(),
group.getTitle(),
group.isMember(m.getSelfAddress()),
group.isBlocked(),
members,
pendingMembers,
requestingMembers));
requestingMembers,
groupInviteLink == null ? '-' : groupInviteLink.getUrl()));
} else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
Base64.encodeBytes(group.groupId),
group.getGroupId().toBase64(),
group.getTitle(),
group.isMember(m.getSelfAddress()),
group.isBlocked()));
@ -61,11 +64,6 @@ public class ListGroupsCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
List<GroupInfo> groups = m.getGroups();
boolean detailed = ns.getBoolean("detailed");

View file

@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
import org.asamk.signal.manager.storage.protocol.IdentityInfo;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
@ -13,7 +13,7 @@ import java.util.List;
public class ListIdentitiesCommand implements LocalCommand {
private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) {
private static void printIdentityFingerprint(Manager m, IdentityInfo 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(),
@ -30,19 +30,15 @@ public class ListIdentitiesCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
if (ns.get("number") == null) {
for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) {
for (IdentityInfo identity : m.getIdentities()) {
printIdentityFingerprint(m, identity);
}
} else {
String number = ns.getString("number");
try {
List<JsonIdentityKeyStore.Identity> identities = m.getIdentities(number);
for (JsonIdentityKeyStore.Identity id : identities) {
List<IdentityInfo> identities = m.getIdentities(number);
for (IdentityInfo id : identities) {
printIdentityFingerprint(m, id);
}
} catch (InvalidNumberException e) {

View file

@ -3,10 +3,11 @@ package org.asamk.signal.commands;
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.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
@ -30,13 +31,8 @@ public class QuitGroupCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
try {
final byte[] groupId = Util.decodeGroupId(ns.getString("group"));
final GroupId groupId = Util.decodeGroupId(ns.getString("group"));
final Pair<Long, List<SendMessageResult>> results = m.sendQuitGroupMessage(groupId);
return handleTimestampAndSendMessageResults(results.first(), results.second());
} catch (IOException e) {

View file

@ -44,7 +44,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
final ObjectMapper jsonProcessor;
if (ns.getBoolean("json")) {
jsonProcessor = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
} else {
jsonProcessor = null;
@ -146,10 +146,6 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
double timeout = 5;
if (ns.getDouble("timeout") != null) {
timeout = ns.getDouble("timeout");

View file

@ -4,12 +4,12 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.RegistrationManager;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import java.io.IOException;
public class RegisterCommand implements LocalCommand {
public class RegisterCommand implements RegistrationCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
@ -21,7 +21,7 @@ public class RegisterCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
public int handleCommand(final Namespace ns, final RegistrationManager m) {
try {
final boolean voiceVerification = ns.getBoolean("voice");
final String captcha = ns.getString("captcha");

View file

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

View file

@ -19,10 +19,6 @@ public class RemoveDeviceCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
try {
int deviceId = ns.getInt("deviceId");
m.removeLinkedDevices(deviceId);

View file

@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
@ -16,14 +17,10 @@ public class RemovePinCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
try {
m.setRegistrationLockPin(Optional.absent());
return 0;
} catch (IOException e) {
} catch (IOException | UnauthenticatedResponseException e) {
System.err.println("Remove pin error: " + e.getMessage());
return 3;
}

View file

@ -5,7 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
@ -79,7 +79,7 @@ public class SendCommand implements DbusCommand {
if (ns.getString("group") != null) {
byte[] groupId;
try {
groupId = Util.decodeGroupId(ns.getString("group"));
groupId = Util.decodeGroupId(ns.getString("group")).serialize();
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;

View file

@ -17,10 +17,6 @@ public class SendContactsCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
try {
m.sendContacts();
return 0;

View file

@ -4,10 +4,11 @@ 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.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
@ -46,11 +47,6 @@ public class SendReactionCommand implements LocalCommand {
@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.");
@ -65,7 +61,7 @@ public class SendReactionCommand implements LocalCommand {
try {
final Pair<Long, List<SendMessageResult>> results;
if (ns.getString("group") != null) {
byte[] groupId = Util.decodeGroupId(ns.getString("group"));
GroupId groupId = Util.decodeGroupId(ns.getString("group"));
results = m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
} else {
results = m.sendMessageReaction(emoji,
@ -74,8 +70,7 @@ public class SendReactionCommand implements LocalCommand {
targetTimestamp,
ns.getList("recipient"));
}
handleTimestampAndSendMessageResults(results.first(), results.second());
return 0;
return handleTimestampAndSendMessageResults(results.first(), results.second());
} catch (IOException e) {
handleIOException(e);
return 3;

View file

@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
@ -18,15 +19,11 @@ public class SetPinCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
try {
String registrationLockPin = ns.getString("registrationLockPin");
m.setRegistrationLockPin(Optional.of(registrationLockPin));
return 0;
} catch (IOException e) {
} catch (IOException | UnauthenticatedResponseException e) {
System.err.println("Set pin error: " + e.getMessage());
return 3;
}

View file

@ -27,10 +27,6 @@ public class TrustCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
String number = ns.getString("number");
if (ns.getBoolean("trust_all_known_keys")) {
boolean res = m.trustIdentityAllKeys(number);

View file

@ -3,9 +3,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
@ -20,11 +21,6 @@ public class UnblockCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
for (String contact_number : ns.<String>getList("contact")) {
try {
m.setContactBlocked(contact_number, false);
@ -36,7 +32,7 @@ public class UnblockCommand implements LocalCommand {
if (ns.<String>getList("group") != null) {
for (String groupIdString : ns.<String>getList("group")) {
try {
byte[] groupId = Util.decodeGroupId(groupIdString);
GroupId groupId = Util.decodeGroupId(groupIdString);
m.setGroupBlocked(groupId, false);
} catch (GroupIdFormatException | GroupNotFoundException e) {
System.err.println(e.getMessage());

View file

@ -16,10 +16,6 @@ public class UnregisterCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
try {
m.unregister();
return 0;

View file

@ -16,10 +16,6 @@ public class UpdateAccountCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
try {
m.updateAccountAttributes();
return 0;

View file

@ -23,11 +23,6 @@ public class UpdateContactCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
String number = ns.getString("number");
String name = ns.getString("name");

View file

@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.util.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.util.Base64;
@ -35,7 +35,7 @@ public class UpdateGroupCommand implements DbusCommand {
byte[] groupId = null;
if (ns.getString("group") != null) {
try {
groupId = Util.decodeGroupId(ns.getString("group"));
groupId = Util.decodeGroupId(ns.getString("group")).serialize();
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;

View file

@ -25,11 +25,6 @@ public class UpdateProfileCommand implements LocalCommand {
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
String name = ns.getString("name");
String avatarPath = ns.getString("avatar");
boolean removeAvatar = ns.getBoolean("remove_avatar");

View file

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

View file

@ -3,12 +3,14 @@ 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.RegistrationManager;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.internal.push.LockedException;
import java.io.IOException;
public class VerifyCommand implements LocalCommand {
public class VerifyCommand implements RegistrationCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
@ -17,11 +19,7 @@ public class VerifyCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (m.isRegistered()) {
System.err.println("User registration is already verified");
return 1;
}
public int handleCommand(final Namespace ns, final RegistrationManager m) {
try {
String verificationCode = ns.getString("verificationCode");
String pin = ns.getString("pin");
@ -31,6 +29,12 @@ public class VerifyCommand implements LocalCommand {
System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: "
+ (e.getTimeRemaining() / 1000 / 60 / 60));
System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN");
return 1;
} catch (KeyBackupServicePinException e) {
System.err.println("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
return 1;
} catch (KeyBackupSystemNoDataException e) {
System.err.println("Verification failed! No KBS data.");
return 3;
} catch (IOException e) {
System.err.println("Verify error: " + e.getMessage());

View file

@ -2,10 +2,11 @@ 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.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.util.ErrorUtils;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.libsignal.util.Pair;
@ -15,7 +16,6 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ -92,7 +92,9 @@ public class DbusSignalImpl implements Signal {
@Override
public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
try {
Pair<Long, List<SendMessageResult>> results = m.sendGroupMessage(message, attachments, groupId);
Pair<Long, List<SendMessageResult>> results = m.sendGroupMessage(message,
attachments,
GroupId.unknownVersion(groupId));
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (IOException e) {
@ -134,7 +136,7 @@ public class DbusSignalImpl implements Signal {
@Override
public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
try {
m.setGroupBlocked(groupId, blocked);
m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked);
} catch (GroupNotFoundException e) {
throw new Error.GroupNotFound(e.getMessage());
}
@ -145,14 +147,14 @@ public class DbusSignalImpl implements Signal {
List<GroupInfo> groups = m.getGroups();
List<byte[]> ids = new ArrayList<>(groups.size());
for (GroupInfo group : groups) {
ids.add(group.groupId);
ids.add(group.getGroupId().serialize());
}
return ids;
}
@Override
public String getGroupName(final byte[] groupId) {
GroupInfo group = m.getGroup(groupId);
GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId));
if (group == null) {
return "";
} else {
@ -162,9 +164,9 @@ public class DbusSignalImpl implements Signal {
@Override
public List<String> getGroupMembers(final byte[] groupId) {
GroupInfo group = m.getGroup(groupId);
GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId));
if (group == null) {
return Collections.emptyList();
return List.of();
} else {
return group.getMembers()
.stream()
@ -189,9 +191,11 @@ public class DbusSignalImpl implements Signal {
if (avatar.isEmpty()) {
avatar = null;
}
final Pair<byte[], List<SendMessageResult>> results = m.updateGroup(groupId, name, members, avatar);
final Pair<GroupId, List<SendMessageResult>> results = m.updateGroup(groupId == null
? null
: GroupId.unknownVersion(groupId), name, members, avatar);
checkSendMessageResults(0, results.second());
return results.first();
return results.first().serialize();
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (GroupNotFoundException | NotAGroupMemberException e) {

View file

@ -1,12 +1,11 @@
package org.asamk.signal.json;
import org.asamk.Signal;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@ -15,10 +14,14 @@ class JsonDataMessage {
long timestamp;
String message;
int expiresInSeconds;
JsonReaction reaction;
JsonQuote quote;
List<JsonMention> mentions;
List<JsonAttachment> attachments;
JsonGroupInfo groupInfo;
JsonDataMessage(SignalServiceDataMessage dataMessage) {
JsonDataMessage(SignalServiceDataMessage dataMessage, Manager m) {
this.timestamp = dataMessage.getTimestamp();
if (dataMessage.getGroupContext().isPresent()) {
if (dataMessage.getGroupContext().get().getGroupV1().isPresent()) {
@ -33,13 +36,29 @@ class JsonDataMessage {
this.message = dataMessage.getBody().get();
}
this.expiresInSeconds = dataMessage.getExpiresInSeconds();
if (dataMessage.getAttachments().isPresent()) {
this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size());
for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) {
this.attachments.add(new JsonAttachment(attachment));
}
if (dataMessage.getReaction().isPresent()) {
this.reaction = new JsonReaction(dataMessage.getReaction().get(), m);
}
if (dataMessage.getQuote().isPresent()) {
this.quote = new JsonQuote(dataMessage.getQuote().get(), m);
}
if (dataMessage.getMentions().isPresent()) {
this.mentions = dataMessage.getMentions()
.get()
.stream()
.map(mention -> new JsonMention(mention, m))
.collect(Collectors.toList());
} else {
this.attachments = new ArrayList<>();
this.mentions = List.of();
}
if (dataMessage.getAttachments().isPresent()) {
this.attachments = dataMessage.getAttachments()
.get()
.stream()
.map(JsonAttachment::new)
.collect(Collectors.toList());
} else {
this.attachments = List.of();
}
}
@ -47,6 +66,9 @@ class JsonDataMessage {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
reaction = null; // TODO Replace these 3 with the proper commands
quote = null;
mentions = null;
attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
}
@ -54,6 +76,9 @@ class JsonDataMessage {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
reaction = null; // TODO Replace these 3 with the proper commands
quote = null;
mentions = null;
attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
}
}

View file

@ -1,6 +1,6 @@
package org.asamk.signal.json;
import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.manager.groups.GroupUtils;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -31,7 +31,7 @@ class JsonGroupInfo {
}
JsonGroupInfo(SignalServiceGroupV2 groupInfo) {
this.groupId = Base64.encodeBytes(GroupUtils.getGroupId(groupInfo.getMasterKey()));
this.groupId = GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64();
this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER";
}

View file

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

View file

@ -1,6 +1,7 @@
package org.asamk.signal.json;
import org.asamk.Signal;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -18,7 +19,7 @@ public class JsonMessageEnvelope {
JsonCallMessage callMessage;
JsonReceiptMessage receiptMessage;
public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content) {
public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content, Manager m) {
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
SignalServiceAddress source = envelope.getSourceAddress();
this.source = source.getLegacyIdentifier();
@ -35,10 +36,10 @@ public class JsonMessageEnvelope {
this.sourceDevice = content.getSenderDevice();
}
if (content.getDataMessage().isPresent()) {
this.dataMessage = new JsonDataMessage(content.getDataMessage().get());
this.dataMessage = new JsonDataMessage(content.getDataMessage().get(), m);
}
if (content.getSyncMessage().isPresent()) {
this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get());
this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get(), m);
}
if (content.getCallMessage().isPresent()) {
this.callMessage = new JsonCallMessage(content.getCallMessage().get());

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,65 @@
package org.asamk.signal.manager;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public class DeviceLinkInfo {
final String deviceIdentifier;
final ECPublicKey deviceKey;
public static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException {
final String rawQuery = linkUri.getRawQuery();
if (isEmpty(rawQuery)) {
throw new RuntimeException("Invalid device link uri");
}
Map<String, String> query = getQueryMap(rawQuery);
String deviceIdentifier = query.get("uuid");
String publicKeyEncoded = query.get("pub_key");
if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) {
throw new RuntimeException("Invalid device link uri");
}
ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
return new DeviceLinkInfo(deviceIdentifier, deviceKey);
}
private static Map<String, String> getQueryMap(String query) {
String[] params = query.split("&");
Map<String, String> map = new HashMap<>();
for (String param : params) {
final String[] paramParts = param.split("=");
String name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8);
String value = URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8);
map.put(name, value);
}
return map;
}
public DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) {
this.deviceIdentifier = deviceIdentifier;
this.deviceKey = deviceKey;
}
public String createDeviceLinkUri() {
return "tsdevice:/?uuid="
+ URLEncoder.encode(deviceIdentifier, StandardCharsets.UTF_8)
+ "&pub_key="
+ URLEncoder.encode(Base64.encodeBytesWithoutPadding(deviceKey.serialize()), StandardCharsets.UTF_8);
}
}

View file

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

View file

@ -1,8 +1,8 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Arrays;
import java.util.Objects;
interface HandleAction {
@ -93,9 +93,9 @@ class SendSyncBlockedListAction implements HandleAction {
class SendGroupInfoRequestAction implements HandleAction {
private final SignalServiceAddress address;
private final byte[] groupId;
private final GroupIdV1 groupId;
public SendGroupInfoRequestAction(final SignalServiceAddress address, final byte[] groupId) {
public SendGroupInfoRequestAction(final SignalServiceAddress address, final GroupIdV1 groupId) {
this.address = address;
this.groupId = groupId;
}
@ -109,14 +109,17 @@ class SendGroupInfoRequestAction implements HandleAction {
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o;
return address.equals(that.address) && Arrays.equals(groupId, that.groupId);
if (!address.equals(that.address)) return false;
return groupId.equals(that.groupId);
}
@Override
public int hashCode() {
int result = Objects.hash(address);
result = 31 * result + Arrays.hashCode(groupId);
int result = address.hashCode();
result = 31 * result + groupId.hashCode();
return result;
}
}
@ -124,9 +127,9 @@ class SendGroupInfoRequestAction implements HandleAction {
class SendGroupUpdateAction implements HandleAction {
private final SignalServiceAddress address;
private final byte[] groupId;
private final GroupIdV1 groupId;
public SendGroupUpdateAction(final SignalServiceAddress address, final byte[] groupId) {
public SendGroupUpdateAction(final SignalServiceAddress address, final GroupIdV1 groupId) {
this.address = address;
this.groupId = groupId;
}
@ -140,14 +143,17 @@ class SendGroupUpdateAction implements HandleAction {
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendGroupUpdateAction that = (SendGroupUpdateAction) o;
return address.equals(that.address) && Arrays.equals(groupId, that.groupId);
if (!address.equals(that.address)) return false;
return groupId.equals(that.groupId);
}
@Override
public int hashCode() {
int result = Objects.hash(address);
result = 31 * result + Arrays.hashCode(groupId);
int result = address.hashCode();
result = 31 * result + groupId.hashCode();
return result;
}
}

View file

@ -1,47 +0,0 @@
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 {
private KeyUtils() {
}
static String createSignalingKey() {
return getSecret(52);
}
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() {
return getSecret(18);
}
static byte[] createGroupId() {
return getSecretBytes(16);
}
static byte[] createStickerUploadKey() {
return getSecretBytes(32);
}
private static String getSecret(int size) {
byte[] secret = getSecretBytes(size);
return Base64.encodeBytes(secret);
}
private static byte[] getSecretBytes(int size) {
byte[] secret = new byte[size];
RandomUtils.getSecureRandom().nextBytes(secret);
return secret;
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager;
public class NotRegisteredException extends Exception {
public NotRegisteredException() {
super("User is not registered.");
}
}

View file

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

View file

@ -1,5 +1,5 @@
/*
Copyright (C) 2015-2020 AsamK and contributors
Copyright (C) 2015-2021 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
@ -16,7 +16,8 @@
*/
package org.asamk.signal.manager;
import org.asamk.signal.storage.SignalAccount;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKeyPair;
@ -31,6 +32,7 @@ import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
@ -45,12 +47,12 @@ public class ProvisioningManager {
private final int registrationId;
private final String password;
public ProvisioningManager(String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) {
public ProvisioningManager(File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) {
this.pathConfig = PathConfig.createDefault(settingsPath);
this.serviceConfiguration = serviceConfiguration;
this.userAgent = userAgent;
identityKey = KeyHelper.generateIdentityKeyPair();
identityKey = KeyUtils.generateIdentityKeyPair();
registrationId = KeyHelper.generateRegistrationId(false);
password = KeyUtils.createPassword();
final SleepTimer timer = new UptimeSleepTimer();
@ -70,8 +72,7 @@ public class ProvisioningManager {
public String getDeviceLinkUri() throws TimeoutException, IOException {
String deviceUuid = accountManager.getNewDeviceUuid();
return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid,
identityKey.getPublicKey().getPublicKey()));
return new DeviceLinkInfo(deviceUuid, identityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
}
public String finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
@ -123,8 +124,10 @@ public class ProvisioningManager {
m.requestSyncBlocked();
m.requestSyncConfiguration();
m.saveAccount();
m.close(false);
}
account.save();
}
return username;

View file

@ -0,0 +1,194 @@
/*
Copyright (C) 2015-2021 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.manager.helper.PinHelper;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
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.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.LockedException;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
public class RegistrationManager implements AutoCloseable {
private SignalAccount account;
private final PathConfig pathConfig;
private final SignalServiceConfiguration serviceConfiguration;
private final String userAgent;
private final SignalServiceAccountManager accountManager;
private final PinHelper pinHelper;
public RegistrationManager(
SignalAccount account,
PathConfig pathConfig,
SignalServiceConfiguration serviceConfiguration,
String userAgent
) {
this.account = account;
this.pathConfig = pathConfig;
this.serviceConfiguration = serviceConfiguration;
this.userAgent = userAgent;
final SleepTimer timer = new UptimeSleepTimer();
this.accountManager = new SignalServiceAccountManager(serviceConfiguration, new DynamicCredentialsProvider(
// Using empty UUID, because registering doesn't work otherwise
null,
account.getUsername(),
account.getPassword(),
account.getSignalingKey(),
SignalServiceAddress.DEFAULT_DEVICE_ID), userAgent, null, timer);
final KeyBackupService keyBackupService = ServiceConfig.createKeyBackupService(accountManager);
this.pinHelper = new PinHelper(keyBackupService);
}
public static RegistrationManager init(
String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent
) throws IOException {
PathConfig pathConfig = PathConfig.createDefault(settingsPath);
if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
IdentityKeyPair identityKey = KeyUtils.generateIdentityKeyPair();
int registrationId = KeyHelper.generateRegistrationId(false);
ProfileKey profileKey = KeyUtils.createProfileKey();
SignalAccount account = SignalAccount.create(pathConfig.getDataPath(),
username,
identityKey,
registrationId,
profileKey);
account.save();
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
}
SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username);
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
}
public void register(boolean voiceVerification, String captcha) throws IOException {
if (account.getPassword() == null) {
account.setPassword(KeyUtils.createPassword());
}
if (voiceVerification) {
accountManager.requestVoiceVerificationCode(Locale.getDefault(),
Optional.fromNullable(captcha),
Optional.absent());
} else {
accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent());
}
account.setRegistered(false);
account.save();
}
public void verifyAccount(
String verificationCode, String pin
) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
verificationCode = verificationCode.replace("-", "");
if (account.getSignalingKey() == null) {
account.setSignalingKey(KeyUtils.createSignalingKey());
}
VerifyAccountResponse response;
try {
response = verifyAccountWithCode(verificationCode, pin, null);
account.setPinMasterKey(null);
} catch (LockedException e) {
if (pin == null) {
throw e;
}
KbsPinData registrationLockData = pinHelper.getRegistrationLockData(pin, e);
if (registrationLockData == null) {
throw e;
}
String registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
try {
response = verifyAccountWithCode(verificationCode, null, registrationLock);
} catch (LockedException _e) {
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
}
account.setPinMasterKey(registrationLockData.getMasterKey());
}
// TODO response.isStorageCapable()
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
account.setDeviceId(SignalServiceAddress.DEFAULT_DEVICE_ID);
account.setMultiDevice(false);
account.setRegistered(true);
account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
account.setRegistrationLockPin(pin);
account.getSignalProtocolStore()
.saveIdentity(account.getSelfAddress(),
account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(),
TrustLevel.TRUSTED_VERIFIED);
try (Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent)) {
m.refreshPreKeys();
m.close(false);
}
account.save();
}
private VerifyAccountResponse verifyAccountWithCode(
final String verificationCode, final String legacyPin, final String registrationLock
) throws IOException {
return accountManager.verifyAccountWithCode(verificationCode,
account.getSignalingKey(),
account.getSignalProtocolStore().getLocalRegistrationId(),
true,
legacyPin,
registrationLock,
account.getSelfUnidentifiedAccessKey(),
account.isUnrestrictedUnidentifiedAccess(),
ServiceConfig.capabilities,
account.isDiscoverableByPhoneNumber());
}
@Override
public void close() throws Exception {
if (account != null) {
account.close();
account = null;
}
}
}

View file

@ -1,7 +1,13 @@
package org.asamk.signal.manager;
import org.bouncycastle.util.encoders.Hex;
import org.signal.zkgroup.ServerPublicParams;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
@ -10,14 +16,13 @@ import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupSe
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.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.Base64;
import java.util.List;
import java.util.Map;
@ -26,7 +31,8 @@ import okhttp3.Interceptor;
public class ServiceConfig {
final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF";
final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
final static int PREKEY_MINIMUM_COUNT = 20;
final static int PREKEY_BATCH_SIZE = 100;
final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
@ -35,6 +41,11 @@ public class ServiceConfig {
final static String CDS_MRENCLAVE = "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15";
final static String KEY_BACKUP_ENCLAVE_NAME = "fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe";
final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode(
"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe");
final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87";
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";
@ -46,18 +57,12 @@ public class ServiceConfig {
private final static Optional<Dns> dns = Optional.absent();
private final static String zkGroupServerPublicParamsHex = "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=";
private final static byte[] zkGroupServerPublicParams;
private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=");
static final AccountAttributes.Capabilities capabilities;
static {
try {
zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex);
} catch (IOException e) {
throw new AssertionError(e);
}
boolean zkGroupAvailable;
try {
new ServerPublicParams(zkGroupServerPublicParams);
@ -74,7 +79,7 @@ public class ServiceConfig {
.header("User-Agent", userAgent)
.build());
final List<Interceptor> interceptors = Collections.singletonList(userAgentInterceptor);
final List<Interceptor> interceptors = List.of(userAgentInterceptor);
return new SignalServiceConfiguration(new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
@ -88,6 +93,10 @@ public class ServiceConfig {
zkGroupServerPublicParams);
}
public static AccountAttributes.Capabilities getCapabilities() {
return capabilities;
}
static KeyStore getIasKeyStore() {
try {
TrustStore contactTrustStore = IAS_TRUST_STORE;
@ -102,6 +111,24 @@ public class ServiceConfig {
}
}
static KeyBackupService createKeyBackupService(SignalServiceAccountManager accountManager) {
KeyStore keyStore = ServiceConfig.getIasKeyStore();
return accountManager.getKeyBackupService(keyStore,
ServiceConfig.KEY_BACKUP_ENCLAVE_NAME,
ServiceConfig.KEY_BACKUP_SERVICE_ID,
ServiceConfig.KEY_BACKUP_MRENCLAVE,
10);
}
static ECPublicKey getUnidentifiedSenderTrustRoot() {
try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
} catch (InvalidKeyException e) {
throw new AssertionError(e);
}
}
private static Map<Integer, SignalCdnUrl[]> makeSignalCdnUrlMapFor(
SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls
) {

View file

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

View file

@ -1,304 +0,0 @@
package org.asamk.signal.manager;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.fingerprint.Fingerprint;
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
import org.whispersystems.libsignal.util.guava.Optional;
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.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;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
class Utils {
static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
List<SignalServiceAttachment> signalServiceAttachments = null;
if (attachments != null) {
signalServiceAttachments = new ArrayList<>(attachments.size());
for (String attachment : attachments) {
try {
signalServiceAttachments.add(createAttachment(new File(attachment)));
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
}
}
}
return signalServiceAttachments;
}
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))) {
mime = URLConnection.guessContentTypeFromStream(bufferedStream);
}
}
if (mime == null) {
return defaultMimeType;
}
return mime;
}
static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
InputStream attachmentStream = new FileInputStream(attachmentFile);
final long attachmentSize = attachmentFile.length();
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();
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 {
InputStream stream = new FileInputStream(file);
final long size = file.length();
String mime = Files.probeContentType(file.toPath());
if (mime == null) {
mime = "application/octet-stream";
}
return new StreamDetails(stream, mime, size);
}
static CertificateValidator getCertificateValidator() {
try {
ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT),
0);
return new CertificateValidator(unidentifiedSenderTrustRoot);
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
}
}
private static Map<String, String> getQueryMap(String query) {
String[] params = query.split("&");
Map<String, String> map = new HashMap<>();
for (String param : params) {
final String[] paramParts = param.split("=");
String name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8);
String value = URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8);
map.put(name, value);
}
return map;
}
static String createDeviceLinkUri(DeviceLinkInfo info) {
return "tsdevice:/?uuid="
+ URLEncoder.encode(info.deviceIdentifier, StandardCharsets.UTF_8)
+ "&pub_key="
+ URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()),
StandardCharsets.UTF_8);
}
static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException {
Map<String, String> query = getQueryMap(linkUri.getRawQuery());
String deviceIdentifier = query.get("uuid");
String publicKeyEncoded = query.get("pub_key");
if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) {
throw new RuntimeException("Invalid device link uri");
}
ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
return new DeviceLinkInfo(deviceIdentifier, deviceKey);
}
static SignalServiceEnvelope loadEnvelope(File file) throws IOException {
try (FileInputStream f = new FileInputStream(file)) {
DataInputStream in = new DataInputStream(f);
int version = in.readInt();
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
in.readUTF();
}
long timestamp = in.readLong();
byte[] content = null;
int contentLen = in.readInt();
if (contentLen > 0) {
content = new byte[contentLen];
in.readFully(content);
}
byte[] legacyMessage = null;
int legacyMessageLen = in.readInt();
if (legacyMessageLen > 0) {
legacyMessage = new byte[legacyMessageLen];
in.readFully(legacyMessage);
}
long serverReceivedTimestamp = 0;
String uuid = null;
if (version >= 2) {
serverReceivedTimestamp = in.readLong();
uuid = in.readUTF();
if ("".equals(uuid)) {
uuid = null;
}
}
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(4); // version
out.writeInt(envelope.getType());
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()) {
out.writeInt(envelope.getContent().length);
out.write(envelope.getContent());
} else {
out.writeInt(0);
}
if (envelope.hasLegacyMessage()) {
out.writeInt(envelope.getLegacyMessage().length);
out.write(envelope.getLegacyMessage());
} else {
out.writeInt(0);
}
out.writeLong(envelope.getServerReceivedTimestamp());
String uuid = envelope.getUuid();
out.writeUTF(uuid == null ? "" : uuid);
out.writeLong(envelope.getServerDeliveredTimestamp());
}
}
}
static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException {
InputStream input = stream.getInputStream();
try (OutputStream output = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[4096];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
return outputFile;
}
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();
}
static class DeviceLinkInfo {
final String deviceIdentifier;
final ECPublicKey deviceKey;
DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) {
this.deviceIdentifier = deviceIdentifier;
this.deviceKey = deviceKey;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.groups;
public class GroupNotFoundException extends Exception {
public GroupNotFoundException(GroupId groupId) {
super("Group not found: " + groupId.toBase64());
}
}

View file

@ -1,14 +1,15 @@
package org.asamk.signal.manager;
package org.asamk.signal.manager.groups;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.storage.groups.GroupInfoV1;
import org.asamk.signal.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
public class GroupUtils {
@ -18,7 +19,7 @@ public class GroupUtils {
) {
if (groupInfo instanceof GroupInfoV1) {
SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
.withId(groupInfo.groupId)
.withId(groupInfo.getGroupId().serialize())
.build();
messageBuilder.asGroupMessage(group);
} else {
@ -30,14 +31,34 @@ public class GroupUtils {
}
}
public static byte[] getGroupId(GroupMasterKey groupMasterKey) {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
return groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
public static GroupId getGroupId(SignalServiceGroupContext context) {
if (context.getGroupV1().isPresent()) {
return GroupId.v1(context.getGroupV1().get().getGroupId());
} else if (context.getGroupV2().isPresent()) {
return getGroupIdV2(context.getGroupV2().get().getMasterKey());
} else {
return null;
}
}
public static GroupMasterKey deriveV2MigrationMasterKey(byte[] groupId) {
public static GroupIdV2 getGroupIdV2(GroupSecretParams groupSecretParams) {
return GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier().serialize());
}
public static GroupIdV2 getGroupIdV2(GroupMasterKey groupMasterKey) {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
return getGroupIdV2(groupSecretParams);
}
public static GroupIdV2 getGroupIdV2(GroupIdV1 groupIdV1) {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(deriveV2MigrationMasterKey(
groupIdV1));
return getGroupIdV2(groupSecretParams);
}
private static GroupMasterKey deriveV2MigrationMasterKey(GroupIdV1 groupIdV1) {
try {
return new GroupMasterKey(new HKDFv3().deriveSecrets(groupId,
return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1.serialize(),
"GV2 Migration".getBytes(),
GroupMasterKey.SIZE));
} catch (InvalidInputException e) {

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.groups;
public class NotAGroupMemberException extends Exception {
public NotAGroupMemberException(GroupId groupId, String groupName) {
super("User is not a member in group: " + groupName + " (" + groupId.toBase64() + ")");
}
}

View file

@ -2,12 +2,18 @@ package org.asamk.signal.manager.helper;
import com.google.protobuf.InvalidProtocolBufferException;
import org.asamk.signal.storage.groups.GroupInfoV2;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupLinkPassword;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.profiles.SignalProfile;
import org.asamk.signal.manager.util.IOUtils;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
@ -15,10 +21,13 @@ import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.groups.UuidCiphertext;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
@ -38,6 +47,8 @@ import java.util.stream.Collectors;
public class GroupHelper {
final static Logger logger = LoggerFactory.getLogger(GroupHelper.class);
private final ProfileKeyCredentialProvider profileKeyCredentialProvider;
private final ProfileProvider profileProvider;
@ -66,6 +77,27 @@ public class GroupHelper {
this.groupAuthorizationProvider = groupAuthorizationProvider;
}
public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
try {
final GroupsV2AuthorizationString groupsV2AuthorizationString = groupAuthorizationProvider.getAuthorizationForToday(
groupSecretParams);
return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
return null;
}
}
public DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
GroupMasterKey groupMasterKey, GroupLinkPassword password
) throws IOException, GroupLinkNotActiveException {
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
return groupsV2Api.getGroupJoinInfo(groupSecretParams,
Optional.fromNullable(password).transform(GroupLinkPassword::serialize),
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
}
public GroupInfoV2 createGroupV2(
String name, Collection<SignalServiceAddress> members, String avatarFile
) throws IOException {
@ -84,15 +116,15 @@ public class GroupHelper {
groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
System.err.println("Failed to create V2 group: " + e.getMessage());
logger.warn("Failed to create V2 group: {}", e.getMessage());
return null;
}
if (decryptedGroup == null) {
System.err.println("Failed to create V2 group!");
logger.warn("Failed to create V2 group, unknown error!");
return null;
}
final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
final GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams);
final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
g.setGroup(decryptedGroup);
@ -114,7 +146,7 @@ public class GroupHelper {
final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
selfAddressProvider.getSelfAddress());
if (profileKeyCredential == null) {
System.err.println("Cannot create a V2 group as self does not have a versioned profile");
logger.warn("Cannot create a V2 group as self does not have a versioned profile");
return null;
}
@ -138,22 +170,23 @@ public class GroupHelper {
}
private boolean areMembersValid(final Collection<SignalServiceAddress> members) {
final int noUuidCapability = members.stream()
final Set<String> noUuidCapability = members.stream()
.filter(address -> !address.getUuid().isPresent())
.collect(Collectors.toUnmodifiableSet())
.size();
if (noUuidCapability > 0) {
System.err.println("Cannot create a V2 group as " + noUuidCapability + " members don't have a UUID.");
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
if (noUuidCapability.size() > 0) {
logger.warn("Cannot create a V2 group as some members don't have a UUID: {}",
String.join(", ", noUuidCapability));
return false;
}
final int noGv2Capability = members.stream()
final Set<SignalProfile> noGv2Capability = members.stream()
.map(profileProvider::getProfile)
.filter(profile -> profile != null && !profile.getCapabilities().gv2)
.collect(Collectors.toUnmodifiableSet())
.size();
if (noGv2Capability > 0) {
System.err.println("Cannot create a V2 group as " + noGv2Capability + " members don't support Groups V2.");
.collect(Collectors.toSet());
if (noGv2Capability.size() > 0) {
logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
noGv2Capability.stream().map(SignalProfile::getName).collect(Collectors.joining(", ")));
return false;
}
@ -192,7 +225,9 @@ public class GroupHelper {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
if (!areMembersValid(newMembers)) return null;
if (!areMembersValid(newMembers)) {
throw new IOException("Failed to update group");
}
Set<GroupCandidate> candidates = newMembers.stream()
.map(member -> new GroupCandidate(member.getUuid().get(),
@ -223,6 +258,32 @@ public class GroupHelper {
}
}
public GroupChange joinGroup(
GroupMasterKey groupMasterKey,
GroupLinkPassword groupLinkPassword,
DecryptedGroupJoinInfo decryptedGroupJoinInfo
) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
final SignalServiceAddress selfAddress = this.selfAddressProvider.getSelfAddress();
final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
selfAddress);
if (profileKeyCredential == null) {
throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
}
boolean requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink()
== AccessControl.AccessRequired.ADMINISTRATOR;
GroupChange.Actions.Builder change = requestToJoin
? groupOperations.createGroupJoinRequest(profileKeyCredential)
: groupOperations.createGroupJoinDirect(profileKeyCredential);
change.setSourceUuid(UuidUtil.toByteString(selfAddress.getUuid().get()));
return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
}
public Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
@ -284,13 +345,27 @@ public class GroupHelper {
throw new IOException(e);
}
GroupChange signedGroupChange = groupsV2Api.patchGroup(change.build(),
GroupChange signedGroupChange = groupsV2Api.patchGroup(changeActions,
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
Optional.absent());
return new Pair<>(decryptedGroupState, signedGroupChange);
}
private GroupChange commitChange(
GroupSecretParams groupSecretParams,
int currentRevision,
GroupChange.Actions.Builder change,
GroupLinkPassword password
) throws IOException {
final int nextRevision = currentRevision + 1;
final GroupChange.Actions changeActions = change.setRevision(nextRevision).build();
return groupsV2Api.patchGroup(changeActions,
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
}
public DecryptedGroup getUpdatedDecryptedGroup(
DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
) {

View file

@ -0,0 +1,89 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.util.PinHashing;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.kbs.HashedPin;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.push.LockedException;
import java.io.IOException;
public class PinHelper {
private final KeyBackupService keyBackupService;
public PinHelper(final KeyBackupService keyBackupService) {
this.keyBackupService = keyBackupService;
}
public void setRegistrationLockPin(
String pin, MasterKey masterKey
) throws IOException, UnauthenticatedResponseException {
final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
final HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession);
pinChangeSession.setPin(hashedPin, masterKey);
pinChangeSession.enableRegistrationLock(masterKey);
}
public void removeRegistrationLockPin() throws IOException, UnauthenticatedResponseException {
final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
pinChangeSession.disableRegistrationLock();
pinChangeSession.removePin();
}
public KbsPinData getRegistrationLockData(
String pin, LockedException e
) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
String basicStorageCredentials = e.getBasicStorageCredentials();
if (basicStorageCredentials == null) {
return null;
}
return getRegistrationLockData(pin, basicStorageCredentials);
}
private KbsPinData getRegistrationLockData(
String pin, String basicStorageCredentials
) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
TokenResponse tokenResponse = keyBackupService.getToken(basicStorageCredentials);
if (tokenResponse == null || tokenResponse.getTries() == 0) {
throw new IOException("KBS Account locked");
}
KbsPinData registrationLockData = restoreMasterKey(pin, basicStorageCredentials, tokenResponse);
if (registrationLockData == null) {
throw new AssertionError("Failed to restore master key");
}
return registrationLockData;
}
private KbsPinData restoreMasterKey(
String pin, String basicStorageCredentials, TokenResponse tokenResponse
) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
if (pin == null) return null;
if (basicStorageCredentials == null) {
throw new AssertionError("Cannot restore KBS key, no storage credentials supplied");
}
KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials,
tokenResponse);
try {
HashedPin hashedPin = PinHashing.hashPin(pin, session);
KbsPinData kbsData = session.restorePin(hashedPin);
if (kbsData == null) {
throw new AssertionError("Null not expected");
}
return kbsData;
} catch (UnauthenticatedResponseException e) {
throw new IOException(e);
}
}
}

View file

@ -95,7 +95,17 @@ public final class ProfileHelper {
? unidentifiedPipe
: messagePipeProvider.getMessagePipe(false);
if (pipe != null) {
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
try {
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
} catch (NoClassDefFoundError e) {
// Native zkgroup lib not available for ProfileKey
if (!address.getNumber().isPresent()) {
throw new NotFoundException("Can't request profile without number");
}
SignalServiceAddress addressWithoutUuid = new SignalServiceAddress(Optional.absent(),
address.getNumber());
return pipe.getProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType);
}
}
throw new IOException("No pipe available!");
@ -106,9 +116,18 @@ public final class ProfileHelper {
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType
) {
) throws NotFoundException {
SignalServiceMessageReceiver receiver = messageReceiverProvider.getMessageReceiver();
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
try {
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
} catch (NoClassDefFoundError e) {
// Native zkgroup lib not available for ProfileKey
if (!address.getNumber().isPresent()) {
throw new NotFoundException("Can't request profile without number");
}
SignalServiceAddress addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber());
return receiver.retrieveProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType);
}
}
private Optional<UnidentifiedAccess> getUnidentifiedAccess(SignalServiceAddress recipient) {

View file

@ -1,6 +1,6 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.storage.profiles.SignalProfile;
import org.asamk.signal.manager.storage.profiles.SignalProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface ProfileProvider {

View file

@ -1,6 +1,6 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.storage.profiles.SignalProfile;
import org.asamk.signal.manager.storage.profiles.SignalProfile;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.guava.Optional;
@ -36,7 +36,7 @@ public class UnidentifiedAccessHelper {
this.senderCertificateProvider = senderCertificateProvider;
}
public byte[] getSelfUnidentifiedAccessKey() {
private byte[] getSelfUnidentifiedAccessKey() {
return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey());
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage;
package org.asamk.signal.manager.storage;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
@ -10,29 +10,36 @@ 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.GroupInfoV1;
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.protocol.RecipientStore;
import org.asamk.signal.storage.protocol.SessionInfo;
import org.asamk.signal.storage.protocol.SignalServiceAddressResolver;
import org.asamk.signal.storage.stickers.StickerStore;
import org.asamk.signal.storage.threads.LegacyJsonThreadStore;
import org.asamk.signal.storage.threads.ThreadInfo;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.contacts.ContactInfo;
import org.asamk.signal.manager.storage.contacts.JsonContactsStore;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.JsonGroupStore;
import org.asamk.signal.manager.storage.messageCache.MessageCache;
import org.asamk.signal.manager.storage.profiles.ProfileStore;
import org.asamk.signal.manager.storage.protocol.IdentityInfo;
import org.asamk.signal.manager.storage.protocol.JsonSignalProtocolStore;
import org.asamk.signal.manager.storage.protocol.RecipientStore;
import org.asamk.signal.manager.storage.protocol.SessionInfo;
import org.asamk.signal.manager.storage.protocol.SignalServiceAddressResolver;
import org.asamk.signal.manager.storage.stickers.StickerStore;
import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
import org.asamk.signal.manager.storage.threads.ThreadInfo;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
@ -52,6 +59,8 @@ import java.util.stream.Collectors;
public class SignalAccount implements Closeable {
final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
private final ObjectMapper jsonProcessor = new ObjectMapper();
private final FileChannel fileChannel;
private final FileLock lock;
@ -61,6 +70,7 @@ public class SignalAccount implements Closeable {
private boolean isMultiDevice = false;
private String password;
private String registrationLockPin;
private MasterKey pinMasterKey;
private String signalingKey;
private ProfileKey profileKey;
private int preKeyIdOffset;
@ -75,6 +85,8 @@ public class SignalAccount implements Closeable {
private ProfileStore profileStore;
private StickerStore stickerStore;
private MessageCache messageCache;
private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
this.fileChannel = fileChannel;
this.lock = lock;
@ -85,12 +97,14 @@ public class SignalAccount implements Closeable {
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}
public static SignalAccount load(String dataPath, String username) throws IOException {
final String fileName = getFileName(dataPath, username);
public static SignalAccount load(File dataPath, String username) throws IOException {
final File fileName = getFileName(dataPath, username);
final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
try {
SignalAccount account = new SignalAccount(pair.first(), pair.second());
account.load(dataPath);
account.migrateLegacyConfigs();
return account;
} catch (Throwable e) {
pair.second().close();
@ -100,11 +114,11 @@ public class SignalAccount implements Closeable {
}
public static SignalAccount create(
String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey
File dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey
) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
String fileName = getFileName(dataPath, username);
if (!new File(fileName).exists()) {
File fileName = getFileName(dataPath, username);
if (!fileName.exists()) {
IOUtils.createPrivateFile(fileName);
}
@ -119,13 +133,16 @@ public class SignalAccount implements Closeable {
account.recipientStore = new RecipientStore();
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
account.registered = false;
return account;
}
public static SignalAccount createLinkedAccount(
String dataPath,
File dataPath,
String username,
UUID uuid,
String password,
@ -136,8 +153,8 @@ public class SignalAccount implements Closeable {
ProfileKey profileKey
) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
String fileName = getFileName(dataPath, username);
if (!new File(fileName).exists()) {
File fileName = getFileName(dataPath, username);
if (!fileName.exists()) {
IOUtils.createPrivateFile(fileName);
}
@ -156,29 +173,65 @@ public class SignalAccount implements Closeable {
account.recipientStore = new RecipientStore();
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
account.registered = true;
account.isMultiDevice = true;
return account;
}
public static String getFileName(String dataPath, String username) {
return dataPath + "/" + username;
public void migrateLegacyConfigs() {
if (getProfileKey() == null && isRegistered()) {
// Old config file, creating new profile key
setProfileKey(KeyUtils.createProfileKey());
save();
}
// Store profile keys only in profile store
for (ContactInfo contact : getContactStore().getContacts()) {
String profileKeyString = contact.profileKey;
if (profileKeyString == null) {
continue;
}
final ProfileKey profileKey;
try {
profileKey = new ProfileKey(Base64.decode(profileKeyString));
} catch (InvalidInputException | IOException e) {
continue;
}
contact.profileKey = null;
getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
}
// Ensure our profile key is stored in profile store
getProfileStore().storeProfileKey(getSelfAddress(), getProfileKey());
}
private static File getGroupCachePath(String dataPath, String username) {
return new File(new File(dataPath, username + ".d"), "group-cache");
public static File getFileName(File dataPath, String username) {
return new File(dataPath, username);
}
public static boolean userExists(String dataPath, String username) {
private static File getUserPath(final File dataPath, final String username) {
return new File(dataPath, username + ".d");
}
public static File getMessageCachePath(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "msg-cache");
}
private static File getGroupCachePath(File dataPath, String username) {
return new File(getUserPath(dataPath, username), "group-cache");
}
public static boolean userExists(File dataPath, String username) {
if (username == null) {
return false;
}
File f = new File(getFileName(dataPath, username));
File f = getFileName(dataPath, username);
return !(!f.exists() || f.isDirectory());
}
private void load(String dataPath) throws IOException {
private void load(File dataPath) throws IOException {
JsonNode rootNode;
synchronized (fileChannel) {
fileChannel.position(0);
@ -198,28 +251,32 @@ public class SignalAccount implements Closeable {
deviceId = node.asInt();
}
if (rootNode.has("isMultiDevice")) {
isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
isMultiDevice = Utils.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
}
username = Util.getNotNullNode(rootNode, "username").asText();
password = Util.getNotNullNode(rootNode, "password").asText();
username = Utils.getNotNullNode(rootNode, "username").asText();
password = Utils.getNotNullNode(rootNode, "password").asText();
JsonNode pinNode = rootNode.get("registrationLockPin");
registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText();
JsonNode pinMasterKeyNode = rootNode.get("pinMasterKey");
pinMasterKey = pinMasterKeyNode == null || pinMasterKeyNode.isNull()
? null
: new MasterKey(Base64.decode(pinMasterKeyNode.asText()));
if (rootNode.has("signalingKey")) {
signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText();
signalingKey = Utils.getNotNullNode(rootNode, "signalingKey").asText();
}
if (rootNode.has("preKeyIdOffset")) {
preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
preKeyIdOffset = Utils.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
} else {
preKeyIdOffset = 0;
}
if (rootNode.has("nextSignedPreKeyId")) {
nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
nextSignedPreKeyId = Utils.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
} else {
nextSignedPreKeyId = 0;
}
if (rootNode.has("profileKey")) {
try {
profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()));
profileKey = new ProfileKey(Base64.decode(Utils.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",
@ -227,9 +284,9 @@ public class SignalAccount implements Closeable {
}
}
signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"),
signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
JsonSignalProtocolStore.class);
registered = Util.getNotNullNode(rootNode, "registered").asBoolean();
registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
JsonNode groupStoreNode = rootNode.get("groupStore");
if (groupStoreNode != null) {
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
@ -273,7 +330,7 @@ public class SignalAccount implements Closeable {
session.address = recipientStore.resolveServiceAddress(session.address);
}
for (JsonIdentityKeyStore.Identity identity : signalProtocolStore.getIdentities()) {
for (IdentityInfo identity : signalProtocolStore.getIdentities()) {
identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
}
}
@ -294,6 +351,8 @@ public class SignalAccount implements Closeable {
stickerStore = new StickerStore();
}
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
JsonNode threadStoreNode = rootNode.get("threadStore");
if (threadStoreNode != null) {
LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode,
@ -309,7 +368,7 @@ public class SignalAccount implements Closeable {
contactInfo.messageExpirationTime = thread.messageExpirationTime;
contactStore.updateContact(contactInfo);
} else {
GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id));
GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
if (groupInfo instanceof GroupInfoV1) {
((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
groupStore.updateGroup(groupInfo);
@ -332,6 +391,7 @@ public class SignalAccount implements Closeable {
.put("isMultiDevice", isMultiDevice)
.put("password", password)
.put("registrationLockPin", registrationLockPin)
.put("pinMasterKey", pinMasterKey == null ? null : Base64.encodeBytes(pinMasterKey.serialize()))
.put("signalingKey", signalingKey)
.put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId)
@ -356,17 +416,17 @@ public class SignalAccount implements Closeable {
}
}
} catch (Exception e) {
System.err.println(String.format("Error saving file: %s", e.getMessage()));
logger.error("Error saving file: {}", e.getMessage());
}
}
private static Pair<FileChannel, FileLock> openFileChannel(String fileName) throws IOException {
FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
FileChannel fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
FileLock lock = fileChannel.tryLock();
if (lock == null) {
System.err.println("Config file is in use by another instance, waiting…");
logger.info("Config file is in use by another instance, waiting…");
lock = fileChannel.lock();
System.err.println("Config file lock acquired.");
logger.info("Config file lock acquired.");
}
return new Pair<>(fileChannel, lock);
}
@ -411,6 +471,10 @@ public class SignalAccount implements Closeable {
return stickerStore;
}
public MessageCache getMessageCache() {
return messageCache;
}
public String getUsername() {
return username;
}
@ -431,6 +495,10 @@ public class SignalAccount implements Closeable {
return deviceId;
}
public void setDeviceId(final int deviceId) {
this.deviceId = deviceId;
}
public String getPassword() {
return password;
}
@ -443,14 +511,18 @@ public class SignalAccount implements Closeable {
return registrationLockPin;
}
public String getRegistrationLock() {
return null; // TODO implement KBS
}
public void setRegistrationLockPin(final String registrationLockPin) {
this.registrationLockPin = registrationLockPin;
}
public MasterKey getPinMasterKey() {
return pinMasterKey;
}
public void setPinMasterKey(final MasterKey pinMasterKey) {
this.pinMasterKey = pinMasterKey;
}
public String getSignalingKey() {
return signalingKey;
}
@ -467,6 +539,10 @@ public class SignalAccount implements Closeable {
this.profileKey = profileKey;
}
public byte[] getSelfUnidentifiedAccessKey() {
return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
}
public int getPreKeyIdOffset() {
return preKeyIdOffset;
}
@ -491,8 +567,19 @@ public class SignalAccount implements Closeable {
isMultiDevice = multiDevice;
}
public boolean isUnrestrictedUnidentifiedAccess() {
// TODO make configurable
return false;
}
public boolean isDiscoverableByPhoneNumber() {
// TODO make configurable
return true;
}
@Override
public void close() throws IOException {
save();
synchronized (fileChannel) {
try {
lock.close();

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.contacts;
package org.asamk.signal.manager.storage.contacts;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.contacts;
package org.asamk.signal.manager.storage.contacts;
import com.fasterxml.jackson.annotation.JsonProperty;

View file

@ -1,8 +1,9 @@
package org.asamk.signal.storage.groups;
package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Set;
@ -11,16 +12,15 @@ import java.util.stream.Stream;
public abstract class GroupInfo {
@JsonProperty
public final byte[] groupId;
public GroupInfo(byte[] groupId) {
this.groupId = groupId;
}
@JsonIgnore
public abstract GroupId getGroupId();
@JsonIgnore
public abstract String getTitle();
@JsonIgnore
public abstract GroupInviteLinkUrl getGroupInviteLink();
@JsonIgnore
public abstract Set<SignalServiceAddress> getMembers();

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.groups;
package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -13,6 +13,11 @@ import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupUtils;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
@ -25,8 +30,9 @@ public class GroupInfoV1 extends GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty
public byte[] expectedV2Id;
private final GroupIdV1 groupId;
private GroupIdV2 expectedV2Id;
@JsonProperty
public String name;
@ -46,13 +52,8 @@ public class GroupInfoV1 extends GroupInfo {
@JsonProperty(defaultValue = "false")
public boolean archived;
public GroupInfoV1(byte[] groupId) {
super(groupId);
}
@Override
public String getTitle() {
return name;
public GroupInfoV1(GroupIdV1 groupId) {
this.groupId = groupId;
}
public GroupInfoV1(
@ -68,8 +69,8 @@ public class GroupInfoV1 extends GroupInfo {
@JsonProperty("messageExpirationTime") int messageExpirationTime,
@JsonProperty("active") boolean _ignored_active
) {
super(groupId);
this.expectedV2Id = expectedV2Id;
this.groupId = GroupId.v1(groupId);
this.expectedV2Id = GroupId.v2(expectedV2Id);
this.name = name;
this.members.addAll(members);
this.color = color;
@ -79,6 +80,40 @@ public class GroupInfoV1 extends GroupInfo {
this.messageExpirationTime = messageExpirationTime;
}
@Override
@JsonIgnore
public GroupIdV1 getGroupId() {
return groupId;
}
@JsonProperty("groupId")
private byte[] getGroupIdJackson() {
return groupId.serialize();
}
@JsonIgnore
public GroupIdV2 getExpectedV2Id() {
if (expectedV2Id == null) {
expectedV2Id = GroupUtils.getGroupIdV2(groupId);
}
return expectedV2Id;
}
@JsonProperty("expectedV2Id")
private byte[] getExpectedV2IdJackson() {
return getExpectedV2Id().serialize();
}
@Override
public String getTitle() {
return name;
}
@Override
public GroupInviteLinkUrl getGroupInviteLink() {
return null;
}
@JsonIgnore
public Set<SignalServiceAddress> getMembers() {
return members;

View file

@ -1,26 +1,34 @@
package org.asamk.signal.storage.groups;
package org.asamk.signal.manager.storage.groups;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
public class GroupInfoV2 extends GroupInfo {
private final GroupIdV2 groupId;
private final GroupMasterKey masterKey;
private boolean blocked;
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
public GroupInfoV2(final byte[] groupId, final GroupMasterKey masterKey) {
super(groupId);
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
this.groupId = groupId;
this.masterKey = masterKey;
}
@Override
public GroupIdV2 getGroupId() {
return groupId;
}
public GroupMasterKey getMasterKey() {
return masterKey;
}
@ -41,10 +49,23 @@ public class GroupInfoV2 extends GroupInfo {
return this.group.getTitle();
}
@Override
public GroupInviteLinkUrl getGroupInviteLink() {
if (this.group == null || this.group.getInviteLinkPassword() == null || (
this.group.getAccessControl().getAddFromInviteLink() != AccessControl.AccessRequired.ANY
&& this.group.getAccessControl().getAddFromInviteLink()
!= AccessControl.AccessRequired.ADMINISTRATOR
)) {
return null;
}
return GroupInviteLinkUrl.forGroup(masterKey, group);
}
@Override
public Set<SignalServiceAddress> getMembers() {
if (this.group == null) {
return Collections.emptySet();
return Set.of();
}
return group.getMembersList()
.stream()
@ -55,7 +76,7 @@ public class GroupInfoV2 extends GroupInfo {
@Override
public Set<SignalServiceAddress> getPendingMembers() {
if (this.group == null) {
return Collections.emptySet();
return Set.of();
}
return group.getPendingMembersList()
.stream()
@ -66,7 +87,7 @@ public class GroupInfoV2 extends GroupInfo {
@Override
public Set<SignalServiceAddress> getRequestingMembers() {
if (this.group == null) {
return Collections.emptySet();
return Set.of();
}
return group.getRequestingMembersList()
.stream()

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.groups;
package org.asamk.signal.manager.storage.groups;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
@ -12,12 +12,17 @@ import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.IOUtils;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.util.Base64;
import java.io.File;
@ -25,7 +30,6 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@ -33,13 +37,15 @@ import java.util.Map;
public class JsonGroupStore {
final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
private static final ObjectMapper jsonProcessor = new ObjectMapper();
public File groupCachePath;
@JsonProperty("groups")
@JsonSerialize(using = GroupsSerializer.class)
@JsonDeserialize(using = GroupsDeserializer.class)
private final Map<String, GroupInfo> groups = new HashMap<>();
private final Map<GroupId, GroupInfo> groups = new HashMap<>();
private JsonGroupStore() {
}
@ -49,70 +55,78 @@ public class JsonGroupStore {
}
public void updateGroup(GroupInfo group) {
groups.put(Base64.encodeBytes(group.groupId), group);
groups.put(group.getGroupId(), group);
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
try {
IOUtils.createPrivateDirectories(groupCachePath);
try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.groupId))) {
try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.getGroupId()))) {
((GroupInfoV2) group).getGroup().writeTo(stream);
}
final File groupFileLegacy = getGroupFileLegacy(group.getGroupId());
if (groupFileLegacy.exists()) {
groupFileLegacy.delete();
}
} catch (IOException e) {
System.err.println("Failed to cache group, ignoring ...");
logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
}
}
}
public void deleteGroup(byte[] groupId) {
groups.remove(Base64.encodeBytes(groupId));
public void deleteGroup(GroupId groupId) {
groups.remove(groupId);
}
public GroupInfo getGroup(byte[] groupId) {
final GroupInfo group = groups.get(Base64.encodeBytes(groupId));
if (group == null & groupId.length == 16) {
return getGroupByV1Id(groupId);
public GroupInfo getGroup(GroupId groupId) {
GroupInfo group = groups.get(groupId);
if (group == null) {
if (groupId instanceof GroupIdV1) {
group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId));
} else if (groupId instanceof GroupIdV2) {
group = getGroupV1ByV2Id((GroupIdV2) groupId);
}
}
loadDecryptedGroup(group);
return group;
}
public GroupInfo getGroupByV1Id(byte[] groupIdV1) {
GroupInfo group = groups.get(Base64.encodeBytes(groupIdV1));
if (group == null) {
group = groups.get(Base64.encodeBytes(GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(groupIdV1))));
}
loadDecryptedGroup(group);
return group;
}
public GroupInfo getGroupByV2Id(byte[] groupIdV2) {
GroupInfo group = groups.get(Base64.encodeBytes(groupIdV2));
if (group == null) {
for (GroupInfo g : groups.values()) {
if (g instanceof GroupInfoV1 && Arrays.equals(groupIdV2, ((GroupInfoV1) g).expectedV2Id)) {
group = g;
break;
private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) {
for (GroupInfo g : groups.values()) {
if (g instanceof GroupInfoV1) {
final GroupInfoV1 gv1 = (GroupInfoV1) g;
if (groupIdV2.equals(gv1.getExpectedV2Id())) {
return gv1;
}
}
}
loadDecryptedGroup(group);
return group;
return null;
}
private void loadDecryptedGroup(final GroupInfo group) {
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
try (FileInputStream stream = new FileInputStream(getGroupFile(group.groupId))) {
File groupFile = getGroupFile(group.getGroupId());
if (!groupFile.exists()) {
groupFile = getGroupFileLegacy(group.getGroupId());
}
if (!groupFile.exists()) {
return;
}
try (FileInputStream stream = new FileInputStream(groupFile)) {
((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream));
} catch (IOException ignored) {
}
}
}
private File getGroupFile(final byte[] groupId) {
return new File(groupCachePath, Hex.toStringCondensed(groupId));
private File getGroupFileLegacy(final GroupId groupId) {
return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
}
public GroupInfoV1 getOrCreateGroupV1(byte[] groupId) {
GroupInfo group = groups.get(Base64.encodeBytes(groupId));
private File getGroupFile(final GroupId groupId) {
return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
}
public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
GroupInfo group = getGroup(groupId);
if (group instanceof GroupInfoV1) {
return (GroupInfoV1) group;
}
@ -146,7 +160,7 @@ public class JsonGroupStore {
} else if (group instanceof GroupInfoV2) {
final GroupInfoV2 groupV2 = (GroupInfoV2) group;
jgen.writeStartObject();
jgen.writeStringField("groupId", Base64.encodeBytes(groupV2.groupId));
jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
jgen.writeStringField("masterKey", Base64.encodeBytes(groupV2.getMasterKey().serialize()));
jgen.writeBooleanField("blocked", groupV2.isBlocked());
jgen.writeEndObject();
@ -158,34 +172,31 @@ public class JsonGroupStore {
}
}
private static class GroupsDeserializer extends JsonDeserializer<Map<String, GroupInfo>> {
private static class GroupsDeserializer extends JsonDeserializer<Map<GroupId, GroupInfo>> {
@Override
public Map<String, GroupInfo> deserialize(
public Map<GroupId, GroupInfo> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
Map<String, GroupInfo> groups = new HashMap<>();
Map<GroupId, GroupInfo> groups = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
GroupInfo g;
if (n.has("masterKey")) {
// a v2 group
byte[] groupId = Base64.decode(n.get("groupId").asText());
GroupIdV2 groupId = GroupIdV2.fromBase64(n.get("groupId").asText());
try {
GroupMasterKey masterKey = new GroupMasterKey(Base64.decode(n.get("masterKey").asText()));
g = new GroupInfoV2(groupId, masterKey);
} catch (InvalidInputException e) {
throw new AssertionError("Invalid master key for group " + Base64.encodeBytes(groupId));
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
}
g.setBlocked(n.get("blocked").asBoolean(false));
} else {
GroupInfoV1 gv1 = jsonProcessor.treeToValue(n, GroupInfoV1.class);
if (gv1.expectedV2Id == null) {
gv1.expectedV2Id = GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(gv1.groupId));
}
g = gv1;
}
groups.put(Base64.encodeBytes(g.groupId), g);
groups.put(g.getGroupId(), g);
}
return groups;

View file

@ -0,0 +1,38 @@
package org.asamk.signal.manager.storage.messageCache;
import org.asamk.signal.manager.util.MessageCacheUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
public final class CachedMessage {
final static Logger logger = LoggerFactory.getLogger(CachedMessage.class);
private final File file;
CachedMessage(final File file) {
this.file = file;
}
public SignalServiceEnvelope loadEnvelope() {
try {
return MessageCacheUtils.loadEnvelope(file);
} catch (IOException e) {
logger.error("Failed to load cached message envelope “{}”: {}", file, e.getMessage());
return null;
}
}
public void delete() {
try {
Files.delete(file.toPath());
} catch (IOException e) {
logger.warn("Failed to delete cached message file “{}”, ignoring: {}", file, e.getMessage());
}
}
}

View file

@ -0,0 +1,79 @@
package org.asamk.signal.manager.storage.messageCache;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.MessageCacheUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class MessageCache {
final static Logger logger = LoggerFactory.getLogger(MessageCache.class);
private final File messageCachePath;
public MessageCache(final File messageCachePath) {
this.messageCachePath = messageCachePath;
}
public Iterable<CachedMessage> getCachedMessages() {
if (!messageCachePath.exists()) {
return Collections.emptyList();
}
return Arrays.stream(Objects.requireNonNull(messageCachePath.listFiles())).flatMap(dir -> {
if (dir.isFile()) {
return Stream.of(dir);
}
final File[] files = Objects.requireNonNull(dir.listFiles());
if (files.length == 0) {
try {
Files.delete(dir.toPath());
} catch (IOException e) {
logger.warn("Failed to delete cache dir “{}”, ignoring: {}", dir, e.getMessage());
}
return Stream.empty();
}
return Arrays.stream(files).filter(File::isFile);
}).map(CachedMessage::new).collect(Collectors.toList());
}
public CachedMessage cacheMessage(SignalServiceEnvelope envelope) {
final long now = new Date().getTime();
final String source = envelope.hasSource() ? envelope.getSourceAddress().getLegacyIdentifier() : "";
try {
File cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp());
MessageCacheUtils.storeEnvelope(envelope, cacheFile);
return new CachedMessage(cacheFile);
} catch (IOException e) {
logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage());
return null;
}
}
private File getMessageCachePath(String sender) {
if (sender == null || sender.isEmpty()) {
return messageCachePath;
}
return new File(messageCachePath, sender.replace("/", "_"));
}
private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException {
File cachePath = getMessageCachePath(sender);
IOUtils.createPrivateDirectories(cachePath);
return new File(cachePath, now + "_" + timestamp);
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.profiles;
package org.asamk.signal.manager.storage.profiles;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
@ -112,7 +112,7 @@ public class ProfileStore {
try {
profileKeyCredential = new ProfileKeyCredential(Base64.decode(entry.get(
"profileKeyCredential").asText()));
} catch (InvalidInputException ignored) {
} catch (Throwable ignored) {
}
}
long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.profiles;
package org.asamk.signal.manager.storage.profiles;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.profiles;
package org.asamk.signal.manager.storage.profiles;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;

View file

@ -0,0 +1,57 @@
package org.asamk.signal.manager.storage.protocol;
import org.asamk.signal.manager.TrustLevel;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Date;
public class IdentityInfo {
SignalServiceAddress address;
IdentityKey identityKey;
TrustLevel trustLevel;
Date added;
public IdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) {
this.address = address;
this.identityKey = identityKey;
this.trustLevel = trustLevel;
this.added = new Date();
}
IdentityInfo(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;
}
public IdentityKey getIdentityKey() {
return this.identityKey;
}
public TrustLevel getTrustLevel() {
return this.trustLevel;
}
public Date getDateAdded() {
return this.added;
}
public byte[] getFingerprint() {
return identityKey.getPublicKey().serialize();
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.protocol;
package org.asamk.signal.manager.storage.protocol;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
@ -9,7 +9,9 @@ import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.util.Util;
import org.asamk.signal.manager.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@ -27,7 +29,9 @@ import java.util.UUID;
public class JsonIdentityKeyStore implements IdentityKeyStore {
private final List<Identity> identities = new ArrayList<>();
final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class);
private final List<IdentityInfo> identities = new ArrayList<>();
private final IdentityKeyPair identityKeyPair;
private final int localRegistrationId;
@ -47,7 +51,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
if (resolver != null) {
return resolver.resolveSignalServiceAddress(identifier);
} else {
return Util.getSignalServiceAddressFromIdentifier(identifier);
return Utils.getSignalServiceAddressFromIdentifier(identifier);
}
}
@ -81,7 +85,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
public boolean saveIdentity(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added
) {
for (Identity id : identities) {
for (IdentityInfo id : identities) {
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue;
}
@ -93,7 +97,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
return true;
}
identities.add(new Identity(serviceAddress, identityKey, trustLevel, added != null ? added : new Date()));
identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, added != null ? added : new Date()));
return false;
}
@ -107,7 +111,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
public void setIdentityTrustLevel(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel
) {
for (Identity id : identities) {
for (IdentityInfo id : identities) {
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue;
}
@ -119,7 +123,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
return;
}
identities.add(new Identity(serviceAddress, identityKey, trustLevel, new Date()));
identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, new Date()));
}
@Override
@ -128,7 +132,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
boolean trustOnFirstUse = true;
for (Identity id : identities) {
for (IdentityInfo id : identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
@ -146,14 +150,14 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
Identity identity = getIdentity(serviceAddress);
IdentityInfo identity = getIdentity(serviceAddress);
return identity == null ? null : identity.getIdentityKey();
}
public Identity getIdentity(SignalServiceAddress serviceAddress) {
public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
long maxDate = 0;
Identity maxIdentity = null;
for (Identity id : this.identities) {
IdentityInfo maxIdentity = null;
for (IdentityInfo id : this.identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
@ -167,14 +171,14 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
return maxIdentity;
}
public List<Identity> getIdentities() {
public List<IdentityInfo> getIdentities() {
// TODO deep copy
return identities;
}
public List<Identity> getIdentities(SignalServiceAddress serviceAddress) {
List<Identity> identities = new ArrayList<>();
for (Identity identity : this.identities) {
public List<IdentityInfo> getIdentities(SignalServiceAddress serviceAddress) {
List<IdentityInfo> identities = new ArrayList<>();
for (IdentityInfo identity : this.identities) {
if (identity.address.matches(serviceAddress)) {
identities.add(identity);
}
@ -209,7 +213,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
UUID uuid = trustedKey.hasNonNull("uuid") ? UuidUtil.parseOrNull(trustedKey.get("uuid")
.asText()) : null;
final SignalServiceAddress serviceAddress = uuid == null
? Util.getSignalServiceAddressFromIdentifier(trustedKeyName)
? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName)
: new SignalServiceAddress(uuid, trustedKeyName);
try {
IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0);
@ -219,7 +223,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
.asLong()) : new Date();
keyStore.saveIdentity(serviceAddress, id, trustLevel, added);
} catch (InvalidKeyException | IOException e) {
System.out.println(String.format("Error while decoding key for: %s", trustedKeyName));
logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
}
}
}
@ -242,7 +246,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
json.writeStringField("identityKey",
Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
json.writeArrayFieldStart("trustedKeys");
for (Identity trustedKey : jsonIdentityKeyStore.identities) {
for (IdentityInfo trustedKey : jsonIdentityKeyStore.identities) {
json.writeStartObject();
if (trustedKey.getAddress().getNumber().isPresent()) {
json.writeStringField("name", trustedKey.getAddress().getNumber().get());
@ -260,53 +264,4 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
}
}
public static class Identity {
SignalServiceAddress address;
IdentityKey identityKey;
TrustLevel trustLevel;
Date added;
public Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) {
this.address = address;
this.identityKey = identityKey;
this.trustLevel = trustLevel;
this.added = new Date();
}
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;
}
public IdentityKey getIdentityKey() {
return this.identityKey;
}
public TrustLevel getTrustLevel() {
return this.trustLevel;
}
public Date getDateAdded() {
return this.added;
}
public byte[] getFingerprint() {
return identityKey.getPublicKey().serialize();
}
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.protocol;
package org.asamk.signal.manager.storage.protocol;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
@ -19,6 +21,8 @@ import java.util.Map;
class JsonPreKeyStore implements PreKeyStore {
final static Logger logger = LoggerFactory.getLogger(JsonPreKeyStore.class);
private final Map<Integer, byte[]> store = new HashMap<>();
public JsonPreKeyStore() {
@ -72,7 +76,7 @@ class JsonPreKeyStore implements PreKeyStore {
try {
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
} catch (IOException e) {
System.err.println(String.format("Error while decoding prekey for: %s", preKeyId));
logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage());
}
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.protocol;
package org.asamk.signal.manager.storage.protocol;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
@ -8,7 +8,9 @@ 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.asamk.signal.manager.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
@ -24,6 +26,8 @@ import java.util.UUID;
class JsonSessionStore implements SessionStore {
final static Logger logger = LoggerFactory.getLogger(JsonSessionStore.class);
private final List<SessionInfo> sessions = new ArrayList<>();
private SignalServiceAddressResolver resolver;
@ -39,7 +43,7 @@ class JsonSessionStore implements SessionStore {
if (resolver != null) {
return resolver.resolveSignalServiceAddress(identifier);
} else {
return Util.getSignalServiceAddressFromIdentifier(identifier);
return Utils.getSignalServiceAddressFromIdentifier(identifier);
}
}
@ -51,7 +55,7 @@ class JsonSessionStore implements SessionStore {
try {
return new SessionRecord(info.sessionRecord);
} catch (IOException e) {
System.err.println("Failed to load session, resetting session: " + e);
logger.warn("Failed to load session, resetting session: {}", e.getMessage());
final SessionRecord sessionRecord = new SessionRecord();
info.sessionRecord = sessionRecord.serialize();
return sessionRecord;
@ -143,7 +147,7 @@ class JsonSessionStore implements SessionStore {
UUID uuid = session.hasNonNull("uuid") ? UuidUtil.parseOrNull(session.get("uuid").asText()) : null;
final SignalServiceAddress serviceAddress = uuid == null
? Util.getSignalServiceAddressFromIdentifier(sessionName)
? Utils.getSignalServiceAddressFromIdentifier(sessionName)
: new SignalServiceAddress(uuid, sessionName);
final int deviceId = session.get("deviceId").asInt();
final String record = session.get("record").asText();
@ -151,7 +155,7 @@ class JsonSessionStore implements SessionStore {
SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record));
sessionStore.sessions.add(sessionInfo);
} catch (IOException e) {
System.err.println(String.format("Error while decoding session for: %s", sessionName));
logger.warn("Error while decoding session for {}: {}", sessionName, e.getMessage());
}
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.protocol;
package org.asamk.signal.manager.storage.protocol;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@ -91,11 +91,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel);
}
public List<JsonIdentityKeyStore.Identity> getIdentities() {
public List<IdentityInfo> getIdentities() {
return identityKeyStore.getIdentities();
}
public List<JsonIdentityKeyStore.Identity> getIdentities(SignalServiceAddress serviceAddress) {
public List<IdentityInfo> getIdentities(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentities(serviceAddress);
}
@ -109,7 +109,7 @@ public class JsonSignalProtocolStore implements SignalProtocolStore {
return identityKeyStore.getIdentity(address);
}
public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) {
public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentity(serviceAddress);
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.protocol;
package org.asamk.signal.manager.storage.protocol;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
@ -21,6 +23,8 @@ import java.util.Map;
class JsonSignedPreKeyStore implements SignedPreKeyStore {
final static Logger logger = LoggerFactory.getLogger(JsonSignedPreKeyStore.class);
private final Map<Integer, byte[]> store = new HashMap<>();
public JsonSignedPreKeyStore() {
@ -89,7 +93,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore {
try {
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
} catch (IOException e) {
System.err.println(String.format("Error while decoding prekey for: %s", preKeyId));
logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage());
}
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.protocol;
package org.asamk.signal.manager.storage.protocol;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.protocol;
package org.asamk.signal.manager.storage.protocol;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.protocol;
package org.asamk.signal.manager.storage.protocol;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.stickers;
package org.asamk.signal.manager.storage.stickers;
public class Sticker {

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.stickers;
package org.asamk.signal.manager.storage.stickers;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.threads;
package org.asamk.signal.manager.storage.threads;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;

View file

@ -1,4 +1,4 @@
package org.asamk.signal.storage.threads;
package org.asamk.signal.manager.storage.threads;
import com.fasterxml.jackson.annotation.JsonProperty;

View file

@ -0,0 +1,79 @@
package org.asamk.signal.manager.util;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class AttachmentUtils {
public static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
List<SignalServiceAttachment> signalServiceAttachments = null;
if (attachments != null) {
signalServiceAttachments = new ArrayList<>(attachments.size());
for (String attachment : attachments) {
try {
signalServiceAttachments.add(createAttachment(new File(attachment)));
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
}
}
}
return signalServiceAttachments;
}
public static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
InputStream attachmentStream = new FileInputStream(attachmentFile);
final long attachmentSize = attachmentFile.length();
final String mime = Utils.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();
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);
}
public static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException {
InputStream input = stream.getInputStream();
try (OutputStream output = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[4096];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
return outputFile;
}
}

View file

@ -0,0 +1,72 @@
package org.asamk.signal.manager.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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.EnumSet;
import java.util.Set;
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
public class IOUtils {
public static File createTempFile() throws IOException {
return File.createTempFile("signal_tmp_", ".tmp");
}
public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.copy(in, baos);
return baos.toByteArray();
}
public static void createPrivateDirectories(File file) throws IOException {
if (file.exists()) {
return;
}
final Path path = file.toPath();
try {
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE);
Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms));
} catch (UnsupportedOperationException e) {
Files.createDirectories(path);
}
}
public static void createPrivateFile(File path) throws IOException {
final Path file = path.toPath();
try {
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE);
Files.createFile(file, PosixFilePermissions.asFileAttribute(perms));
} catch (UnsupportedOperationException e) {
Files.createFile(file);
}
}
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);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more