X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/7e897fa6d0aaa87646b51efa3ee1a5ecfaa3865e..b738f5740c94fe7a5df9e322e1345a99ef0c5ce5:/src/main/java/org/asamk/signal/manager/Utils.java diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index dbcd7e42..0a815ea9 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -1,7 +1,5 @@ package org.asamk.signal.manager; -import org.apache.http.util.TextUtils; -import org.asamk.signal.AttachmentInvalidException; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; @@ -14,50 +12,106 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.InvalidNumberException; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; -import org.whispersystems.signalservice.internal.util.Base64; +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.*; +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.*; +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 getSignalServiceAttachments(List attachments) throws AttachmentInvalidException { - List SignalServiceAttachments = null; + List signalServiceAttachments = null; if (attachments != null) { - SignalServiceAttachments = new ArrayList<>(attachments.size()); + signalServiceAttachments = new ArrayList<>(attachments.size()); for (String attachment : attachments) { try { - SignalServiceAttachments.add(createAttachment(new File(attachment))); + signalServiceAttachments.add(createAttachment(new File(attachment))); } catch (IOException e) { throw new AttachmentInvalidException(attachment, e); } } } - return SignalServiceAttachments; + 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(); - String mime = Files.probeContentType(attachmentFile.toPath()); + 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 preview = Optional.absent(); + Optional caption = Optional.absent(); + Optional blurHash = Optional.absent(); + final Optional 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"; } - // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option - Optional preview = Optional.absent(); - Optional caption = Optional.absent(); - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, null); + return new StreamDetails(stream, mime, size); } static CertificateValidator getCertificateValidator() { try { - ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); + ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), + 0); return new CertificateValidator(unidentifiedSenderTrustRoot); } catch (InvalidKeyException | IOException e) { throw new AssertionError(e); @@ -68,31 +122,20 @@ class Utils { String[] params = query.split("&"); Map map = new HashMap<>(); for (String param : params) { - String name = null; final String[] paramParts = param.split("="); - try { - name = URLDecoder.decode(paramParts[0], "utf-8"); - } catch (UnsupportedEncodingException e) { - // Impossible - } - String value = null; - try { - value = URLDecoder.decode(paramParts[1], "utf-8"); - } catch (UnsupportedEncodingException e) { - // Impossible - } + 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) { - try { - return "tsdevice:/?uuid=" + URLEncoder.encode(info.deviceIdentifier, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()), "utf-8"); - } catch (UnsupportedEncodingException e) { - // Shouldn't happen - return null; - } + 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 { @@ -100,7 +143,7 @@ class Utils { String deviceIdentifier = query.get("uuid"); String publicKeyEncoded = query.get("pub_key"); - if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) { + if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) { throw new RuntimeException("Invalid device link uri"); } @@ -109,38 +152,19 @@ class Utils { return new DeviceLinkInfo(deviceIdentifier, deviceKey); } - static Set getSignalServiceAddresses(Collection recipients, String localNumber) { - Set recipientsTS = new HashSet<>(recipients.size()); - for (String recipient : recipients) { - try { - recipientsTS.add(getPushAddress(recipient, localNumber)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - return null; - } - } - return recipientsTS; - } - - static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { - return PhoneNumberFormatter.formatNumber(number, localNumber); - } - - private static SignalServiceAddress getPushAddress(String number, String localNumber) throws InvalidNumberException { - String e164number = canonicalizeNumber(number, localNumber); - return new SignalServiceAddress(e164number); - } - static SignalServiceEnvelope loadEnvelope(File file) throws IOException { try (FileInputStream f = new FileInputStream(file)) { DataInputStream in = new DataInputStream(f); int version = in.readInt(); - if (version > 2) { + if (version > 4) { return null; } int type = in.readInt(); String source = in.readUTF(); + UUID sourceUuid = null; + if (version >= 3) { + sourceUuid = UuidUtil.parseOrNull(in.readUTF()); + } int sourceDevice = in.readInt(); if (version == 1) { // read legacy relay field @@ -159,25 +183,41 @@ class Utils { legacyMessage = new byte[legacyMessageLen]; in.readFully(legacyMessage); } - long serverTimestamp = 0; + long serverReceivedTimestamp = 0; String uuid = null; - if (version == 2) { - serverTimestamp = in.readLong(); + if (version >= 2) { + serverReceivedTimestamp = in.readLong(); uuid = in.readUTF(); if ("".equals(uuid)) { uuid = null; } } - return new SignalServiceEnvelope(type, source, sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); + long serverDeliveredTimestamp = 0; + if (version >= 4) { + serverDeliveredTimestamp = in.readLong(); + } + Optional addressOptional = sourceUuid == null && source.isEmpty() + ? Optional.absent() + : Optional.of(new SignalServiceAddress(sourceUuid, source)); + return new SignalServiceEnvelope(type, + addressOptional, + sourceDevice, + timestamp, + legacyMessage, + content, + serverReceivedTimestamp, + serverDeliveredTimestamp, + uuid); } } static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { try (FileOutputStream f = new FileOutputStream(file)) { try (DataOutputStream out = new DataOutputStream(f)) { - out.writeInt(2); // version + out.writeInt(4); // version out.writeInt(envelope.getType()); - out.writeUTF(envelope.getSource()); + 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()) { @@ -192,9 +232,10 @@ class Utils { } else { out.writeInt(0); } - out.writeLong(envelope.getServerTimestamp()); + out.writeLong(envelope.getServerReceivedTimestamp()); String uuid = envelope.getUuid(); out.writeUTF(uuid == null ? "" : uuid); + out.writeLong(envelope.getServerDeliveredTimestamp()); } } } @@ -216,8 +257,37 @@ class Utils { return outputFile; } - static String computeSafetyNumber(String ownUsername, IdentityKey ownIdentityKey, String theirUsername, IdentityKey theirIdentityKey) { - Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(ownUsername, ownIdentityKey, theirUsername, theirIdentityKey); + static String computeSafetyNumber( + SignalServiceAddress ownAddress, + IdentityKey ownIdentityKey, + SignalServiceAddress theirAddress, + IdentityKey theirIdentityKey + ) { + int version; + byte[] ownId; + byte[] theirId; + + if (ServiceConfig.capabilities.isUuid() && ownAddress.getUuid().isPresent() && theirAddress.getUuid() + .isPresent()) { + // Version 2: UUID user + version = 2; + ownId = UuidUtil.toByteArray(ownAddress.getUuid().get()); + theirId = UuidUtil.toByteArray(theirAddress.getUuid().get()); + } else { + // Version 1: E164 user + version = 1; + if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) { + return "INVALID ID"; + } + ownId = ownAddress.getNumber().get().getBytes(); + theirId = theirAddress.getNumber().get().getBytes(); + } + + Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, + ownId, + ownIdentityKey, + theirId, + theirIdentityKey); return fingerprint.getDisplayableFingerprint().getDisplayText(); }