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