]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/manager/Utils.java
Implement support for sending/receiving Group V2 messages
[signal-cli] / src / main / java / org / asamk / signal / manager / Utils.java
1 package org.asamk.signal.manager;
2
3 import org.signal.libsignal.metadata.certificate.CertificateValidator;
4 import org.whispersystems.libsignal.IdentityKey;
5 import org.whispersystems.libsignal.InvalidKeyException;
6 import org.whispersystems.libsignal.ecc.Curve;
7 import org.whispersystems.libsignal.ecc.ECPublicKey;
8 import org.whispersystems.libsignal.fingerprint.Fingerprint;
9 import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
10 import org.whispersystems.libsignal.util.guava.Optional;
11 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
12 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
13 import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
14 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
15 import org.whispersystems.signalservice.api.util.StreamDetails;
16 import org.whispersystems.signalservice.api.util.UuidUtil;
17 import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
18 import org.whispersystems.util.Base64;
19
20 import java.io.BufferedInputStream;
21 import java.io.DataInputStream;
22 import java.io.DataOutputStream;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.FileNotFoundException;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.OutputStream;
30 import java.net.URI;
31 import java.net.URLConnection;
32 import java.net.URLDecoder;
33 import java.net.URLEncoder;
34 import java.nio.charset.StandardCharsets;
35 import java.nio.file.Files;
36 import java.util.ArrayList;
37 import java.util.HashMap;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.UUID;
41
42 import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
43
44 class Utils {
45
46 static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
47 List<SignalServiceAttachment> signalServiceAttachments = null;
48 if (attachments != null) {
49 signalServiceAttachments = new ArrayList<>(attachments.size());
50 for (String attachment : attachments) {
51 try {
52 signalServiceAttachments.add(createAttachment(new File(attachment)));
53 } catch (IOException e) {
54 throw new AttachmentInvalidException(attachment, e);
55 }
56 }
57 }
58 return signalServiceAttachments;
59 }
60
61 static String getFileMimeType(File file, String defaultMimeType) throws IOException {
62 String mime = Files.probeContentType(file.toPath());
63 if (mime == null) {
64 try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) {
65 mime = URLConnection.guessContentTypeFromStream(bufferedStream);
66 }
67 }
68 if (mime == null) {
69 return defaultMimeType;
70 }
71 return mime;
72 }
73
74 static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
75 InputStream attachmentStream = new FileInputStream(attachmentFile);
76 final long attachmentSize = attachmentFile.length();
77 final String mime = getFileMimeType(attachmentFile, "application/octet-stream");
78 // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option
79 final long uploadTimestamp = System.currentTimeMillis();
80 Optional<byte[]> preview = Optional.absent();
81 Optional<String> caption = Optional.absent();
82 Optional<String> blurHash = Optional.absent();
83 final Optional<ResumableUploadSpec> resumableUploadSpec = Optional.absent();
84 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, false, preview, 0, 0, uploadTimestamp, caption, blurHash, null, null, resumableUploadSpec);
85 }
86
87 static StreamDetails createStreamDetailsFromFile(File file) throws IOException {
88 InputStream stream = new FileInputStream(file);
89 final long size = file.length();
90 String mime = Files.probeContentType(file.toPath());
91 if (mime == null) {
92 mime = "application/octet-stream";
93 }
94 return new StreamDetails(stream, mime, size);
95 }
96
97 static CertificateValidator getCertificateValidator() {
98 try {
99 ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
100 return new CertificateValidator(unidentifiedSenderTrustRoot);
101 } catch (InvalidKeyException | IOException e) {
102 throw new AssertionError(e);
103 }
104 }
105
106 private static Map<String, String> getQueryMap(String query) {
107 String[] params = query.split("&");
108 Map<String, String> map = new HashMap<>();
109 for (String param : params) {
110 final String[] paramParts = param.split("=");
111 String name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8);
112 String value = URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8);
113 map.put(name, value);
114 }
115 return map;
116 }
117
118 static String createDeviceLinkUri(DeviceLinkInfo info) {
119 return "tsdevice:/?uuid=" + URLEncoder.encode(info.deviceIdentifier, StandardCharsets.UTF_8) + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()), StandardCharsets.UTF_8);
120 }
121
122 static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException {
123 Map<String, String> query = getQueryMap(linkUri.getRawQuery());
124 String deviceIdentifier = query.get("uuid");
125 String publicKeyEncoded = query.get("pub_key");
126
127 if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) {
128 throw new RuntimeException("Invalid device link uri");
129 }
130
131 ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
132
133 return new DeviceLinkInfo(deviceIdentifier, deviceKey);
134 }
135
136 static SignalServiceEnvelope loadEnvelope(File file) throws IOException {
137 try (FileInputStream f = new FileInputStream(file)) {
138 DataInputStream in = new DataInputStream(f);
139 int version = in.readInt();
140 if (version > 4) {
141 return null;
142 }
143 int type = in.readInt();
144 String source = in.readUTF();
145 UUID sourceUuid = null;
146 if (version >= 3) {
147 sourceUuid = UuidUtil.parseOrNull(in.readUTF());
148 }
149 int sourceDevice = in.readInt();
150 if (version == 1) {
151 // read legacy relay field
152 in.readUTF();
153 }
154 long timestamp = in.readLong();
155 byte[] content = null;
156 int contentLen = in.readInt();
157 if (contentLen > 0) {
158 content = new byte[contentLen];
159 in.readFully(content);
160 }
161 byte[] legacyMessage = null;
162 int legacyMessageLen = in.readInt();
163 if (legacyMessageLen > 0) {
164 legacyMessage = new byte[legacyMessageLen];
165 in.readFully(legacyMessage);
166 }
167 long serverReceivedTimestamp = 0;
168 String uuid = null;
169 if (version >= 2) {
170 serverReceivedTimestamp = in.readLong();
171 uuid = in.readUTF();
172 if ("".equals(uuid)) {
173 uuid = null;
174 }
175 }
176 long serverDeliveredTimestamp = 0;
177 if (version >= 4) {
178 serverDeliveredTimestamp = in.readLong();
179 }
180 Optional<SignalServiceAddress> addressOptional = sourceUuid == null && source.isEmpty()
181 ? Optional.absent()
182 : Optional.of(new SignalServiceAddress(sourceUuid, source));
183 return new SignalServiceEnvelope(type, addressOptional, sourceDevice, timestamp, legacyMessage, content, serverReceivedTimestamp, serverDeliveredTimestamp, uuid);
184 }
185 }
186
187 static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
188 try (FileOutputStream f = new FileOutputStream(file)) {
189 try (DataOutputStream out = new DataOutputStream(f)) {
190 out.writeInt(4); // version
191 out.writeInt(envelope.getType());
192 out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : "");
193 out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : "");
194 out.writeInt(envelope.getSourceDevice());
195 out.writeLong(envelope.getTimestamp());
196 if (envelope.hasContent()) {
197 out.writeInt(envelope.getContent().length);
198 out.write(envelope.getContent());
199 } else {
200 out.writeInt(0);
201 }
202 if (envelope.hasLegacyMessage()) {
203 out.writeInt(envelope.getLegacyMessage().length);
204 out.write(envelope.getLegacyMessage());
205 } else {
206 out.writeInt(0);
207 }
208 out.writeLong(envelope.getServerReceivedTimestamp());
209 String uuid = envelope.getUuid();
210 out.writeUTF(uuid == null ? "" : uuid);
211 out.writeLong(envelope.getServerDeliveredTimestamp());
212 }
213 }
214 }
215
216 static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException {
217 InputStream input = stream.getInputStream();
218
219 try (OutputStream output = new FileOutputStream(outputFile)) {
220 byte[] buffer = new byte[4096];
221 int read;
222
223 while ((read = input.read(buffer)) != -1) {
224 output.write(buffer, 0, read);
225 }
226 } catch (FileNotFoundException e) {
227 e.printStackTrace();
228 return null;
229 }
230 return outputFile;
231 }
232
233 static String computeSafetyNumber(SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
234 int version;
235 byte[] ownId;
236 byte[] theirId;
237
238 if (ServiceConfig.capabilities.isUuid()
239 && ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) {
240 // Version 2: UUID user
241 version = 2;
242 ownId = UuidUtil.toByteArray(ownAddress.getUuid().get());
243 theirId = UuidUtil.toByteArray(theirAddress.getUuid().get());
244 } else {
245 // Version 1: E164 user
246 version = 1;
247 if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) {
248 return "INVALID ID";
249 }
250 ownId = ownAddress.getNumber().get().getBytes();
251 theirId = theirAddress.getNumber().get().getBytes();
252 }
253
254 Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, ownId, ownIdentityKey, theirId, theirIdentityKey);
255 return fingerprint.getDisplayableFingerprint().getDisplayText();
256 }
257
258 static class DeviceLinkInfo {
259
260 final String deviceIdentifier;
261 final ECPublicKey deviceKey;
262
263 DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) {
264 this.deviceIdentifier = deviceIdentifier;
265 this.deviceKey = deviceKey;
266 }
267 }
268 }