mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Cleanup utils
This commit is contained in:
parent
b738f5740c
commit
bbdd6a8910
19 changed files with 477 additions and 456 deletions
60
src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java
Normal file
60
src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
72
src/main/java/org/asamk/signal/manager/util/IOUtils.java
Normal file
72
src/main/java/org/asamk/signal/manager/util/IOUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
97
src/main/java/org/asamk/signal/manager/util/Utils.java
Normal file
97
src/main/java/org/asamk/signal/manager/util/Utils.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue