hopefully fixed some stuff

This commit is contained in:
technillogue 2021-04-25 19:05:59 -04:00
parent f1b1bafea8
commit b5fd2f920d
66 changed files with 57 additions and 7705 deletions

View file

@ -1,76 +0,0 @@
apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'eclipse'
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
mainClassName = 'org.asamk.signal.Main'
version = '0.7.1'
compileJava.options.encoding = 'UTF-8'
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_15'
implementation 'org.bouncycastle:bcprov-jdk15on:1.67'
implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1'
implementation 'com.github.hypfvieh:dbus-java:3.2.4'
implementation 'org.slf4j:slf4j-simple:1.7.30'
}
jar {
manifest {
attributes(
'Implementation-Title': project.name,
'Implementation-Version': project.version,
'Main-Class': project.mainClassName,
)
}
}
run {
if (project.hasProperty("appArgs")) {
// allow passing command-line arguments to the main application e.g.:
// $ gradle run -PappArgs="['-u', '+...', 'daemon', '--json']"
args Eval.me(appArgs)
}
}
// Find any 3rd party libraries which have released new versions
// to the central Maven repo since we last upgraded.
task checkLibVersions {
doLast {
def checked = [:]
allprojects {
configurations.each { configuration ->
configuration.allDependencies.each { dependency ->
def version = dependency.version
if (!checked[dependency]) {
def group = dependency.group
def path = group.replace('.', '/')
def name = dependency.name
def url = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
try {
def metadata = new XmlSlurper().parseText(url.toURL().text)
def newest = metadata.versioning.latest;
if ("$version" != "$newest") {
println "UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}"
}
} catch (FileNotFoundException e) {
logger.debug "Unable to download $url: $e.message"
} catch (org.xml.sax.SAXParseException e) {
logger.debug "Unable to parse $url: $e.message"
}
checked[dependency] = true
}
}
}
}
}
}

View file

@ -1,16 +0,0 @@
[Unit]
Description=Send secure messages to Signal clients
Requires=dbus.socket
After=dbus.socket
Wants=network-online.target
After=network-online.target
[Service]
Type=dbus
Environment="SIGNAL_CLI_OPTS=-Xms2m"
ExecStart=%dir%/bin/signal-cli -u %number% --config /var/lib/signal-cli daemon --system
User=signal-cli
BusName=org.asamk.Signal
[Install]
Alias=dbus-org.asamk.Signal.service

View file

@ -161,10 +161,6 @@ import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.asamk.signal.manager.ServiceConfig.CDS_MRENCLAVE;
import static org.asamk.signal.manager.ServiceConfig.capabilities;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.manager.ServiceConfig.getIasKeyStore;
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
public class Manager implements Closeable {

View file

@ -1,18 +0,0 @@
/*
* This settings file was auto generated by the Gradle buildInit task
*
* The settings file is used to specify which projects to include in your build.
* In a single project build this file can be empty or even removed.
*
* Detailed information about configuring a multi-project build in Gradle can be found
* in the user guide at http://gradle.org/docs/2.2.1/userguide/multi_project_builds.html
*/
/*
// To declare projects as part of a multi-project build use the 'include' method
include 'shared'
include 'api'
include 'services:webservice'
*/
rootProject.name = 'signal-cli'

View file

@ -13,6 +13,7 @@ public class Commands {
addCommand("daemon", new DaemonCommand());
addCommand("stdio", new StdioCommand());
addCommand("getUserStatus", new GetUserStatusCommand());
addCommand("getUserStatus", new GetUserStatusCommand());
addCommand("link", new LinkCommand());
addCommand("listContacts", new ListContactsCommand());
addCommand("listDevices", new ListDevicesCommand());

View file

@ -9,6 +9,7 @@ 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;
@ -51,6 +52,30 @@ class JsonDataMessage {
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonQuote quote;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonMention> mentions;
@JsonProperty
final long timestamp;
@JsonProperty
final String message;
@JsonProperty
final Integer expiresInSeconds;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final Boolean viewOnce;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonReaction reaction;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonQuote quote;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonMention> mentions;
@ -71,6 +96,25 @@ class JsonDataMessage {
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonSharedContact> contacts;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonAttachment> attachments;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonSticker sticker;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonRemoteDelete remoteDelete;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonSharedContact> contacts;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonGroupInfo groupInfo;
JsonReaction reaction;
JsonQuote quote;
List<JsonMention> mentions;
@ -157,8 +201,19 @@ class JsonDataMessage {
}
if (message.isExpirationUpdate()) {
System.out.println("Is Expiration update: " + message.isExpirationUpdate());
}
}
}*/
this.sticker = dataMessage.getSticker().isPresent() ? new JsonSticker(dataMessage.getSticker().get()) : null;
if (dataMessage.getSharedContacts().isPresent()) {
this.contacts = dataMessage.getSharedContacts()
.get()
.stream()
.map(JsonSharedContact::new)
.collect(Collectors.toList());
} else {
this.contacts = List.of();
}
}
public JsonDataMessage(Signal.MessageReceived messageReceived) {

View file

@ -1,12 +0,0 @@
package org.asamk.signal.manager;
public class AttachmentInvalidException extends Exception {
public AttachmentInvalidException(String message) {
super(message);
}
public AttachmentInvalidException(String attachment, Exception e) {
super(attachment + ": " + e.getMessage());
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,158 +0,0 @@
package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Objects;
interface HandleAction {
void execute(Manager m) throws Throwable;
}
class SendReceiptAction implements HandleAction {
private final SignalServiceAddress address;
private final long timestamp;
public SendReceiptAction(final SignalServiceAddress address, final long timestamp) {
this.address = address;
this.timestamp = timestamp;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendReceipt(address, timestamp);
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendReceiptAction that = (SendReceiptAction) o;
return timestamp == that.timestamp && address.equals(that.address);
}
@Override
public int hashCode() {
return Objects.hash(address, timestamp);
}
}
class SendSyncContactsAction implements HandleAction {
private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction();
private SendSyncContactsAction() {
}
public static SendSyncContactsAction create() {
return INSTANCE;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendContacts();
}
}
class SendSyncGroupsAction implements HandleAction {
private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction();
private SendSyncGroupsAction() {
}
public static SendSyncGroupsAction create() {
return INSTANCE;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendGroups();
}
}
class SendSyncBlockedListAction implements HandleAction {
private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction();
private SendSyncBlockedListAction() {
}
public static SendSyncBlockedListAction create() {
return INSTANCE;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendBlockedList();
}
}
class SendGroupInfoRequestAction implements HandleAction {
private final SignalServiceAddress address;
private final GroupIdV1 groupId;
public SendGroupInfoRequestAction(final SignalServiceAddress address, final GroupIdV1 groupId) {
this.address = address;
this.groupId = groupId;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendGroupInfoRequest(groupId, address);
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o;
if (!address.equals(that.address)) return false;
return groupId.equals(that.groupId);
}
@Override
public int hashCode() {
int result = address.hashCode();
result = 31 * result + groupId.hashCode();
return result;
}
}
class SendGroupUpdateAction implements HandleAction {
private final SignalServiceAddress address;
private final GroupIdV1 groupId;
public SendGroupUpdateAction(final SignalServiceAddress address, final GroupIdV1 groupId) {
this.address = address;
this.groupId = groupId;
}
@Override
public void execute(Manager m) throws Throwable {
m.sendUpdateGroupMessage(groupId, address);
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendGroupUpdateAction that = (SendGroupUpdateAction) o;
if (!address.equals(that.address)) return false;
return groupId.equals(that.groupId);
}
@Override
public int hashCode() {
int result = address.hashCode();
result = 31 * result + groupId.hashCode();
return result;
}
}

View file

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

View file

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

View file

@ -1,43 +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[] createStickerUploadKey() {
return getSecretBytes(32);
}
private static String getSecret(int size) {
byte[] secret = getSecretBytes(size);
return Base64.encodeBytes(secret);
}
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,8 +0,0 @@
package org.asamk.signal.manager;
public class NotAGroupMemberException extends Exception {
public NotAGroupMemberException(GroupId groupId, String groupName) {
super("User is not a member in group: " + groupName + " (" + groupId.toBase64() + ")");
}
}

View file

@ -1,34 +0,0 @@
package org.asamk.signal.manager;
import java.io.File;
public class PathConfig {
private final File dataPath;
private final File attachmentsPath;
private final File avatarsPath;
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 File dataPath, final File attachmentsPath, final File avatarsPath) {
this.dataPath = dataPath;
this.attachmentsPath = attachmentsPath;
this.avatarsPath = avatarsPath;
}
public File getDataPath() {
return dataPath;
}
public File getAttachmentsPath() {
return attachmentsPath;
}
public File getAvatarsPath() {
return avatarsPath;
}
}

View file

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

View file

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

View file

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

View file

@ -1,42 +0,0 @@
package org.asamk.signal.manager;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
public enum TrustLevel {
UNTRUSTED,
TRUSTED_UNVERIFIED,
TRUSTED_VERIFIED;
private static TrustLevel[] cachedValues = null;
public static TrustLevel fromInt(int i) {
if (TrustLevel.cachedValues == null) {
TrustLevel.cachedValues = TrustLevel.values();
}
return TrustLevel.cachedValues[i];
}
public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) {
switch (verifiedState) {
case DEFAULT:
return TRUSTED_UNVERIFIED;
case UNVERIFIED:
return UNTRUSTED;
case VERIFIED:
return TRUSTED_VERIFIED;
}
throw new RuntimeException("Unknown verified state: " + verifiedState);
}
public VerifiedMessage.VerifiedState toVerifiedState() {
switch (this) {
case TRUSTED_UNVERIFIED:
return VerifiedMessage.VerifiedState.DEFAULT;
case UNTRUSTED:
return VerifiedMessage.VerifiedState.UNVERIFIED;
case TRUSTED_VERIFIED:
return VerifiedMessage.VerifiedState.VERIFIED;
}
throw new RuntimeException("Unknown verified state: " + this);
}
}

View file

@ -1,22 +0,0 @@
package org.asamk.signal.manager;
import java.io.File;
public class UserAlreadyExists extends Exception {
private final String username;
private final File fileName;
public UserAlreadyExists(String username, File fileName) {
this.username = username;
this.fileName = fileName;
}
public String getUsername() {
return username;
}
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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,517 +0,0 @@
package org.asamk.signal.storage;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.storage.contacts.ContactInfo;
import org.asamk.signal.storage.contacts.JsonContactsStore;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.storage.groups.GroupInfoV1;
import org.asamk.signal.storage.groups.JsonGroupStore;
import org.asamk.signal.storage.profiles.ProfileStore;
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
import org.asamk.signal.storage.protocol.JsonSignalProtocolStore;
import org.asamk.signal.storage.protocol.RecipientStore;
import org.asamk.signal.storage.protocol.SessionInfo;
import org.asamk.signal.storage.protocol.SignalServiceAddressResolver;
import org.asamk.signal.storage.stickers.StickerStore;
import org.asamk.signal.storage.threads.LegacyJsonThreadStore;
import org.asamk.signal.storage.threads.ThreadInfo;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Collection;
import java.util.UUID;
import java.util.stream.Collectors;
public class SignalAccount implements Closeable {
final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
private final ObjectMapper jsonProcessor = new ObjectMapper();
private final FileChannel fileChannel;
private final FileLock lock;
private String username;
private UUID uuid;
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
private boolean isMultiDevice = false;
private String password;
private String registrationLockPin;
private String signalingKey;
private ProfileKey profileKey;
private int preKeyIdOffset;
private int nextSignedPreKeyId;
private boolean registered = false;
private JsonSignalProtocolStore signalProtocolStore;
private JsonGroupStore groupStore;
private JsonContactsStore contactStore;
private RecipientStore recipientStore;
private ProfileStore profileStore;
private StickerStore stickerStore;
private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
this.fileChannel = fileChannel;
this.lock = lock;
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}
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);
return account;
} catch (Throwable e) {
pair.second().close();
pair.first().close();
throw e;
}
}
public static SignalAccount create(
File dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey
) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
File fileName = getFileName(dataPath, username);
if (!fileName.exists()) {
IOUtils.createPrivateFile(fileName);
}
final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
SignalAccount account = new SignalAccount(pair.first(), pair.second());
account.username = username;
account.profileKey = profileKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.contactStore = new JsonContactsStore();
account.recipientStore = new RecipientStore();
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
account.registered = false;
return account;
}
public static SignalAccount createLinkedAccount(
File dataPath,
String username,
UUID uuid,
String password,
int deviceId,
IdentityKeyPair identityKey,
int registrationId,
String signalingKey,
ProfileKey profileKey
) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
File fileName = getFileName(dataPath, username);
if (!fileName.exists()) {
IOUtils.createPrivateFile(fileName);
}
final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
SignalAccount account = new SignalAccount(pair.first(), pair.second());
account.username = username;
account.uuid = uuid;
account.password = password;
account.profileKey = profileKey;
account.deviceId = deviceId;
account.signalingKey = signalingKey;
account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.contactStore = new JsonContactsStore();
account.recipientStore = new RecipientStore();
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
account.registered = true;
account.isMultiDevice = true;
return account;
}
public static File getFileName(File dataPath, String username) {
return new File(dataPath, 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 = getFileName(dataPath, username);
return !(!f.exists() || f.isDirectory());
}
private void load(File dataPath) throws IOException {
JsonNode rootNode;
synchronized (fileChannel) {
fileChannel.position(0);
rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
}
JsonNode uuidNode = rootNode.get("uuid");
if (uuidNode != null && !uuidNode.isNull()) {
try {
uuid = UUID.fromString(uuidNode.asText());
} catch (IllegalArgumentException e) {
throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e);
}
}
JsonNode node = rootNode.get("deviceId");
if (node != null) {
deviceId = node.asInt();
}
if (rootNode.has("isMultiDevice")) {
isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
}
username = Util.getNotNullNode(rootNode, "username").asText();
password = Util.getNotNullNode(rootNode, "password").asText();
JsonNode pinNode = rootNode.get("registrationLockPin");
registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText();
if (rootNode.has("signalingKey")) {
signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText();
}
if (rootNode.has("preKeyIdOffset")) {
preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
} else {
preKeyIdOffset = 0;
}
if (rootNode.has("nextSignedPreKeyId")) {
nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
} else {
nextSignedPreKeyId = 0;
}
if (rootNode.has("profileKey")) {
try {
profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()));
} catch (InvalidInputException e) {
throw new IOException(
"Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
e);
}
}
signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"),
JsonSignalProtocolStore.class);
registered = Util.getNotNullNode(rootNode, "registered").asBoolean();
JsonNode groupStoreNode = rootNode.get("groupStore");
if (groupStoreNode != null) {
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
groupStore.groupCachePath = getGroupCachePath(dataPath, username);
}
if (groupStore == null) {
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
}
JsonNode contactStoreNode = rootNode.get("contactStore");
if (contactStoreNode != null) {
contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
}
if (contactStore == null) {
contactStore = new JsonContactsStore();
}
JsonNode recipientStoreNode = rootNode.get("recipientStore");
if (recipientStoreNode != null) {
recipientStore = jsonProcessor.convertValue(recipientStoreNode, RecipientStore.class);
}
if (recipientStore == null) {
recipientStore = new RecipientStore();
recipientStore.resolveServiceAddress(getSelfAddress());
for (ContactInfo contact : contactStore.getContacts()) {
recipientStore.resolveServiceAddress(contact.getAddress());
}
for (GroupInfo group : groupStore.getGroups()) {
if (group instanceof GroupInfoV1) {
GroupInfoV1 groupInfoV1 = (GroupInfoV1) group;
groupInfoV1.members = groupInfoV1.members.stream()
.map(m -> recipientStore.resolveServiceAddress(m))
.collect(Collectors.toSet());
}
}
for (SessionInfo session : signalProtocolStore.getSessions()) {
session.address = recipientStore.resolveServiceAddress(session.address);
}
for (JsonIdentityKeyStore.Identity identity : signalProtocolStore.getIdentities()) {
identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
}
}
JsonNode profileStoreNode = rootNode.get("profileStore");
if (profileStoreNode != null) {
profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
}
if (profileStore == null) {
profileStore = new ProfileStore();
}
JsonNode stickerStoreNode = rootNode.get("stickerStore");
if (stickerStoreNode != null) {
stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class);
}
if (stickerStore == null) {
stickerStore = new StickerStore();
}
JsonNode threadStoreNode = rootNode.get("threadStore");
if (threadStoreNode != null) {
LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode,
LegacyJsonThreadStore.class);
// Migrate thread info to group and contact store
for (ThreadInfo thread : threadStore.getThreads()) {
if (thread.id == null || thread.id.isEmpty()) {
continue;
}
try {
ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
if (contactInfo != null) {
contactInfo.messageExpirationTime = thread.messageExpirationTime;
contactStore.updateContact(contactInfo);
} else {
GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
if (groupInfo instanceof GroupInfoV1) {
((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
groupStore.updateGroup(groupInfo);
}
}
} catch (Exception ignored) {
}
}
}
}
public void save() {
if (fileChannel == null) {
return;
}
ObjectNode rootNode = jsonProcessor.createObjectNode();
rootNode.put("username", username)
.put("uuid", uuid == null ? null : uuid.toString())
.put("deviceId", deviceId)
.put("isMultiDevice", isMultiDevice)
.put("password", password)
.put("registrationLockPin", registrationLockPin)
.put("signalingKey", signalingKey)
.put("preKeyIdOffset", preKeyIdOffset)
.put("nextSignedPreKeyId", nextSignedPreKeyId)
.put("profileKey", Base64.encodeBytes(profileKey.serialize()))
.put("registered", registered)
.putPOJO("axolotlStore", signalProtocolStore)
.putPOJO("groupStore", groupStore)
.putPOJO("contactStore", contactStore)
.putPOJO("recipientStore", recipientStore)
.putPOJO("profileStore", profileStore)
.putPOJO("stickerStore", stickerStore);
try {
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
// Write to memory first to prevent corrupting the file in case of serialization errors
jsonProcessor.writeValue(output, rootNode);
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
synchronized (fileChannel) {
fileChannel.position(0);
input.transferTo(Channels.newOutputStream(fileChannel));
fileChannel.truncate(fileChannel.position());
fileChannel.force(false);
}
}
} catch (Exception e) {
logger.error("Error saving file: {}", e.getMessage());
}
}
private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
FileChannel fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
FileLock lock = fileChannel.tryLock();
if (lock == null) {
logger.info("Config file is in use by another instance, waiting…");
lock = fileChannel.lock();
logger.info("Config file lock acquired.");
}
return new Pair<>(fileChannel, lock);
}
public void setResolver(final SignalServiceAddressResolver resolver) {
signalProtocolStore.setResolver(resolver);
}
public void addPreKeys(Collection<PreKeyRecord> records) {
for (PreKeyRecord record : records) {
signalProtocolStore.storePreKey(record.getId(), record);
}
preKeyIdOffset = (preKeyIdOffset + records.size()) % Medium.MAX_VALUE;
}
public void addSignedPreKey(SignedPreKeyRecord record) {
signalProtocolStore.storeSignedPreKey(record.getId(), record);
nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
}
public JsonSignalProtocolStore getSignalProtocolStore() {
return signalProtocolStore;
}
public JsonGroupStore getGroupStore() {
return groupStore;
}
public JsonContactsStore getContactStore() {
return contactStore;
}
public RecipientStore getRecipientStore() {
return recipientStore;
}
public ProfileStore getProfileStore() {
return profileStore;
}
public StickerStore getStickerStore() {
return stickerStore;
}
public String getUsername() {
return username;
}
public UUID getUuid() {
return uuid;
}
public void setUuid(final UUID uuid) {
this.uuid = uuid;
}
public SignalServiceAddress getSelfAddress() {
return new SignalServiceAddress(uuid, username);
}
public int getDeviceId() {
return deviceId;
}
public String getPassword() {
return password;
}
public void setPassword(final String password) {
this.password = password;
}
public String getRegistrationLockPin() {
return registrationLockPin;
}
public String getRegistrationLock() {
return null; // TODO implement KBS
}
public void setRegistrationLockPin(final String registrationLockPin) {
this.registrationLockPin = registrationLockPin;
}
public String getSignalingKey() {
return signalingKey;
}
public void setSignalingKey(final String signalingKey) {
this.signalingKey = signalingKey;
}
public ProfileKey getProfileKey() {
return profileKey;
}
public void setProfileKey(final ProfileKey profileKey) {
this.profileKey = profileKey;
}
public int getPreKeyIdOffset() {
return preKeyIdOffset;
}
public int getNextSignedPreKeyId() {
return nextSignedPreKeyId;
}
public boolean isRegistered() {
return registered;
}
public void setRegistered(final boolean registered) {
this.registered = registered;
}
public boolean isMultiDevice() {
return isMultiDevice;
}
public void setMultiDevice(final boolean multiDevice) {
isMultiDevice = multiDevice;
}
@Override
public void close() throws IOException {
synchronized (fileChannel) {
try {
lock.close();
} catch (ClosedChannelException ignored) {
}
fileChannel.close();
}
}
}

View file

@ -1,53 +0,0 @@
package org.asamk.signal.storage.contacts;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.UUID;
import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY;
public class ContactInfo {
@JsonProperty
public String name;
@JsonProperty
public String number;
@JsonProperty
public UUID uuid;
@JsonProperty
public String color;
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
@JsonProperty(access = WRITE_ONLY)
public String profileKey;
@JsonProperty(defaultValue = "false")
public boolean blocked;
@JsonProperty
public Integer inboxPosition;
@JsonProperty(defaultValue = "false")
public boolean archived;
public ContactInfo() {
}
public ContactInfo(SignalServiceAddress address) {
this.number = address.getNumber().orNull();
this.uuid = address.getUuid().orNull();
}
@JsonIgnore
public SignalServiceAddress getAddress() {
return new SignalServiceAddress(uuid, number);
}
}

View file

@ -1,52 +0,0 @@
package org.asamk.signal.storage.contacts;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList;
import java.util.List;
public class JsonContactsStore {
@JsonProperty("contacts")
private List<ContactInfo> contacts = new ArrayList<>();
public void updateContact(ContactInfo contact) {
final SignalServiceAddress contactAddress = contact.getAddress();
for (int i = 0; i < contacts.size(); i++) {
if (contacts.get(i).getAddress().matches(contactAddress)) {
contacts.set(i, contact);
return;
}
}
contacts.add(contact);
}
public ContactInfo getContact(SignalServiceAddress address) {
for (ContactInfo contact : contacts) {
if (contact.getAddress().matches(address)) {
if (contact.uuid == null) {
contact.uuid = address.getUuid().orNull();
} else if (contact.number == null) {
contact.number = address.getNumber().orNull();
}
return contact;
}
}
return null;
}
public List<ContactInfo> getContacts() {
return new ArrayList<>(contacts);
}
/**
* Remove all contacts from the store
*/
public void clear() {
contacts.clear();
}
}

View file

@ -1,77 +0,0 @@
package org.asamk.signal.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public abstract class GroupInfo {
@JsonIgnore
public abstract GroupId getGroupId();
@JsonIgnore
public abstract String getTitle();
@JsonIgnore
public abstract GroupInviteLinkUrl getGroupInviteLink();
@JsonIgnore
public abstract Set<SignalServiceAddress> getMembers();
@JsonIgnore
public Set<SignalServiceAddress> getPendingMembers() {
return Set.of();
}
@JsonIgnore
public Set<SignalServiceAddress> getRequestingMembers() {
return Set.of();
}
@JsonIgnore
public abstract boolean isBlocked();
@JsonIgnore
public abstract void setBlocked(boolean blocked);
@JsonIgnore
public abstract int getMessageExpirationTime();
@JsonIgnore
public Set<SignalServiceAddress> getMembersWithout(SignalServiceAddress address) {
return getMembers().stream().filter(member -> !member.matches(address)).collect(Collectors.toSet());
}
@JsonIgnore
public Set<SignalServiceAddress> getMembersIncludingPendingWithout(SignalServiceAddress address) {
return Stream.concat(getMembers().stream(), getPendingMembers().stream())
.filter(member -> !member.matches(address))
.collect(Collectors.toSet());
}
@JsonIgnore
public boolean isMember(SignalServiceAddress address) {
for (SignalServiceAddress member : getMembers()) {
if (member.matches(address)) {
return true;
}
}
return false;
}
@JsonIgnore
public boolean isPendingMember(SignalServiceAddress address) {
for (SignalServiceAddress member : getPendingMembers()) {
if (member.matches(address)) {
return true;
}
}
return false;
}
}

View file

@ -1,212 +0,0 @@
package org.asamk.signal.storage.groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdV1;
import org.asamk.signal.manager.GroupIdV2;
import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.asamk.signal.manager.GroupUtils;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class GroupInfoV1 extends GroupInfo {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
private final GroupIdV1 groupId;
private GroupIdV2 expectedV2Id;
@JsonProperty
public String name;
@JsonProperty
@JsonDeserialize(using = MembersDeserializer.class)
@JsonSerialize(using = MembersSerializer.class)
public Set<SignalServiceAddress> members = new HashSet<>();
@JsonProperty
public String color;
@JsonProperty(defaultValue = "0")
public int messageExpirationTime;
@JsonProperty(defaultValue = "false")
public boolean blocked;
@JsonProperty
public Integer inboxPosition;
@JsonProperty(defaultValue = "false")
public boolean archived;
public GroupInfoV1(GroupIdV1 groupId) {
this.groupId = groupId;
}
public GroupInfoV1(
@JsonProperty("groupId") byte[] groupId,
@JsonProperty("expectedV2Id") byte[] expectedV2Id,
@JsonProperty("name") String name,
@JsonProperty("members") Collection<SignalServiceAddress> members,
@JsonProperty("avatarId") long _ignored_avatarId,
@JsonProperty("color") String color,
@JsonProperty("blocked") boolean blocked,
@JsonProperty("inboxPosition") Integer inboxPosition,
@JsonProperty("archived") boolean archived,
@JsonProperty("messageExpirationTime") int messageExpirationTime,
@JsonProperty("active") boolean _ignored_active
) {
this.groupId = GroupId.v1(groupId);
this.expectedV2Id = GroupId.v2(expectedV2Id);
this.name = name;
this.members.addAll(members);
this.color = color;
this.blocked = blocked;
this.inboxPosition = inboxPosition;
this.archived = archived;
this.messageExpirationTime = messageExpirationTime;
}
@Override
@JsonIgnore
public GroupIdV1 getGroupId() {
return groupId;
}
@JsonProperty("groupId")
private byte[] getGroupIdJackson() {
return groupId.serialize();
}
@JsonIgnore
public GroupIdV2 getExpectedV2Id() {
if (expectedV2Id == null) {
expectedV2Id = GroupUtils.getGroupIdV2(groupId);
}
return expectedV2Id;
}
@JsonProperty("expectedV2Id")
private byte[] getExpectedV2IdJackson() {
return expectedV2Id.serialize();
}
@Override
public String getTitle() {
return name;
}
@Override
public GroupInviteLinkUrl getGroupInviteLink() {
return null;
}
@JsonIgnore
public Set<SignalServiceAddress> getMembers() {
return members;
}
@Override
public boolean isBlocked() {
return blocked;
}
@Override
public void setBlocked(final boolean blocked) {
this.blocked = blocked;
}
@Override
public int getMessageExpirationTime() {
return messageExpirationTime;
}
public void addMembers(Collection<SignalServiceAddress> addresses) {
for (SignalServiceAddress address : addresses) {
if (this.members.contains(address)) {
continue;
}
removeMember(address);
this.members.add(address);
}
}
public void removeMember(SignalServiceAddress address) {
this.members.removeIf(member -> member.matches(address));
}
private static final class JsonSignalServiceAddress {
@JsonProperty
private UUID uuid;
@JsonProperty
private String number;
JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) {
this.uuid = uuid;
this.number = number;
}
JsonSignalServiceAddress(SignalServiceAddress address) {
this.uuid = address.getUuid().orNull();
this.number = address.getNumber().orNull();
}
SignalServiceAddress toSignalServiceAddress() {
return new SignalServiceAddress(uuid, number);
}
}
private static class MembersSerializer extends JsonSerializer<Set<SignalServiceAddress>> {
@Override
public void serialize(
final Set<SignalServiceAddress> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
jgen.writeStartArray(value.size());
for (SignalServiceAddress address : value) {
if (address.getUuid().isPresent()) {
jgen.writeObject(new JsonSignalServiceAddress(address));
} else {
jgen.writeString(address.getNumber().get());
}
}
jgen.writeEndArray();
}
}
private static class MembersDeserializer extends JsonDeserializer<Set<SignalServiceAddress>> {
@Override
public Set<SignalServiceAddress> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
Set<SignalServiceAddress> addresses = new HashSet<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
if (n.isTextual()) {
addresses.add(new SignalServiceAddress(null, n.textValue()));
} else {
JsonSignalServiceAddress address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class);
addresses.add(address.toSignalServiceAddress());
}
}
return addresses;
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,122 +0,0 @@
package org.asamk.signal.storage.profiles;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.io.File;
public class SignalProfile {
@JsonProperty
private final String identityKey;
@JsonProperty
private final String name;
private final File avatarFile;
@JsonProperty
private final String unidentifiedAccess;
@JsonProperty
private final boolean unrestrictedUnidentifiedAccess;
@JsonProperty
private final Capabilities capabilities;
public SignalProfile(
final String identityKey,
final String name,
final File avatarFile,
final String unidentifiedAccess,
final boolean unrestrictedUnidentifiedAccess,
final SignalServiceProfile.Capabilities capabilities
) {
this.identityKey = identityKey;
this.name = name;
this.avatarFile = avatarFile;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = new Capabilities();
this.capabilities.storage = capabilities.isStorage();
this.capabilities.gv1Migration = capabilities.isGv1Migration();
this.capabilities.gv2 = capabilities.isGv2();
}
public SignalProfile(
@JsonProperty("identityKey") final String identityKey,
@JsonProperty("name") final String name,
@JsonProperty("unidentifiedAccess") final String unidentifiedAccess,
@JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess,
@JsonProperty("capabilities") final Capabilities capabilities
) {
this.identityKey = identityKey;
this.name = name;
this.avatarFile = null;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = capabilities;
}
public String getIdentityKey() {
return identityKey;
}
public String getName() {
return name;
}
public File getAvatarFile() {
return avatarFile;
}
public String getUnidentifiedAccess() {
return unidentifiedAccess;
}
public boolean isUnrestrictedUnidentifiedAccess() {
return unrestrictedUnidentifiedAccess;
}
public Capabilities getCapabilities() {
return capabilities;
}
@Override
public String toString() {
return "SignalProfile{"
+ "identityKey='"
+ identityKey
+ '\''
+ ", name='"
+ name
+ '\''
+ ", avatarFile="
+ avatarFile
+ ", unidentifiedAccess='"
+ unidentifiedAccess
+ '\''
+ ", unrestrictedUnidentifiedAccess="
+ unrestrictedUnidentifiedAccess
+ ", capabilities="
+ capabilities
+ '}';
}
public static class Capabilities {
@JsonIgnore
public boolean uuid;
@JsonProperty
public boolean gv2;
@JsonProperty
public boolean storage;
@JsonProperty
public boolean gv1Migration;
}
}

View file

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

View file

@ -1,316 +0,0 @@
package org.asamk.signal.storage.protocol;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.IdentityKeyStore;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
public class JsonIdentityKeyStore implements IdentityKeyStore {
final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class);
private final List<Identity> identities = new ArrayList<>();
private final IdentityKeyPair identityKeyPair;
private final int localRegistrationId;
private SignalServiceAddressResolver resolver;
public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) {
this.identityKeyPair = identityKeyPair;
this.localRegistrationId = localRegistrationId;
}
public void setResolver(final SignalServiceAddressResolver resolver) {
this.resolver = resolver;
}
private SignalServiceAddress resolveSignalServiceAddress(String identifier) {
if (resolver != null) {
return resolver.resolveSignalServiceAddress(identifier);
} else {
return Util.getSignalServiceAddressFromIdentifier(identifier);
}
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyPair;
}
@Override
public int getLocalRegistrationId() {
return localRegistrationId;
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return saveIdentity(resolveSignalServiceAddress(address.getName()),
identityKey,
TrustLevel.TRUSTED_UNVERIFIED,
null);
}
/**
* Adds the given identityKey for the user name and sets the trustLevel and added timestamp.
* If the identityKey already exists, the trustLevel and added timestamp are NOT updated.
*
* @param serviceAddress User address, i.e. phone number and/or uuid
* @param identityKey The user's public key
* @param trustLevel Level of trust: untrusted, trusted, trusted and verified
* @param added Added timestamp, if null and the key is newly added, the current time is used.
*/
public boolean saveIdentity(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added
) {
for (Identity id : identities) {
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue;
}
if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
id.address = serviceAddress;
}
// Identity already exists, not updating the trust level
return true;
}
identities.add(new Identity(serviceAddress, identityKey, trustLevel, added != null ? added : new Date()));
return false;
}
/**
* Update trustLevel for the given identityKey for the user name.
*
* @param serviceAddress User address, i.e. phone number and/or uuid
* @param identityKey The user's public key
* @param trustLevel Level of trust: untrusted, trusted, trusted and verified
*/
public void setIdentityTrustLevel(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel
) {
for (Identity id : identities) {
if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) {
continue;
}
if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) {
id.address = serviceAddress;
}
id.trustLevel = trustLevel;
return;
}
identities.add(new Identity(serviceAddress, identityKey, trustLevel, new Date()));
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
// TODO implement possibility for different handling of incoming/outgoing trust decisions
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
boolean trustOnFirstUse = true;
for (Identity id : identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
if (id.identityKey.equals(identityKey)) {
return id.isTrusted();
} else {
trustOnFirstUse = false;
}
}
return trustOnFirstUse;
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
Identity identity = getIdentity(serviceAddress);
return identity == null ? null : identity.getIdentityKey();
}
public Identity getIdentity(SignalServiceAddress serviceAddress) {
long maxDate = 0;
Identity maxIdentity = null;
for (Identity id : this.identities) {
if (!id.address.matches(serviceAddress)) {
continue;
}
final long time = id.getDateAdded().getTime();
if (maxIdentity == null || maxDate <= time) {
maxDate = time;
maxIdentity = id;
}
}
return maxIdentity;
}
public List<Identity> getIdentities() {
// TODO deep copy
return identities;
}
public List<Identity> getIdentities(SignalServiceAddress serviceAddress) {
List<Identity> identities = new ArrayList<>();
for (Identity identity : this.identities) {
if (identity.address.matches(serviceAddress)) {
identities.add(identity);
}
}
return identities;
}
public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer<JsonIdentityKeyStore> {
@Override
public JsonIdentityKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
try {
int localRegistrationId = node.get("registrationId").asInt();
IdentityKeyPair identityKeyPair = new IdentityKeyPair(Base64.decode(node.get("identityKey").asText()));
JsonIdentityKeyStore keyStore = new JsonIdentityKeyStore(identityKeyPair, localRegistrationId);
JsonNode trustedKeysNode = node.get("trustedKeys");
if (trustedKeysNode.isArray()) {
for (JsonNode trustedKey : trustedKeysNode) {
String trustedKeyName = trustedKey.hasNonNull("name") ? trustedKey.get("name").asText() : null;
if (UuidUtil.isUuid(trustedKeyName)) {
// Ignore identities that were incorrectly created with UUIDs as name
continue;
}
UUID uuid = trustedKey.hasNonNull("uuid") ? UuidUtil.parseOrNull(trustedKey.get("uuid")
.asText()) : null;
final SignalServiceAddress serviceAddress = uuid == null
? Util.getSignalServiceAddressFromIdentifier(trustedKeyName)
: new SignalServiceAddress(uuid, trustedKeyName);
try {
IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0);
TrustLevel trustLevel = trustedKey.has("trustLevel") ? TrustLevel.fromInt(trustedKey.get(
"trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
Date added = trustedKey.has("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp")
.asLong()) : new Date();
keyStore.saveIdentity(serviceAddress, id, trustLevel, added);
} catch (InvalidKeyException | IOException e) {
logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
}
}
}
return keyStore;
} catch (InvalidKeyException e) {
throw new IOException(e);
}
}
}
public static class JsonIdentityKeyStoreSerializer extends JsonSerializer<JsonIdentityKeyStore> {
@Override
public void serialize(
JsonIdentityKeyStore jsonIdentityKeyStore, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartObject();
json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId());
json.writeStringField("identityKey",
Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize()));
json.writeArrayFieldStart("trustedKeys");
for (Identity trustedKey : jsonIdentityKeyStore.identities) {
json.writeStartObject();
if (trustedKey.getAddress().getNumber().isPresent()) {
json.writeStringField("name", trustedKey.getAddress().getNumber().get());
}
if (trustedKey.getAddress().getUuid().isPresent()) {
json.writeStringField("uuid", trustedKey.getAddress().getUuid().get().toString());
}
json.writeStringField("identityKey", Base64.encodeBytes(trustedKey.identityKey.serialize()));
json.writeNumberField("trustLevel", trustedKey.trustLevel.ordinal());
json.writeNumberField("addedTimestamp", trustedKey.added.getTime());
json.writeEndObject();
}
json.writeEndArray();
json.writeEndObject();
}
}
public 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,108 +0,0 @@
package org.asamk.signal.storage.protocol;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.util.HashMap;
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() {
}
private void addPreKeys(Map<Integer, byte[]> preKeys) {
store.putAll(preKeys);
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
try {
if (!store.containsKey(preKeyId)) {
throw new InvalidKeyIdException("No such prekeyrecord!");
}
return new PreKeyRecord(store.get(preKeyId));
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
store.put(preKeyId, record.serialize());
}
@Override
public boolean containsPreKey(int preKeyId) {
return store.containsKey(preKeyId);
}
@Override
public void removePreKey(int preKeyId) {
store.remove(preKeyId);
}
public static class JsonPreKeyStoreDeserializer extends JsonDeserializer<JsonPreKeyStore> {
@Override
public JsonPreKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Map<Integer, byte[]> preKeyMap = new HashMap<>();
if (node.isArray()) {
for (JsonNode preKey : node) {
Integer preKeyId = preKey.get("id").asInt();
try {
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
} catch (IOException e) {
logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage());
}
}
}
JsonPreKeyStore keyStore = new JsonPreKeyStore();
keyStore.addPreKeys(preKeyMap);
return keyStore;
}
}
public static class JsonPreKeyStoreSerializer extends JsonSerializer<JsonPreKeyStore> {
@Override
public void serialize(
JsonPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartArray();
for (Map.Entry<Integer, byte[]> preKey : jsonPreKeyStore.store.entrySet()) {
json.writeStartObject();
json.writeNumberField("id", preKey.getKey());
json.writeStringField("record", Base64.encodeBytes(preKey.getValue()));
json.writeEndObject();
}
json.writeEndArray();
}
}
}

View file

@ -1,190 +0,0 @@
package org.asamk.signal.storage.protocol;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
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;
public JsonSessionStore() {
}
public void setResolver(final SignalServiceAddressResolver resolver) {
this.resolver = resolver;
}
private SignalServiceAddress resolveSignalServiceAddress(String identifier) {
if (resolver != null) {
return resolver.resolveSignalServiceAddress(identifier);
} else {
return Util.getSignalServiceAddressFromIdentifier(identifier);
}
}
@Override
public synchronized SessionRecord loadSession(SignalProtocolAddress address) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
for (SessionInfo info : sessions) {
if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) {
try {
return new SessionRecord(info.sessionRecord);
} catch (IOException e) {
logger.warn("Failed to load session, resetting session: {}", e.getMessage());
final SessionRecord sessionRecord = new SessionRecord();
info.sessionRecord = sessionRecord.serialize();
return sessionRecord;
}
}
}
return new SessionRecord();
}
public synchronized List<SessionInfo> getSessions() {
return sessions;
}
@Override
public synchronized List<Integer> getSubDeviceSessions(String name) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name);
List<Integer> deviceIds = new LinkedList<>();
for (SessionInfo info : sessions) {
if (info.address.matches(serviceAddress) && info.deviceId != 1) {
deviceIds.add(info.deviceId);
}
}
return deviceIds;
}
@Override
public synchronized void storeSession(SignalProtocolAddress address, SessionRecord record) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
for (SessionInfo info : sessions) {
if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) {
if (!info.address.getUuid().isPresent() || !info.address.getNumber().isPresent()) {
info.address = serviceAddress;
}
info.sessionRecord = record.serialize();
return;
}
}
sessions.add(new SessionInfo(serviceAddress, address.getDeviceId(), record.serialize()));
}
@Override
public synchronized boolean containsSession(SignalProtocolAddress address) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
for (SessionInfo info : sessions) {
if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) {
return true;
}
}
return false;
}
@Override
public synchronized void deleteSession(SignalProtocolAddress address) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName());
sessions.removeIf(info -> info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId());
}
@Override
public synchronized void deleteAllSessions(String name) {
SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name);
deleteAllSessions(serviceAddress);
}
public synchronized void deleteAllSessions(SignalServiceAddress serviceAddress) {
sessions.removeIf(info -> info.address.matches(serviceAddress));
}
public static class JsonSessionStoreDeserializer extends JsonDeserializer<JsonSessionStore> {
@Override
public JsonSessionStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
JsonSessionStore sessionStore = new JsonSessionStore();
if (node.isArray()) {
for (JsonNode session : node) {
String sessionName = session.hasNonNull("name") ? session.get("name").asText() : null;
if (UuidUtil.isUuid(sessionName)) {
// Ignore sessions that were incorrectly created with UUIDs as name
continue;
}
UUID uuid = session.hasNonNull("uuid") ? UuidUtil.parseOrNull(session.get("uuid").asText()) : null;
final SignalServiceAddress serviceAddress = uuid == null
? Util.getSignalServiceAddressFromIdentifier(sessionName)
: new SignalServiceAddress(uuid, sessionName);
final int deviceId = session.get("deviceId").asInt();
final String record = session.get("record").asText();
try {
SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record));
sessionStore.sessions.add(sessionInfo);
} catch (IOException e) {
logger.warn("Error while decoding session for {}: {}", sessionName, e.getMessage());
}
}
}
return sessionStore;
}
}
public static class JsonSessionStoreSerializer extends JsonSerializer<JsonSessionStore> {
@Override
public void serialize(
JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartArray();
for (SessionInfo sessionInfo : jsonSessionStore.sessions) {
json.writeStartObject();
if (sessionInfo.address.getNumber().isPresent()) {
json.writeStringField("name", sessionInfo.address.getNumber().get());
}
if (sessionInfo.address.getUuid().isPresent()) {
json.writeStringField("uuid", sessionInfo.address.getUuid().get().toString());
}
json.writeNumberField("deviceId", sessionInfo.deviceId);
json.writeStringField("record", Base64.encodeBytes(sessionInfo.sessionRecord));
json.writeEndObject();
}
json.writeEndArray();
}
}
}

View file

@ -1,198 +0,0 @@
package org.asamk.signal.storage.protocol;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.asamk.signal.manager.TrustLevel;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
public class JsonSignalProtocolStore implements SignalProtocolStore {
@JsonProperty("preKeys")
@JsonDeserialize(using = JsonPreKeyStore.JsonPreKeyStoreDeserializer.class)
@JsonSerialize(using = JsonPreKeyStore.JsonPreKeyStoreSerializer.class)
private JsonPreKeyStore preKeyStore;
@JsonProperty("sessionStore")
@JsonDeserialize(using = JsonSessionStore.JsonSessionStoreDeserializer.class)
@JsonSerialize(using = JsonSessionStore.JsonSessionStoreSerializer.class)
private JsonSessionStore sessionStore;
@JsonProperty("signedPreKeyStore")
@JsonDeserialize(using = JsonSignedPreKeyStore.JsonSignedPreKeyStoreDeserializer.class)
@JsonSerialize(using = JsonSignedPreKeyStore.JsonSignedPreKeyStoreSerializer.class)
private JsonSignedPreKeyStore signedPreKeyStore;
@JsonProperty("identityKeyStore")
@JsonDeserialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreDeserializer.class)
@JsonSerialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreSerializer.class)
private JsonIdentityKeyStore identityKeyStore;
public JsonSignalProtocolStore() {
}
public JsonSignalProtocolStore(
JsonPreKeyStore preKeyStore,
JsonSessionStore sessionStore,
JsonSignedPreKeyStore signedPreKeyStore,
JsonIdentityKeyStore identityKeyStore
) {
this.preKeyStore = preKeyStore;
this.sessionStore = sessionStore;
this.signedPreKeyStore = signedPreKeyStore;
this.identityKeyStore = identityKeyStore;
}
public JsonSignalProtocolStore(IdentityKeyPair identityKeyPair, int registrationId) {
preKeyStore = new JsonPreKeyStore();
sessionStore = new JsonSessionStore();
signedPreKeyStore = new JsonSignedPreKeyStore();
this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId);
}
public void setResolver(final SignalServiceAddressResolver resolver) {
sessionStore.setResolver(resolver);
identityKeyStore.setResolver(resolver);
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return identityKeyStore.getIdentityKeyPair();
}
@Override
public int getLocalRegistrationId() {
return identityKeyStore.getLocalRegistrationId();
}
@Override
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
return identityKeyStore.saveIdentity(address, identityKey);
}
public void saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) {
identityKeyStore.saveIdentity(serviceAddress, identityKey, trustLevel, null);
}
public void setIdentityTrustLevel(
SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel
) {
identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel);
}
public List<JsonIdentityKeyStore.Identity> getIdentities() {
return identityKeyStore.getIdentities();
}
public List<JsonIdentityKeyStore.Identity> getIdentities(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentities(serviceAddress);
}
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
}
@Override
public IdentityKey getIdentity(SignalProtocolAddress address) {
return identityKeyStore.getIdentity(address);
}
public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) {
return identityKeyStore.getIdentity(serviceAddress);
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
return preKeyStore.loadPreKey(preKeyId);
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
preKeyStore.storePreKey(preKeyId, record);
}
@Override
public boolean containsPreKey(int preKeyId) {
return preKeyStore.containsPreKey(preKeyId);
}
@Override
public void removePreKey(int preKeyId) {
preKeyStore.removePreKey(preKeyId);
}
@Override
public SessionRecord loadSession(SignalProtocolAddress address) {
return sessionStore.loadSession(address);
}
public List<SessionInfo> getSessions() {
return sessionStore.getSessions();
}
@Override
public List<Integer> getSubDeviceSessions(String name) {
return sessionStore.getSubDeviceSessions(name);
}
@Override
public void storeSession(SignalProtocolAddress address, SessionRecord record) {
sessionStore.storeSession(address, record);
}
@Override
public boolean containsSession(SignalProtocolAddress address) {
return sessionStore.containsSession(address);
}
@Override
public void deleteSession(SignalProtocolAddress address) {
sessionStore.deleteSession(address);
}
@Override
public void deleteAllSessions(String name) {
sessionStore.deleteAllSessions(name);
}
public void deleteAllSessions(SignalServiceAddress serviceAddress) {
sessionStore.deleteAllSessions(serviceAddress);
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
return signedPreKeyStore.loadSignedPreKey(signedPreKeyId);
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
return signedPreKeyStore.loadSignedPreKeys();
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return signedPreKeyStore.containsSignedPreKey(signedPreKeyId);
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
signedPreKeyStore.removeSignedPreKey(signedPreKeyId);
}
}

View file

@ -1,125 +0,0 @@
package org.asamk.signal.storage.protocol;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
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() {
}
private void addSignedPreKeys(Map<Integer, byte[]> preKeys) {
store.putAll(preKeys);
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
try {
if (!store.containsKey(signedPreKeyId)) {
throw new InvalidKeyIdException("No such signedprekeyrecord! " + signedPreKeyId);
}
return new SignedPreKeyRecord(store.get(signedPreKeyId));
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
try {
List<SignedPreKeyRecord> results = new LinkedList<>();
for (byte[] serialized : store.values()) {
results.add(new SignedPreKeyRecord(serialized));
}
return results;
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
store.put(signedPreKeyId, record.serialize());
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return store.containsKey(signedPreKeyId);
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
store.remove(signedPreKeyId);
}
public static class JsonSignedPreKeyStoreDeserializer extends JsonDeserializer<JsonSignedPreKeyStore> {
@Override
public JsonSignedPreKeyStore deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
Map<Integer, byte[]> preKeyMap = new HashMap<>();
if (node.isArray()) {
for (JsonNode preKey : node) {
Integer preKeyId = preKey.get("id").asInt();
try {
preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText()));
} catch (IOException e) {
logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage());
}
}
}
JsonSignedPreKeyStore keyStore = new JsonSignedPreKeyStore();
keyStore.addSignedPreKeys(preKeyMap);
return keyStore;
}
}
public static class JsonSignedPreKeyStoreSerializer extends JsonSerializer<JsonSignedPreKeyStore> {
@Override
public void serialize(
JsonSignedPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartArray();
for (Map.Entry<Integer, byte[]> signedPreKey : jsonPreKeyStore.store.entrySet()) {
json.writeStartObject();
json.writeNumberField("id", signedPreKey.getKey());
json.writeStringField("record", Base64.encodeBytes(signedPreKey.getValue()));
json.writeEndObject();
}
json.writeEndArray();
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,60 +0,0 @@
package org.asamk.signal.storage.threads;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LegacyJsonThreadStore {
private static final ObjectMapper jsonProcessor = new ObjectMapper();
@JsonProperty("threads")
@JsonSerialize(using = MapToListSerializer.class)
@JsonDeserialize(using = ThreadsDeserializer.class)
private Map<String, ThreadInfo> threads = new HashMap<>();
public List<ThreadInfo> getThreads() {
return new ArrayList<>(threads.values());
}
private static class MapToListSerializer extends JsonSerializer<Map<?, ?>> {
@Override
public void serialize(
final Map<?, ?> value, final JsonGenerator jgen, final SerializerProvider provider
) throws IOException {
jgen.writeObject(value.values());
}
}
private static class ThreadsDeserializer extends JsonDeserializer<Map<String, ThreadInfo>> {
@Override
public Map<String, ThreadInfo> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
Map<String, ThreadInfo> threads = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {
ThreadInfo t = jsonProcessor.treeToValue(n, ThreadInfo.class);
threads.put(t.id, t);
}
return threads;
}
}
}

View file

@ -1,12 +0,0 @@
package org.asamk.signal.storage.threads;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ThreadInfo {
@JsonProperty
public String id;
@JsonProperty
public int messageExpirationTime;
}