Cleanup utils

This commit is contained in:
AsamK 2020-12-29 22:48:39 +01:00
parent b738f5740c
commit bbdd6a8910
19 changed files with 477 additions and 456 deletions

View file

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

View file

@ -37,8 +37,11 @@ import org.asamk.signal.manager.storage.profiles.SignalProfile;
import org.asamk.signal.manager.storage.profiles.SignalProfileEntry;
import org.asamk.signal.manager.storage.protocol.IdentityInfo;
import org.asamk.signal.manager.storage.stickers.Sticker;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.MessageCacheUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
import org.signal.libsignal.metadata.ProtocolDuplicateMessageException;
@ -50,6 +53,7 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
@ -125,6 +129,7 @@ import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
@ -185,6 +190,7 @@ public class Manager implements Closeable {
final static Logger logger = LoggerFactory.getLogger(Manager.class);
private final SleepTimer timer = new UptimeSleepTimer();
private final CertificateValidator certificateValidator = new CertificateValidator(ServiceConfig.getUnidentifiedSenderTrustRoot());
private final SignalServiceConfiguration serviceConfiguration;
private final String userAgent;
@ -419,7 +425,7 @@ public class Manager implements Closeable {
}
public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
Utils.DeviceLinkInfo info = Utils.parseDeviceLinkUri(linkUri);
DeviceLinkInfo info = DeviceLinkInfo.parseDeviceLinkUri(linkUri);
addDevice(info.deviceIdentifier, info.deviceKey);
}
@ -696,7 +702,7 @@ public class Manager implements Closeable {
return Optional.absent();
}
return Optional.of(Utils.createAttachment(file));
return Optional.of(AttachmentUtils.createAttachment(file));
}
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(String number) throws IOException {
@ -705,7 +711,7 @@ public class Manager implements Closeable {
return Optional.absent();
}
return Optional.of(Utils.createAttachment(file));
return Optional.of(AttachmentUtils.createAttachment(file));
}
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
@ -751,7 +757,7 @@ public class Manager implements Closeable {
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withBody(messageText);
if (attachments != null) {
messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments));
messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments));
}
return sendGroupMessage(messageBuilder, groupId);
@ -928,7 +934,7 @@ public class Manager implements Closeable {
newE164Members.remove(contact.getNumber());
}
throw new IOException("Failed to add members "
+ Util.join(", ", newE164Members)
+ String.join(", ", newE164Members)
+ " to group: Not registered on Signal");
}
@ -971,7 +977,7 @@ public class Manager implements Closeable {
File aFile = getGroupAvatarFile(g.getGroupId());
if (aFile.exists()) {
try {
group.withAvatar(Utils.createAttachment(aFile));
group.withAvatar(AttachmentUtils.createAttachment(aFile));
} catch (IOException e) {
throw new AttachmentInvalidException(aFile.toString(), e);
}
@ -1022,7 +1028,7 @@ public class Manager implements Closeable {
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withBody(messageText);
if (attachments != null) {
List<SignalServiceAttachment> attachmentStreams = Utils.getSignalServiceAttachments(attachments);
List<SignalServiceAttachment> attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments);
// Upload attachments here, so we only upload once even for multiple recipients
SignalServiceMessageSender messageSender = createMessageSender();
@ -1510,7 +1516,7 @@ public class Manager implements Closeable {
private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException {
SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(),
account.getSignalProtocolStore(),
Utils.getCertificateValidator());
certificateValidator);
try {
return cipher.decrypt(envelope);
} catch (ProtocolUntrustedIdentityException e) {
@ -1820,7 +1826,7 @@ public class Manager implements Closeable {
) {
SignalServiceEnvelope envelope;
try {
envelope = Utils.loadEnvelope(fileEntry);
envelope = MessageCacheUtils.loadEnvelope(fileEntry);
if (envelope == null) {
return;
}
@ -1887,7 +1893,7 @@ public class Manager implements Closeable {
try {
String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : "";
File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp());
Utils.storeEnvelope(envelope1, cacheFile);
MessageCacheUtils.storeEnvelope(envelope1, cacheFile);
} catch (IOException e) {
logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage());
}
@ -2240,7 +2246,7 @@ public class Manager implements Closeable {
return retrieveAttachment(pointer, getContactAvatarFile(number), false);
} else {
SignalServiceAttachmentStream stream = attachment.asStream();
return Utils.retrieveAttachment(stream, getContactAvatarFile(number));
return AttachmentUtils.retrieveAttachment(stream, getContactAvatarFile(number));
}
}
@ -2257,7 +2263,7 @@ public class Manager implements Closeable {
return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
} else {
SignalServiceAttachmentStream stream = attachment.asStream();
return Utils.retrieveAttachment(stream, getGroupAvatarFile(groupId));
return AttachmentUtils.retrieveAttachment(stream, getGroupAvatarFile(groupId));
}
}
@ -2509,7 +2515,7 @@ public class Manager implements Closeable {
}
public ContactInfo getContact(String number) {
return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number));
return account.getContactStore().getContact(Utils.getSignalServiceAddressFromIdentifier(number));
}
public GroupInfo getGroup(GroupId groupId) {
@ -2613,7 +2619,8 @@ public class Manager implements Closeable {
public String computeSafetyNumber(
SignalServiceAddress theirAddress, IdentityKey theirIdentityKey
) {
return Utils.computeSafetyNumber(account.getSelfAddress(),
return Utils.computeSafetyNumber(ServiceConfig.capabilities.isUuid(),
account.getSelfAddress(),
getIdentityKeyPair().getPublicKey(),
theirAddress,
theirIdentityKey);
@ -2626,12 +2633,12 @@ public class Manager implements Closeable {
public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
String canonicalizedNumber = UuidUtil.isUuid(identifier)
? identifier
: Util.canonicalizeNumber(identifier, account.getUsername());
: PhoneNumberFormatter.formatNumber(identifier, account.getUsername());
return resolveSignalServiceAddress(canonicalizedNumber);
}
public SignalServiceAddress resolveSignalServiceAddress(String identifier) {
SignalServiceAddress address = Util.getSignalServiceAddressFromIdentifier(identifier);
SignalServiceAddress address = Utils.getSignalServiceAddressFromIdentifier(identifier);
return resolveSignalServiceAddress(address);
}

View file

@ -17,6 +17,7 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKeyPair;
@ -71,8 +72,7 @@ public class ProvisioningManager {
public String getDeviceLinkUri() throws TimeoutException, IOException {
String deviceUuid = accountManager.getNewDeviceUuid();
return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid,
identityKey.getPublicKey().getPublicKey()));
return new DeviceLinkInfo(deviceUuid, identityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
}
public String finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {

View file

@ -1,6 +1,9 @@
package org.asamk.signal.manager;
import org.signal.zkgroup.ServerPublicParams;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.push.TrustStore;
@ -106,6 +109,14 @@ public class ServiceConfig {
}
}
static ECPublicKey getUnidentifiedSenderTrustRoot() {
try {
return Curve.decodePoint(Base64.decode(UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
} catch (InvalidKeyException | IOException e) {
throw new AssertionError(e);
}
}
private static Map<Integer, SignalCdnUrl[]> makeSignalCdnUrlMapFor(
SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls
) {

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,6 +1,6 @@
package org.asamk.signal.manager.groups;
import static org.asamk.signal.manager.KeyUtils.getSecretBytes;
import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes;
public class GroupIdV1 extends GroupId {

View file

@ -1,6 +1,6 @@
package org.asamk.signal.manager.groups;
import org.asamk.signal.manager.KeyUtils;
import org.asamk.signal.manager.util.KeyUtils;
import java.util.Arrays;

View file

@ -7,7 +7,7 @@ import org.asamk.signal.manager.groups.GroupLinkPassword;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.profiles.SignalProfile;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.manager.util.IOUtils;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member;

View file

@ -25,8 +25,8 @@ import org.asamk.signal.manager.storage.protocol.SignalServiceAddressResolver;
import org.asamk.signal.manager.storage.stickers.StickerStore;
import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
import org.asamk.signal.manager.storage.threads.ThreadInfo;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger;
@ -211,28 +211,28 @@ public class SignalAccount implements Closeable {
deviceId = node.asInt();
}
if (rootNode.has("isMultiDevice")) {
isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
isMultiDevice = Utils.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
}
username = Util.getNotNullNode(rootNode, "username").asText();
password = Util.getNotNullNode(rootNode, "password").asText();
username = Utils.getNotNullNode(rootNode, "username").asText();
password = Utils.getNotNullNode(rootNode, "password").asText();
JsonNode pinNode = rootNode.get("registrationLockPin");
registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText();
if (rootNode.has("signalingKey")) {
signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText();
signalingKey = Utils.getNotNullNode(rootNode, "signalingKey").asText();
}
if (rootNode.has("preKeyIdOffset")) {
preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
preKeyIdOffset = Utils.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
} else {
preKeyIdOffset = 0;
}
if (rootNode.has("nextSignedPreKeyId")) {
nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
nextSignedPreKeyId = Utils.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
} else {
nextSignedPreKeyId = 0;
}
if (rootNode.has("profileKey")) {
try {
profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()));
profileKey = new ProfileKey(Base64.decode(Utils.getNotNullNode(rootNode, "profileKey").asText()));
} catch (InvalidInputException e) {
throw new IOException(
"Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
@ -240,9 +240,9 @@ public class SignalAccount implements Closeable {
}
}
signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"),
signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
JsonSignalProtocolStore.class);
registered = Util.getNotNullNode(rootNode, "registered").asBoolean();
registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
JsonNode groupStoreNode = rootNode.get("groupStore");
if (groupStoreNode != null) {
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);

View file

@ -16,8 +16,8 @@ import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.groups.GroupIdV2;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.IOUtils;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;

View file

@ -9,7 +9,7 @@ import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.util.Util;
import org.asamk.signal.manager.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
@ -51,7 +51,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
if (resolver != null) {
return resolver.resolveSignalServiceAddress(identifier);
} else {
return Util.getSignalServiceAddressFromIdentifier(identifier);
return Utils.getSignalServiceAddressFromIdentifier(identifier);
}
}
@ -213,7 +213,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore {
UUID uuid = trustedKey.hasNonNull("uuid") ? UuidUtil.parseOrNull(trustedKey.get("uuid")
.asText()) : null;
final SignalServiceAddress serviceAddress = uuid == null
? Util.getSignalServiceAddressFromIdentifier(trustedKeyName)
? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName)
: new SignalServiceAddress(uuid, trustedKeyName);
try {
IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0);

View file

@ -8,7 +8,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.asamk.signal.util.Util;
import org.asamk.signal.manager.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.SignalProtocolAddress;
@ -43,7 +43,7 @@ class JsonSessionStore implements SessionStore {
if (resolver != null) {
return resolver.resolveSignalServiceAddress(identifier);
} else {
return Util.getSignalServiceAddressFromIdentifier(identifier);
return Utils.getSignalServiceAddressFromIdentifier(identifier);
}
}
@ -147,7 +147,7 @@ class JsonSessionStore implements SessionStore {
UUID uuid = session.hasNonNull("uuid") ? UuidUtil.parseOrNull(session.get("uuid").asText()) : null;
final SignalServiceAddress serviceAddress = uuid == null
? Util.getSignalServiceAddressFromIdentifier(sessionName)
? Utils.getSignalServiceAddressFromIdentifier(sessionName)
: new SignalServiceAddress(uuid, sessionName);
final int deviceId = session.get("deviceId").asInt();
final String record = session.get("record").asText();

View file

@ -0,0 +1,79 @@
package org.asamk.signal.manager.util;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class AttachmentUtils {
public static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
List<SignalServiceAttachment> signalServiceAttachments = null;
if (attachments != null) {
signalServiceAttachments = new ArrayList<>(attachments.size());
for (String attachment : attachments) {
try {
signalServiceAttachments.add(createAttachment(new File(attachment)));
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
}
}
}
return signalServiceAttachments;
}
public static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
InputStream attachmentStream = new FileInputStream(attachmentFile);
final long attachmentSize = attachmentFile.length();
final String mime = Utils.getFileMimeType(attachmentFile, "application/octet-stream");
// TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option
final long uploadTimestamp = System.currentTimeMillis();
Optional<byte[]> preview = Optional.absent();
Optional<String> caption = Optional.absent();
Optional<String> blurHash = Optional.absent();
final Optional<ResumableUploadSpec> resumableUploadSpec = Optional.absent();
return new SignalServiceAttachmentStream(attachmentStream,
mime,
attachmentSize,
Optional.of(attachmentFile.getName()),
false,
false,
preview,
0,
0,
uploadTimestamp,
caption,
blurHash,
null,
null,
resumableUploadSpec);
}
public static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException {
InputStream input = stream.getInputStream();
try (OutputStream output = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[4096];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
return outputFile;
}
}

View file

@ -0,0 +1,72 @@
package org.asamk.signal.manager.util;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.EnumSet;
import java.util.Set;
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
public class IOUtils {
public static File createTempFile() throws IOException {
return File.createTempFile("signal_tmp_", ".tmp");
}
public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.copy(in, baos);
return baos.toByteArray();
}
public static void createPrivateDirectories(File file) throws IOException {
if (file.exists()) {
return;
}
final Path path = file.toPath();
try {
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE);
Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms));
} catch (UnsupportedOperationException e) {
Files.createDirectories(path);
}
}
public static void createPrivateFile(File path) throws IOException {
final Path file = path.toPath();
try {
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE);
Files.createFile(file, PosixFilePermissions.asFileAttribute(perms));
} catch (UnsupportedOperationException e) {
Files.createFile(file);
}
}
public static void copyStreamToFile(InputStream input, File outputFile) throws IOException {
copyStreamToFile(input, outputFile, 8192);
}
public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException {
try (OutputStream output = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[bufferSize];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
}
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager;
package org.asamk.signal.manager.util;
import org.asamk.signal.util.RandomUtils;
import org.signal.zkgroup.InvalidInputException;
@ -10,11 +10,11 @@ public class KeyUtils {
private KeyUtils() {
}
static String createSignalingKey() {
public static String createSignalingKey() {
return getSecret(52);
}
static ProfileKey createProfileKey() {
public static ProfileKey createProfileKey() {
try {
return new ProfileKey(getSecretBytes(32));
} catch (InvalidInputException e) {
@ -22,11 +22,11 @@ public class KeyUtils {
}
}
static String createPassword() {
public static String createPassword() {
return getSecret(18);
}
static byte[] createStickerUploadKey() {
public static byte[] createStickerUploadKey() {
return getSecretBytes(32);
}

View file

@ -0,0 +1,105 @@
package org.asamk.signal.manager.util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.UUID;
public class MessageCacheUtils {
public 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);
}
}
public 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());
}
}
}
}

View file

@ -0,0 +1,97 @@
package org.asamk.signal.manager.util;
import com.fasterxml.jackson.databind.JsonNode;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.fingerprint.Fingerprint;
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidObjectException;
import java.net.URLConnection;
import java.nio.file.Files;
public class Utils {
public 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;
}
public 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);
}
public static String computeSafetyNumber(
boolean isUuidCapable,
SignalServiceAddress ownAddress,
IdentityKey ownIdentityKey,
SignalServiceAddress theirAddress,
IdentityKey theirIdentityKey
) {
int version;
byte[] ownId;
byte[] theirId;
if (isUuidCapable && 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();
}
public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) {
if (UuidUtil.isUuid(identifier)) {
return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null);
} else {
return new SignalServiceAddress(null, identifier);
}
}
public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
JsonNode node = parent.get(name);
if (node == null) {
throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ",
name));
}
return node;
}
}

View file

@ -1,35 +1,16 @@
package org.asamk.signal.util;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.EnumSet;
import java.util.Set;
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
public class IOUtils {
private IOUtils() {
}
public static File createTempFile() throws IOException {
return File.createTempFile("signal_tmp_", ".tmp");
}
public static String readAll(InputStream in, Charset charset) throws IOException {
StringWriter output = new StringWriter();
byte[] buffer = new byte[4096];
@ -40,36 +21,6 @@ public class IOUtils {
return output.toString();
}
public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.copy(in, baos);
return baos.toByteArray();
}
public static void createPrivateDirectories(File file) throws IOException {
if (file.exists()) {
return;
}
final Path path = file.toPath();
try {
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE);
Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms));
} catch (UnsupportedOperationException e) {
Files.createDirectories(path);
}
}
public static void createPrivateFile(File path) throws IOException {
final Path file = path.toPath();
try {
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE);
Files.createFile(file, PosixFilePermissions.asFileAttribute(perms));
} catch (UnsupportedOperationException e) {
Files.createFile(file);
}
}
public static File getDataHomeDir() {
String dataHome = System.getenv("XDG_DATA_HOME");
if (dataHome != null) {
@ -78,19 +29,4 @@ public class IOUtils {
return new File(new File(System.getProperty("user.home"), ".local"), "share");
}
public static void copyStreamToFile(InputStream input, File outputFile) throws IOException {
copyStreamToFile(input, outputFile, 8192);
}
public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException {
try (OutputStream output = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[bufferSize];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
}
}
}

View file

@ -1,15 +1,7 @@
package org.asamk.signal.util;
import com.fasterxml.jackson.databind.JsonNode;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.InvalidObjectException;
public class Util {
@ -26,41 +18,7 @@ public class Util {
return f.toString();
}
public static String join(CharSequence separator, Iterable<? extends CharSequence> list) {
StringBuilder buf = new StringBuilder();
for (CharSequence str : list) {
if (buf.length() > 0) {
buf.append(separator);
}
buf.append(str);
}
return buf.toString();
}
public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
JsonNode node = parent.get(name);
if (node == null) {
throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ",
name));
}
return node;
}
public static GroupId decodeGroupId(String groupId) throws GroupIdFormatException {
return GroupId.fromBase64(groupId);
}
public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException {
return PhoneNumberFormatter.formatNumber(number, localNumber);
}
public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) {
if (UuidUtil.isUuid(identifier)) {
return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null);
} else {
return new SignalServiceAddress(null, identifier);
}
}
}