1 package org
.asamk
.signal
.manager
;
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
;
20 import java
.io
.BufferedInputStream
;
21 import java
.io
.DataInputStream
;
22 import java
.io
.DataOutputStream
;
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
;
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
;
40 import java
.util
.UUID
;
42 import static org
.whispersystems
.signalservice
.internal
.util
.Util
.isEmpty
;
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
) {
52 signalServiceAttachments
.add(createAttachment(new File(attachment
)));
53 } catch (IOException e
) {
54 throw new AttachmentInvalidException(attachment
, e
);
58 return signalServiceAttachments
;
61 static String
getFileMimeType(File file
, String defaultMimeType
) throws IOException
{
62 String mime
= Files
.probeContentType(file
.toPath());
64 try (InputStream bufferedStream
= new BufferedInputStream(new FileInputStream(file
))) {
65 mime
= URLConnection
.guessContentTypeFromStream(bufferedStream
);
69 return defaultMimeType
;
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
);
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());
92 mime
= "application/octet-stream";
94 return new StreamDetails(stream
, mime
, size
);
97 static CertificateValidator
getCertificateValidator() {
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
);
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
);
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
);
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");
127 if (isEmpty(deviceIdentifier
) || isEmpty(publicKeyEncoded
)) {
128 throw new RuntimeException("Invalid device link uri");
131 ECPublicKey deviceKey
= Curve
.decodePoint(Base64
.decode(publicKeyEncoded
), 0);
133 return new DeviceLinkInfo(deviceIdentifier
, deviceKey
);
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();
143 int type
= in.readInt();
144 String source
= in.readUTF();
145 UUID sourceUuid
= null;
147 sourceUuid
= UuidUtil
.parseOrNull(in.readUTF());
149 int sourceDevice
= in.readInt();
151 // read legacy relay field
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
);
161 byte[] legacyMessage
= null;
162 int legacyMessageLen
= in.readInt();
163 if (legacyMessageLen
> 0) {
164 legacyMessage
= new byte[legacyMessageLen
];
165 in.readFully(legacyMessage
);
167 long serverReceivedTimestamp
= 0;
170 serverReceivedTimestamp
= in.readLong();
172 if ("".equals(uuid
)) {
176 long serverDeliveredTimestamp
= 0;
178 serverDeliveredTimestamp
= in.readLong();
180 Optional
<SignalServiceAddress
> addressOptional
= sourceUuid
== null && source
.isEmpty()
182 : Optional
.of(new SignalServiceAddress(sourceUuid
, source
));
183 return new SignalServiceEnvelope(type
, addressOptional
, sourceDevice
, timestamp
, legacyMessage
, content
, serverReceivedTimestamp
, serverDeliveredTimestamp
, uuid
);
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());
202 if (envelope
.hasLegacyMessage()) {
203 out
.writeInt(envelope
.getLegacyMessage().length
);
204 out
.write(envelope
.getLegacyMessage());
208 out
.writeLong(envelope
.getServerReceivedTimestamp());
209 String uuid
= envelope
.getUuid();
210 out
.writeUTF(uuid
== null ?
"" : uuid
);
211 out
.writeLong(envelope
.getServerDeliveredTimestamp());
216 static File
retrieveAttachment(SignalServiceAttachmentStream stream
, File outputFile
) throws IOException
{
217 InputStream input
= stream
.getInputStream();
219 try (OutputStream output
= new FileOutputStream(outputFile
)) {
220 byte[] buffer
= new byte[4096];
223 while ((read
= input
.read(buffer
)) != -1) {
224 output
.write(buffer
, 0, read
);
226 } catch (FileNotFoundException e
) {
233 static String
computeSafetyNumber(SignalServiceAddress ownAddress
, IdentityKey ownIdentityKey
, SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
) {
238 if (ServiceConfig
.capabilities
.isUuid()
239 && ownAddress
.getUuid().isPresent() && theirAddress
.getUuid().isPresent()) {
240 // Version 2: UUID user
242 ownId
= UuidUtil
.toByteArray(ownAddress
.getUuid().get());
243 theirId
= UuidUtil
.toByteArray(theirAddress
.getUuid().get());
245 // Version 1: E164 user
247 if (!ownAddress
.getNumber().isPresent() || !theirAddress
.getNumber().isPresent()) {
250 ownId
= ownAddress
.getNumber().get().getBytes();
251 theirId
= theirAddress
.getNumber().get().getBytes();
254 Fingerprint fingerprint
= new NumericFingerprintGenerator(5200).createFor(version
, ownId
, ownIdentityKey
, theirId
, theirIdentityKey
);
255 return fingerprint
.getDisplayableFingerprint().getDisplayText();
258 static class DeviceLinkInfo
{
260 final String deviceIdentifier
;
261 final ECPublicKey deviceKey
;
263 DeviceLinkInfo(final String deviceIdentifier
, final ECPublicKey deviceKey
) {
264 this.deviceIdentifier
= deviceIdentifier
;
265 this.deviceKey
= deviceKey
;