2 Copyright (C) 2015-2020 AsamK and contributors
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 package org
.asamk
.signal
.manager
;
19 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
21 import org
.asamk
.Signal
;
22 import org
.asamk
.signal
.AttachmentInvalidException
;
23 import org
.asamk
.signal
.GroupNotFoundException
;
24 import org
.asamk
.signal
.JsonStickerPack
;
25 import org
.asamk
.signal
.NotAGroupMemberException
;
26 import org
.asamk
.signal
.StickerPackInvalidException
;
27 import org
.asamk
.signal
.TrustLevel
;
28 import org
.asamk
.signal
.UserAlreadyExists
;
29 import org
.asamk
.signal
.storage
.SignalAccount
;
30 import org
.asamk
.signal
.storage
.contacts
.ContactInfo
;
31 import org
.asamk
.signal
.storage
.groups
.GroupInfo
;
32 import org
.asamk
.signal
.storage
.groups
.JsonGroupStore
;
33 import org
.asamk
.signal
.storage
.protocol
.JsonIdentityKeyStore
;
34 import org
.asamk
.signal
.storage
.threads
.ThreadInfo
;
35 import org
.asamk
.signal
.util
.IOUtils
;
36 import org
.asamk
.signal
.util
.Util
;
37 import org
.signal
.libsignal
.metadata
.InvalidMetadataMessageException
;
38 import org
.signal
.libsignal
.metadata
.InvalidMetadataVersionException
;
39 import org
.signal
.libsignal
.metadata
.ProtocolDuplicateMessageException
;
40 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyException
;
41 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyIdException
;
42 import org
.signal
.libsignal
.metadata
.ProtocolInvalidMessageException
;
43 import org
.signal
.libsignal
.metadata
.ProtocolInvalidVersionException
;
44 import org
.signal
.libsignal
.metadata
.ProtocolLegacyMessageException
;
45 import org
.signal
.libsignal
.metadata
.ProtocolNoSessionException
;
46 import org
.signal
.libsignal
.metadata
.ProtocolUntrustedIdentityException
;
47 import org
.signal
.libsignal
.metadata
.SelfSendException
;
48 import org
.signal
.libsignal
.metadata
.certificate
.InvalidCertificateException
;
49 import org
.signal
.zkgroup
.InvalidInputException
;
50 import org
.signal
.zkgroup
.VerificationFailedException
;
51 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
52 import org
.whispersystems
.libsignal
.IdentityKey
;
53 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
54 import org
.whispersystems
.libsignal
.InvalidKeyException
;
55 import org
.whispersystems
.libsignal
.InvalidMessageException
;
56 import org
.whispersystems
.libsignal
.InvalidVersionException
;
57 import org
.whispersystems
.libsignal
.ecc
.Curve
;
58 import org
.whispersystems
.libsignal
.ecc
.ECKeyPair
;
59 import org
.whispersystems
.libsignal
.ecc
.ECPublicKey
;
60 import org
.whispersystems
.libsignal
.state
.PreKeyRecord
;
61 import org
.whispersystems
.libsignal
.state
.SignedPreKeyRecord
;
62 import org
.whispersystems
.libsignal
.util
.KeyHelper
;
63 import org
.whispersystems
.libsignal
.util
.Medium
;
64 import org
.whispersystems
.libsignal
.util
.Pair
;
65 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
66 import org
.whispersystems
.signalservice
.api
.SignalServiceAccountManager
;
67 import org
.whispersystems
.signalservice
.api
.SignalServiceMessagePipe
;
68 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageReceiver
;
69 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
70 import org
.whispersystems
.signalservice
.api
.crypto
.InvalidCiphertextException
;
71 import org
.whispersystems
.signalservice
.api
.crypto
.ProfileCipher
;
72 import org
.whispersystems
.signalservice
.api
.crypto
.SignalServiceCipher
;
73 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
74 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
75 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
76 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
77 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
78 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentPointer
;
79 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
80 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceContent
;
81 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
82 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceEnvelope
;
83 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
84 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
;
85 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
.StickerInfo
;
86 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
87 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
88 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
89 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
90 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
91 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
92 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
93 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
94 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceInfo
;
95 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
96 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
97 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
98 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
99 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
100 import org
.whispersystems
.signalservice
.api
.push
.ContactTokenDetails
;
101 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
102 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.AuthorizationFailedException
;
103 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.EncapsulatedExceptions
;
104 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NetworkFailureException
;
105 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
106 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
107 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
108 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
109 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
110 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
111 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
112 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
113 import org
.whispersystems
.util
.Base64
;
116 import java
.io
.FileInputStream
;
117 import java
.io
.FileNotFoundException
;
118 import java
.io
.FileOutputStream
;
119 import java
.io
.IOException
;
120 import java
.io
.InputStream
;
121 import java
.io
.OutputStream
;
123 import java
.net
.URISyntaxException
;
124 import java
.net
.URLEncoder
;
125 import java
.nio
.file
.Files
;
126 import java
.nio
.file
.Paths
;
127 import java
.nio
.file
.StandardCopyOption
;
128 import java
.util
.ArrayList
;
129 import java
.util
.Arrays
;
130 import java
.util
.Collection
;
131 import java
.util
.Collections
;
132 import java
.util
.Date
;
133 import java
.util
.HashSet
;
134 import java
.util
.LinkedList
;
135 import java
.util
.List
;
136 import java
.util
.Locale
;
137 import java
.util
.Map
;
138 import java
.util
.Objects
;
139 import java
.util
.Set
;
140 import java
.util
.concurrent
.TimeUnit
;
141 import java
.util
.concurrent
.TimeoutException
;
142 import java
.util
.zip
.ZipEntry
;
143 import java
.util
.zip
.ZipFile
;
145 public class Manager
implements Signal
{
147 private static final SignalServiceProfile
.Capabilities capabilities
= new SignalServiceProfile
.Capabilities(false, false);
149 private final String settingsPath
;
150 private final String dataPath
;
151 private final String attachmentsPath
;
152 private final String avatarsPath
;
153 private final SleepTimer timer
= new UptimeSleepTimer();
155 private SignalAccount account
;
156 private String username
;
157 private SignalServiceAccountManager accountManager
;
158 private SignalServiceMessagePipe messagePipe
= null;
159 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
161 public Manager(String username
, String settingsPath
) {
162 this.username
= username
;
163 this.settingsPath
= settingsPath
;
164 this.dataPath
= this.settingsPath
+ "/data";
165 this.attachmentsPath
= this.settingsPath
+ "/attachments";
166 this.avatarsPath
= this.settingsPath
+ "/avatars";
170 public String
getUsername() {
174 private SignalServiceAccountManager
getSignalServiceAccountManager() {
175 return new SignalServiceAccountManager(BaseConfig
.serviceConfiguration
, null, account
.getUsername(), account
.getPassword(), account
.getDeviceId(), BaseConfig
.USER_AGENT
, timer
);
178 private IdentityKey
getIdentity() {
179 return account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
182 public int getDeviceId() {
183 return account
.getDeviceId();
186 private String
getMessageCachePath() {
187 return this.dataPath
+ "/" + username
+ ".d/msg-cache";
190 private String
getMessageCachePath(String sender
) {
191 return getMessageCachePath() + "/" + sender
.replace("/", "_");
194 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
195 String cachePath
= getMessageCachePath(sender
);
196 IOUtils
.createPrivateDirectories(cachePath
);
197 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
200 public boolean userHasKeys() {
201 return account
!= null && account
.getSignalProtocolStore() != null;
204 public void init() throws IOException
{
205 if (!SignalAccount
.userExists(dataPath
, username
)) {
208 account
= SignalAccount
.load(dataPath
, username
);
210 migrateLegacyConfigs();
212 accountManager
= getSignalServiceAccountManager();
214 if (account
.isRegistered() && accountManager
.getPreKeysCount() < BaseConfig
.PREKEY_MINIMUM_COUNT
) {
218 } catch (AuthorizationFailedException e
) {
219 System
.err
.println("Authorization failed, was the number registered elsewhere?");
224 private void migrateLegacyConfigs() {
225 // Copy group avatars that were previously stored in the attachments folder
226 // to the new avatar folder
227 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
228 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
229 File avatarFile
= getGroupAvatarFile(g
.groupId
);
230 File attachmentFile
= getAttachmentFile(g
.getAvatarId());
231 if (!avatarFile
.exists() && attachmentFile
.exists()) {
233 IOUtils
.createPrivateDirectories(avatarsPath
);
234 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
235 } catch (Exception e
) {
240 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
243 if (account
.getProfileKey() == null) {
244 // Old config file, creating new profile key
245 account
.setProfileKey(KeyUtils
.createProfileKey());
250 private void createNewIdentity() throws IOException
{
251 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
252 int registrationId
= KeyHelper
.generateRegistrationId(false);
253 if (username
== null) {
254 account
= SignalAccount
.createTemporaryAccount(identityKey
, registrationId
);
256 ProfileKey profileKey
= KeyUtils
.createProfileKey();
257 account
= SignalAccount
.create(dataPath
, username
, identityKey
, registrationId
, profileKey
);
262 public boolean isRegistered() {
263 return account
!= null && account
.isRegistered();
266 public void register(boolean voiceVerification
) throws IOException
{
267 if (account
== null) {
270 account
.setPassword(KeyUtils
.createPassword());
271 accountManager
= getSignalServiceAccountManager();
273 if (voiceVerification
) {
274 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
276 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
279 account
.setRegistered(false);
283 public void updateAccountAttributes() throws IOException
{
284 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities
);
287 public void setProfileName(String name
) throws IOException
{
288 accountManager
.setProfileName(account
.getProfileKey(), name
);
291 public void setProfileAvatar(File avatar
) throws IOException
{
292 final StreamDetails streamDetails
= Utils
.createStreamDetailsFromFile(avatar
);
293 accountManager
.setProfileAvatar(account
.getProfileKey(), streamDetails
);
294 streamDetails
.getStream().close();
297 public void removeProfileAvatar() throws IOException
{
298 accountManager
.setProfileAvatar(account
.getProfileKey(), null);
301 public void unregister() throws IOException
{
302 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
303 // If this is the master device, other users can't send messages to this number anymore.
304 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
305 accountManager
.setGcmId(Optional
.absent());
307 account
.setRegistered(false);
311 public String
getDeviceLinkUri() throws TimeoutException
, IOException
{
312 if (account
== null) {
315 account
.setPassword(KeyUtils
.createPassword());
316 accountManager
= getSignalServiceAccountManager();
317 String uuid
= accountManager
.getNewDeviceUuid();
319 return Utils
.createDeviceLinkUri(new Utils
.DeviceLinkInfo(uuid
, getIdentity().getPublicKey()));
322 public void finishDeviceLink(String deviceName
) throws IOException
, InvalidKeyException
, TimeoutException
, UserAlreadyExists
{
323 account
.setSignalingKey(KeyUtils
.createSignalingKey());
324 SignalServiceAccountManager
.NewDeviceRegistrationReturn ret
= accountManager
.finishNewDeviceRegistration(account
.getSignalProtocolStore().getIdentityKeyPair(), account
.getSignalingKey(), false, true, account
.getSignalProtocolStore().getLocalRegistrationId(), deviceName
);
326 username
= ret
.getNumber();
327 // TODO do this check before actually registering
328 if (SignalAccount
.userExists(dataPath
, username
)) {
329 throw new UserAlreadyExists(username
, SignalAccount
.getFileName(dataPath
, username
));
332 // Create new account with the synced identity
333 byte[] profileKeyBytes
= ret
.getProfileKey();
334 ProfileKey profileKey
;
335 if (profileKeyBytes
== null) {
336 profileKey
= KeyUtils
.createProfileKey();
339 profileKey
= new ProfileKey(profileKeyBytes
);
340 } catch (InvalidInputException e
) {
341 throw new IOException("Received invalid profileKey", e
);
344 account
= SignalAccount
.createLinkedAccount(dataPath
, username
, account
.getPassword(), ret
.getDeviceId(), ret
.getIdentity(), account
.getSignalProtocolStore().getLocalRegistrationId(), account
.getSignalingKey(), profileKey
);
349 requestSyncContacts();
350 requestSyncBlocked();
351 requestSyncConfiguration();
356 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
357 List
<DeviceInfo
> devices
= accountManager
.getDevices();
358 account
.setMultiDevice(devices
.size() > 1);
363 public void removeLinkedDevices(int deviceId
) throws IOException
{
364 accountManager
.removeDevice(deviceId
);
365 List
<DeviceInfo
> devices
= accountManager
.getDevices();
366 account
.setMultiDevice(devices
.size() > 1);
370 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
371 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
373 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
376 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
377 IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
378 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
380 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
381 account
.setMultiDevice(true);
385 private List
<PreKeyRecord
> generatePreKeys() {
386 List
<PreKeyRecord
> records
= new ArrayList
<>(BaseConfig
.PREKEY_BATCH_SIZE
);
388 final int offset
= account
.getPreKeyIdOffset();
389 for (int i
= 0; i
< BaseConfig
.PREKEY_BATCH_SIZE
; i
++) {
390 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
391 ECKeyPair keyPair
= Curve
.generateKeyPair();
392 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
397 account
.addPreKeys(records
);
403 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
405 ECKeyPair keyPair
= Curve
.generateKeyPair();
406 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
407 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
409 account
.addSignedPreKey(record);
413 } catch (InvalidKeyException e
) {
414 throw new AssertionError(e
);
418 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
419 verificationCode
= verificationCode
.replace("-", "");
420 account
.setSignalingKey(KeyUtils
.createSignalingKey());
421 // TODO make unrestricted unidentified access configurable
422 accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, capabilities
);
424 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
425 account
.setRegistered(true);
426 account
.setRegistrationLockPin(pin
);
432 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
433 if (pin
.isPresent()) {
434 account
.setRegistrationLockPin(pin
.get());
435 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
437 account
.setRegistrationLockPin(null);
438 accountManager
.removeV1Pin();
443 private void refreshPreKeys() throws IOException
{
444 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
445 final IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
446 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
448 accountManager
.setPreKeys(getIdentity(), signedPreKeyRecord
, oneTimePreKeys
);
451 private SignalServiceMessageReceiver
getMessageReceiver() {
452 return new SignalServiceMessageReceiver(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), BaseConfig
.USER_AGENT
, null, timer
);
455 private SignalServiceMessageSender
getMessageSender() {
456 return new SignalServiceMessageSender(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
457 account
.getDeviceId(), account
.getSignalProtocolStore(), BaseConfig
.USER_AGENT
, account
.isMultiDevice(), Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent());
460 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
461 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
466 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
467 } catch (IOException ignored
) {
471 SignalServiceMessageReceiver receiver
= getMessageReceiver();
473 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
474 } catch (VerificationFailedException e
) {
475 throw new AssertionError(e
);
479 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
480 File file
= getGroupAvatarFile(groupId
);
481 if (!file
.exists()) {
482 return Optional
.absent();
485 return Optional
.of(Utils
.createAttachment(file
));
488 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
489 File file
= getContactAvatarFile(number
);
490 if (!file
.exists()) {
491 return Optional
.absent();
494 return Optional
.of(Utils
.createAttachment(file
));
497 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
498 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
500 throw new GroupNotFoundException(groupId
);
502 for (String member
: g
.members
) {
503 if (member
.equals(account
.getUsername())) {
507 throw new NotAGroupMemberException(groupId
, g
.name
);
510 public List
<GroupInfo
> getGroups() {
511 return account
.getGroupStore().getGroups();
515 public void sendGroupMessage(String messageText
, List
<String
> attachments
,
517 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
518 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
519 if (attachments
!= null) {
520 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
522 if (groupId
!= null) {
523 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
526 messageBuilder
.asGroupMessage(group
);
528 ThreadInfo thread
= account
.getThreadStore().getThread(Base64
.encodeBytes(groupId
));
529 if (thread
!= null) {
530 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
533 final GroupInfo g
= getGroupForSending(groupId
);
535 final Collection
<SignalServiceAddress
> membersSend
= getSignalServiceAddresses(g
.members
);
536 // Don't send group message to ourself
537 membersSend
.remove(account
.getSelfAddress());
538 sendMessageLegacy(messageBuilder
, membersSend
);
541 public void sendGroupMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
542 long targetSentTimestamp
, byte[] groupId
)
543 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
544 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
545 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
546 .withReaction(reaction
)
547 .withProfileKey(account
.getProfileKey().serialize());
548 if (groupId
!= null) {
549 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
552 messageBuilder
.asGroupMessage(group
);
554 final GroupInfo g
= getGroupForSending(groupId
);
555 final Collection
<SignalServiceAddress
> membersSend
= getSignalServiceAddresses(g
.members
);
556 // Don't send group message to ourself
557 membersSend
.remove(account
.getSelfAddress());
558 sendMessageLegacy(messageBuilder
, membersSend
);
561 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
, InvalidNumberException
{
562 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
566 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
567 .asGroupMessage(group
);
569 final GroupInfo g
= getGroupForSending(groupId
);
570 g
.members
.remove(account
.getUsername());
571 account
.getGroupStore().updateGroup(g
);
573 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(g
.members
));
576 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<String
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
578 if (groupId
== null) {
580 g
= new GroupInfo(KeyUtils
.createGroupId());
581 g
.members
.add(account
.getUsername());
583 g
= getGroupForSending(groupId
);
590 if (members
!= null) {
591 Set
<String
> newMembers
= new HashSet
<>();
592 for (String member
: members
) {
593 member
= Utils
.canonicalizeNumber(member
, account
.getUsername());
594 if (g
.members
.contains(member
)) {
597 newMembers
.add(member
);
598 g
.members
.add(member
);
600 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newMembers
);
601 if (contacts
.size() != newMembers
.size()) {
602 // Some of the new members are not registered on Signal
603 for (ContactTokenDetails contact
: contacts
) {
604 newMembers
.remove(contact
.getNumber());
606 System
.err
.println("Failed to add members " + Util
.join(", ", newMembers
) + " to group: Not registered on Signal");
607 System
.err
.println("Aborting…");
612 if (avatarFile
!= null) {
613 IOUtils
.createPrivateDirectories(avatarsPath
);
614 File aFile
= getGroupAvatarFile(g
.groupId
);
615 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
618 account
.getGroupStore().updateGroup(g
);
620 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
622 final Collection
<SignalServiceAddress
> membersSend
= getSignalServiceAddresses(g
.members
);
623 // Don't send group message to ourself
624 membersSend
.remove(account
.getSelfAddress());
625 sendMessageLegacy(messageBuilder
, membersSend
);
629 private void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
630 if (groupId
== null) {
633 GroupInfo g
= getGroupForSending(groupId
);
635 if (!g
.members
.contains(recipient
.getNumber().get())) {
639 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
641 // Send group message only to the recipient who requested it
642 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
645 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
646 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
649 .withMembers(new ArrayList
<>(g
.getMembers()));
651 File aFile
= getGroupAvatarFile(g
.groupId
);
652 if (aFile
.exists()) {
654 group
.withAvatar(Utils
.createAttachment(aFile
));
655 } catch (IOException e
) {
656 throw new AttachmentInvalidException(aFile
.toString(), e
);
660 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
661 .asGroupMessage(group
.build());
663 ThreadInfo thread
= account
.getThreadStore().getThread(Base64
.encodeBytes(g
.groupId
));
664 if (thread
!= null) {
665 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
668 return messageBuilder
;
671 private void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
672 if (groupId
== null) {
676 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
679 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
680 .asGroupMessage(group
.build());
682 ThreadInfo thread
= account
.getThreadStore().getThread(Base64
.encodeBytes(groupId
));
683 if (thread
!= null) {
684 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
687 // Send group info request message to the recipient who sent us a message with this groupId
688 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
692 public void sendMessage(String message
, List
<String
> attachments
, String recipient
)
693 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
, InvalidNumberException
{
694 List
<String
> recipients
= new ArrayList
<>(1);
695 recipients
.add(recipient
);
696 sendMessage(message
, attachments
, recipients
);
700 public void sendMessage(String messageText
, List
<String
> attachments
,
701 List
<String
> recipients
)
702 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
703 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
704 if (attachments
!= null) {
705 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
707 // Upload attachments here, so we only upload once even for multiple recipients
708 SignalServiceMessageSender messageSender
= getMessageSender();
709 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
710 for (SignalServiceAttachment attachment
: attachmentStreams
) {
711 if (attachment
.isStream()) {
712 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
713 } else if (attachment
.isPointer()) {
714 attachmentPointers
.add(attachment
.asPointer());
718 messageBuilder
.withAttachments(attachmentPointers
);
720 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
721 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
724 public void sendMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
725 long targetSentTimestamp
, List
<String
> recipients
)
726 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
727 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
728 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
729 .withReaction(reaction
)
730 .withProfileKey(account
.getProfileKey().serialize());
731 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
735 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
736 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
737 .asEndSessionMessage();
739 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
743 public String
getContactName(String number
) throws InvalidNumberException
{
744 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
745 ContactInfo contact
= account
.getContactStore().getContact(canonicalizedNumber
);
746 if (contact
== null) {
754 public void setContactName(String number
, String name
) throws InvalidNumberException
{
755 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
756 ContactInfo contact
= account
.getContactStore().getContact(canonicalizedNumber
);
757 if (contact
== null) {
758 contact
= new ContactInfo();
759 contact
.number
= canonicalizedNumber
;
760 System
.err
.println("Add contact " + canonicalizedNumber
+ " named " + name
);
762 System
.err
.println("Updating contact " + canonicalizedNumber
+ " name " + contact
.name
+ " -> " + name
);
765 account
.getContactStore().updateContact(contact
);
770 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
771 number
= Utils
.canonicalizeNumber(number
, account
.getUsername());
772 ContactInfo contact
= account
.getContactStore().getContact(number
);
773 if (contact
== null) {
774 contact
= new ContactInfo();
775 contact
.number
= number
;
776 System
.err
.println("Adding and " + (blocked ?
"blocking" : "unblocking") + " contact " + number
);
778 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " contact " + number
);
780 contact
.blocked
= blocked
;
781 account
.getContactStore().updateContact(contact
);
786 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
787 GroupInfo group
= getGroup(groupId
);
789 throw new GroupNotFoundException(groupId
);
791 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " group " + Base64
.encodeBytes(groupId
));
792 group
.blocked
= blocked
;
793 account
.getGroupStore().updateGroup(group
);
799 public List
<byte[]> getGroupIds() {
800 List
<GroupInfo
> groups
= getGroups();
801 List
<byte[]> ids
= new ArrayList
<>(groups
.size());
802 for (GroupInfo group
: groups
) {
803 ids
.add(group
.groupId
);
809 public String
getGroupName(byte[] groupId
) {
810 GroupInfo group
= getGroup(groupId
);
819 public List
<String
> getGroupMembers(byte[] groupId
) {
820 GroupInfo group
= getGroup(groupId
);
822 return new ArrayList
<>();
824 return new ArrayList
<>(group
.members
);
829 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
830 if (groupId
.length
== 0) {
833 if (name
.isEmpty()) {
836 if (members
.size() == 0) {
839 if (avatar
.isEmpty()) {
842 return sendUpdateGroupMessage(groupId
, name
, members
, avatar
);
846 * Change the expiration timer for a thread (number of groupId)
848 public void setExpirationTimer(String numberOrGroupId
, int messageExpirationTimer
) {
849 ThreadInfo thread
= account
.getThreadStore().getThread(numberOrGroupId
);
850 thread
.messageExpirationTime
= messageExpirationTimer
;
851 account
.getThreadStore().updateThread(thread
);
855 * Upload the sticker pack from path.
857 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
858 * @return if successful, returns the URL to install the sticker pack in the signal app
860 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
861 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
863 SignalServiceMessageSender messageSender
= getMessageSender();
865 byte[] packKey
= KeyUtils
.createStickerUploadKey();
866 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
869 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, "utf-8") + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), "utf-8"))
871 } catch (URISyntaxException e
) {
872 throw new AssertionError(e
);
876 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
878 String rootPath
= null;
880 final File file
= new File(path
);
881 if (file
.getName().endsWith(".zip")) {
882 zip
= new ZipFile(file
);
883 } else if (file
.getName().equals("manifest.json")) {
884 rootPath
= file
.getParent();
886 throw new StickerPackInvalidException("Could not find manifest.json");
889 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
891 if (pack
.stickers
== null) {
892 throw new StickerPackInvalidException("Must set a 'stickers' field.");
895 if (pack
.stickers
.isEmpty()) {
896 throw new StickerPackInvalidException("Must include stickers.");
899 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
900 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
901 if (sticker
.file
== null) {
902 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
905 Pair
<InputStream
, Long
> data
;
907 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
908 } catch (IOException ignored
) {
909 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
912 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""));
913 stickers
.add(stickerInfo
);
916 StickerInfo cover
= null;
917 if (pack
.cover
!= null) {
918 if (pack
.cover
.file
== null) {
919 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
922 Pair
<InputStream
, Long
> data
;
924 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
925 } catch (IOException ignored
) {
926 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
929 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""));
932 return new SignalServiceStickerManifestUpload(
939 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
940 InputStream inputStream
;
942 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
944 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
946 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
949 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
951 final ZipEntry entry
= zip
.getEntry(subfile
);
952 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
954 final File file
= new File(rootPath
, subfile
);
955 return new Pair
<>(new FileInputStream(file
), file
.length());
959 private void requestSyncGroups() throws IOException
{
960 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
961 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
963 sendSyncMessage(message
);
964 } catch (UntrustedIdentityException e
) {
969 private void requestSyncContacts() throws IOException
{
970 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
971 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
973 sendSyncMessage(message
);
974 } catch (UntrustedIdentityException e
) {
979 private void requestSyncBlocked() throws IOException
{
980 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
981 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
983 sendSyncMessage(message
);
984 } catch (UntrustedIdentityException e
) {
989 private void requestSyncConfiguration() throws IOException
{
990 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
991 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
993 sendSyncMessage(message
);
994 } catch (UntrustedIdentityException e
) {
999 private byte[] getSenderCertificate() throws IOException
{
1000 byte[] certificate
= accountManager
.getSenderCertificate();
1001 // TODO cache for a day
1005 private byte[] getSelfUnidentifiedAccessKey() {
1006 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
1009 private static SignalProfile
decryptProfile(SignalServiceProfile encryptedProfile
, ProfileKey profileKey
) throws IOException
{
1010 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
1012 return new SignalProfile(
1013 encryptedProfile
.getIdentityKey(),
1014 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
1015 encryptedProfile
.getAvatar(),
1016 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
1017 encryptedProfile
.isUnrestrictedUnidentifiedAccess()
1019 } catch (InvalidCiphertextException e
) {
1024 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) throws IOException
{
1025 ContactInfo contact
= account
.getContactStore().getContact(recipient
.getNumber().get());
1026 if (contact
== null || contact
.profileKey
== null) {
1029 ProfileKey theirProfileKey
;
1031 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
1032 } catch (InvalidInputException e
) {
1033 throw new AssertionError(e
);
1035 SignalProfile targetProfile
= decryptProfile(getRecipientProfile(recipient
, Optional
.absent()), theirProfileKey
);
1037 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1041 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1042 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1045 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1048 private Optional
<UnidentifiedAccessPair
> getAccessForSync() throws IOException
{
1049 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1050 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1052 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1053 return Optional
.absent();
1057 return Optional
.of(new UnidentifiedAccessPair(
1058 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1059 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1061 } catch (InvalidCertificateException e
) {
1062 return Optional
.absent();
1066 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) throws IOException
{
1067 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1068 for (SignalServiceAddress recipient
: recipients
) {
1069 result
.add(getAccessFor(recipient
));
1074 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) throws IOException
{
1075 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1076 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1077 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1079 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1080 return Optional
.absent();
1084 return Optional
.of(new UnidentifiedAccessPair(
1085 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1086 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1088 } catch (InvalidCertificateException e
) {
1089 return Optional
.absent();
1093 private void sendSyncMessage(SignalServiceSyncMessage message
)
1094 throws IOException
, UntrustedIdentityException
{
1095 SignalServiceMessageSender messageSender
= getMessageSender();
1097 messageSender
.sendMessage(message
, getAccessForSync());
1098 } catch (UntrustedIdentityException e
) {
1099 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1105 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1107 private void sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1108 throws EncapsulatedExceptions
, IOException
{
1109 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1111 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1112 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1113 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1115 for (SendMessageResult result
: results
) {
1116 if (result
.isUnregisteredFailure()) {
1117 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getNumber().get(), null));
1118 } else if (result
.isNetworkFailure()) {
1119 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getNumber().get(), null));
1120 } else if (result
.getIdentityFailure() != null) {
1121 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getNumber().get(), result
.getIdentityFailure().getIdentityKey()));
1124 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1125 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1129 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1130 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1131 final String username
= account
.getUsername();
1133 for (String number
: numbers
) {
1134 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, username
);
1135 if (canonicalizedNumber
.equals(username
)) {
1136 signalServiceAddresses
.add(account
.getSelfAddress());
1138 // TODO get corresponding uuid
1139 signalServiceAddresses
.add(new SignalServiceAddress(null, canonicalizedNumber
));
1142 return signalServiceAddresses
;
1145 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1146 throws IOException
{
1147 if (messagePipe
== null) {
1148 messagePipe
= getMessageReceiver().createMessagePipe();
1150 if (unidentifiedMessagePipe
== null) {
1151 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1153 SignalServiceDataMessage message
= null;
1155 SignalServiceMessageSender messageSender
= getMessageSender();
1157 message
= messageBuilder
.build();
1158 if (message
.getGroupInfo().isPresent()) {
1160 final boolean isRecipientUpdate
= false;
1161 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1162 for (SendMessageResult r
: result
) {
1163 if (r
.getIdentityFailure() != null) {
1164 account
.getSignalProtocolStore().saveIdentity(r
.getAddress().getNumber().get(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1168 } catch (UntrustedIdentityException e
) {
1169 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1170 return Collections
.emptyList();
1172 } else if (recipients
.size() == 1 && recipients
.contains(account
.getSelfAddress())) {
1173 SignalServiceAddress recipient
= account
.getSelfAddress();
1174 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1175 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1176 message
.getTimestamp(),
1178 message
.getExpiresInSeconds(),
1179 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1181 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1183 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1185 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1186 } catch (UntrustedIdentityException e
) {
1187 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1188 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1192 // Send to all individually, so sync messages are sent correctly
1193 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1194 for (SignalServiceAddress address
: recipients
) {
1195 ThreadInfo thread
= account
.getThreadStore().getThread(address
.getNumber().get());
1196 if (thread
!= null) {
1197 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
1199 messageBuilder
.withExpiration(0);
1201 message
= messageBuilder
.build();
1203 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1204 results
.add(result
);
1205 } catch (UntrustedIdentityException e
) {
1206 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1207 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1213 if (message
!= null && message
.isEndSession()) {
1214 for (SignalServiceAddress recipient
: recipients
) {
1215 handleEndSession(recipient
.getNumber().get());
1222 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, ProtocolUntrustedIdentityException
, SelfSendException
, UnsupportedDataMessageException
{
1223 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1225 return cipher
.decrypt(envelope
);
1226 } catch (ProtocolUntrustedIdentityException e
) {
1227 // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else
1228 // account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
1233 private void handleEndSession(String source
) {
1234 account
.getSignalProtocolStore().deleteAllSessions(source
);
1237 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1239 if (message
.getGroupInfo().isPresent()) {
1240 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1241 threadId
= Base64
.encodeBytes(groupInfo
.getGroupId());
1242 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1243 switch (groupInfo
.getType()) {
1245 if (group
== null) {
1246 group
= new GroupInfo(groupInfo
.getGroupId());
1249 if (groupInfo
.getAvatar().isPresent()) {
1250 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1251 if (avatar
.isPointer()) {
1253 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1254 } catch (IOException
| InvalidMessageException e
) {
1255 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getId() + "): " + e
.getMessage());
1260 if (groupInfo
.getName().isPresent()) {
1261 group
.name
= groupInfo
.getName().get();
1264 if (groupInfo
.getMembers().isPresent()) {
1265 group
.addMembers(groupInfo
.getMembers().get());
1268 account
.getGroupStore().updateGroup(group
);
1271 if (group
== null) {
1273 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1274 } catch (IOException
| EncapsulatedExceptions e
) {
1275 e
.printStackTrace();
1280 if (group
== null) {
1282 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1283 } catch (IOException
| EncapsulatedExceptions e
) {
1284 e
.printStackTrace();
1287 group
.members
.remove(source
.getNumber().get());
1288 account
.getGroupStore().updateGroup(group
);
1292 if (group
!= null) {
1294 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1295 } catch (IOException
| EncapsulatedExceptions e
) {
1296 e
.printStackTrace();
1297 } catch (NotAGroupMemberException e
) {
1298 // We have left this group, so don't send a group update message
1305 threadId
= destination
.getNumber().get();
1307 threadId
= source
.getNumber().get();
1310 if (message
.isEndSession()) {
1311 handleEndSession(isSync ? destination
.getNumber().get() : source
.getNumber().get());
1313 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1314 ThreadInfo thread
= account
.getThreadStore().getThread(threadId
);
1315 if (thread
== null) {
1316 thread
= new ThreadInfo();
1317 thread
.id
= threadId
;
1319 if (thread
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1320 thread
.messageExpirationTime
= message
.getExpiresInSeconds();
1321 account
.getThreadStore().updateThread(thread
);
1324 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1325 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1326 if (attachment
.isPointer()) {
1328 retrieveAttachment(attachment
.asPointer());
1329 } catch (IOException
| InvalidMessageException e
) {
1330 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getId() + "): " + e
.getMessage());
1335 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1336 if (source
.equals(account
.getSelfAddress())) {
1338 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1339 } catch (InvalidInputException ignored
) {
1342 ContactInfo contact
= account
.getContactStore().getContact(source
.getNumber().get());
1343 if (contact
== null) {
1344 contact
= new ContactInfo();
1345 contact
.number
= source
.getNumber().get();
1347 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1348 account
.getContactStore().updateContact(contact
);
1350 if (message
.getPreviews().isPresent()) {
1351 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1352 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1353 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1354 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1356 retrieveAttachment(attachment
);
1357 } catch (IOException
| InvalidMessageException e
) {
1358 System
.err
.println("Failed to retrieve attachment (" + attachment
.getId() + "): " + e
.getMessage());
1365 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1366 final File cachePath
= new File(getMessageCachePath());
1367 if (!cachePath
.exists()) {
1370 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1371 if (!dir
.isDirectory()) {
1375 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1376 if (!fileEntry
.isFile()) {
1379 SignalServiceEnvelope envelope
;
1381 envelope
= Utils
.loadEnvelope(fileEntry
);
1382 if (envelope
== null) {
1385 } catch (IOException e
) {
1386 e
.printStackTrace();
1389 SignalServiceContent content
= null;
1390 if (!envelope
.isReceipt()) {
1392 content
= decryptMessage(envelope
);
1393 } catch (Exception e
) {
1396 handleMessage(envelope
, content
, ignoreAttachments
);
1399 handler
.handleMessage(envelope
, content
, null);
1401 Files
.delete(fileEntry
.toPath());
1402 } catch (IOException e
) {
1403 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1406 // Try to delete directory if empty
1411 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1412 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1413 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1416 if (messagePipe
== null) {
1417 messagePipe
= messageReceiver
.createMessagePipe();
1421 SignalServiceEnvelope envelope
;
1422 SignalServiceContent content
= null;
1423 Exception exception
= null;
1424 final long now
= new Date().getTime();
1426 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1427 // store message on disk, before acknowledging receipt to the server
1429 File cacheFile
= getMessageCacheFile(envelope1
.getSourceE164().get(), now
, envelope1
.getTimestamp());
1430 Utils
.storeEnvelope(envelope1
, cacheFile
);
1431 } catch (IOException e
) {
1432 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1435 } catch (TimeoutException e
) {
1436 if (returnOnTimeout
)
1439 } catch (InvalidVersionException e
) {
1440 System
.err
.println("Ignoring error: " + e
.getMessage());
1443 if (!envelope
.isReceipt()) {
1445 content
= decryptMessage(envelope
);
1446 } catch (Exception e
) {
1449 handleMessage(envelope
, content
, ignoreAttachments
);
1452 if (!isMessageBlocked(envelope
, content
)) {
1453 handler
.handleMessage(envelope
, content
, exception
);
1455 if (!(exception
instanceof ProtocolUntrustedIdentityException
)) {
1456 File cacheFile
= null;
1458 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1459 Files
.delete(cacheFile
.toPath());
1460 // Try to delete directory if empty
1461 new File(getMessageCachePath()).delete();
1462 } catch (IOException e
) {
1463 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1468 if (messagePipe
!= null) {
1469 messagePipe
.shutdown();
1475 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1476 SignalServiceAddress source
;
1477 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1478 source
= envelope
.getSourceAddress();
1479 } else if (content
!= null) {
1480 source
= content
.getSender();
1484 ContactInfo sourceContact
= getContact(source
.getNumber().get());
1485 if (sourceContact
!= null && sourceContact
.blocked
) {
1489 if (content
!= null && content
.getDataMessage().isPresent()) {
1490 SignalServiceDataMessage message
= content
.getDataMessage().get();
1491 if (message
.getGroupInfo().isPresent()) {
1492 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1493 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1494 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1502 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1503 if (content
!= null) {
1504 SignalServiceAddress sender
;
1505 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1506 sender
= envelope
.getSourceAddress();
1508 sender
= content
.getSender();
1510 if (content
.getDataMessage().isPresent()) {
1511 SignalServiceDataMessage message
= content
.getDataMessage().get();
1512 handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
);
1514 if (content
.getSyncMessage().isPresent()) {
1515 account
.setMultiDevice(true);
1516 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1517 if (syncMessage
.getSent().isPresent()) {
1518 SentTranscriptMessage message
= syncMessage
.getSent().get();
1519 handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
);
1521 if (syncMessage
.getRequest().isPresent()) {
1522 RequestMessage rm
= syncMessage
.getRequest().get();
1523 if (rm
.isContactsRequest()) {
1526 } catch (UntrustedIdentityException
| IOException e
) {
1527 e
.printStackTrace();
1530 if (rm
.isGroupsRequest()) {
1533 } catch (UntrustedIdentityException
| IOException e
) {
1534 e
.printStackTrace();
1537 if (rm
.isBlockedListRequest()) {
1540 } catch (UntrustedIdentityException
| IOException e
) {
1541 e
.printStackTrace();
1544 // TODO Handle rm.isConfigurationRequest();
1546 if (syncMessage
.getGroups().isPresent()) {
1547 File tmpFile
= null;
1549 tmpFile
= IOUtils
.createTempFile();
1550 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1551 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1553 while ((g
= s
.read()) != null) {
1554 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1555 if (syncGroup
== null) {
1556 syncGroup
= new GroupInfo(g
.getId());
1558 if (g
.getName().isPresent()) {
1559 syncGroup
.name
= g
.getName().get();
1561 syncGroup
.addMembers(g
.getMembers());
1562 if (!g
.isActive()) {
1563 syncGroup
.members
.remove(account
.getUsername());
1565 // Add ourself to the member set as it's marked as active
1566 syncGroup
.members
.add(account
.getUsername());
1568 syncGroup
.blocked
= g
.isBlocked();
1569 if (g
.getColor().isPresent()) {
1570 syncGroup
.color
= g
.getColor().get();
1573 if (g
.getAvatar().isPresent()) {
1574 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1576 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1577 syncGroup
.archived
= g
.isArchived();
1578 account
.getGroupStore().updateGroup(syncGroup
);
1581 } catch (Exception e
) {
1582 e
.printStackTrace();
1584 if (tmpFile
!= null) {
1586 Files
.delete(tmpFile
.toPath());
1587 } catch (IOException e
) {
1588 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1593 if (syncMessage
.getBlockedList().isPresent()) {
1594 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1595 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1596 if (address
.getNumber().isPresent()) {
1598 setContactBlocked(address
.getNumber().get(), true);
1599 } catch (InvalidNumberException e
) {
1600 e
.printStackTrace();
1604 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1606 setGroupBlocked(groupId
, true);
1607 } catch (GroupNotFoundException e
) {
1608 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1612 if (syncMessage
.getContacts().isPresent()) {
1613 File tmpFile
= null;
1615 tmpFile
= IOUtils
.createTempFile();
1616 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1617 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1618 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1619 if (contactsMessage
.isComplete()) {
1620 account
.getContactStore().clear();
1623 while ((c
= s
.read()) != null) {
1624 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1625 account
.setProfileKey(c
.getProfileKey().get());
1627 ContactInfo contact
= account
.getContactStore().getContact(c
.getAddress().getNumber().get());
1628 if (contact
== null) {
1629 contact
= new ContactInfo();
1630 contact
.number
= c
.getAddress().getNumber().get();
1632 if (c
.getName().isPresent()) {
1633 contact
.name
= c
.getName().get();
1635 if (c
.getColor().isPresent()) {
1636 contact
.color
= c
.getColor().get();
1638 if (c
.getProfileKey().isPresent()) {
1639 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1641 if (c
.getVerified().isPresent()) {
1642 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1643 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1645 if (c
.getExpirationTimer().isPresent()) {
1646 ThreadInfo thread
= account
.getThreadStore().getThread(c
.getAddress().getNumber().get());
1647 if (thread
== null) {
1648 thread
= new ThreadInfo();
1649 thread
.id
= c
.getAddress().getNumber().get();
1651 thread
.messageExpirationTime
= c
.getExpirationTimer().get();
1652 account
.getThreadStore().updateThread(thread
);
1654 contact
.blocked
= c
.isBlocked();
1655 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1656 contact
.archived
= c
.isArchived();
1657 account
.getContactStore().updateContact(contact
);
1659 if (c
.getAvatar().isPresent()) {
1660 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1664 } catch (Exception e
) {
1665 e
.printStackTrace();
1667 if (tmpFile
!= null) {
1669 Files
.delete(tmpFile
.toPath());
1670 } catch (IOException e
) {
1671 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1676 if (syncMessage
.getVerified().isPresent()) {
1677 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1678 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1680 if (syncMessage
.getConfiguration().isPresent()) {
1687 private File
getContactAvatarFile(String number
) {
1688 return new File(avatarsPath
, "contact-" + number
);
1691 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
{
1692 IOUtils
.createPrivateDirectories(avatarsPath
);
1693 if (attachment
.isPointer()) {
1694 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1695 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1697 SignalServiceAttachmentStream stream
= attachment
.asStream();
1698 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1702 private File
getGroupAvatarFile(byte[] groupId
) {
1703 return new File(avatarsPath
, "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1706 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
{
1707 IOUtils
.createPrivateDirectories(avatarsPath
);
1708 if (attachment
.isPointer()) {
1709 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1710 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1712 SignalServiceAttachmentStream stream
= attachment
.asStream();
1713 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1717 public File
getAttachmentFile(long attachmentId
) {
1718 return new File(attachmentsPath
, attachmentId
+ "");
1721 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
{
1722 IOUtils
.createPrivateDirectories(attachmentsPath
);
1723 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getId()), true);
1726 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
{
1727 if (storePreview
&& pointer
.getPreview().isPresent()) {
1728 File previewFile
= new File(outputFile
+ ".preview");
1729 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1730 byte[] preview
= pointer
.getPreview().get();
1731 output
.write(preview
, 0, preview
.length
);
1732 } catch (FileNotFoundException e
) {
1733 e
.printStackTrace();
1738 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1740 File tmpFile
= IOUtils
.createTempFile();
1741 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
)) {
1742 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1743 byte[] buffer
= new byte[4096];
1746 while ((read
= input
.read(buffer
)) != -1) {
1747 output
.write(buffer
, 0, read
);
1749 } catch (FileNotFoundException e
) {
1750 e
.printStackTrace();
1755 Files
.delete(tmpFile
.toPath());
1756 } catch (IOException e
) {
1757 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1763 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
{
1764 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1765 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
);
1769 public boolean isRemote() {
1773 private void sendGroups() throws IOException
, UntrustedIdentityException
{
1774 File groupsFile
= IOUtils
.createTempFile();
1777 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1778 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1779 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1780 ThreadInfo info
= account
.getThreadStore().getThread(Base64
.encodeBytes(record.groupId
));
1781 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1782 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1783 record.members
.contains(account
.getUsername()), Optional
.fromNullable(info
!= null ? info
.messageExpirationTime
: null),
1784 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1788 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1789 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1790 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1791 .withStream(groupsFileStream
)
1792 .withContentType("application/octet-stream")
1793 .withLength(groupsFile
.length())
1796 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1801 Files
.delete(groupsFile
.toPath());
1802 } catch (IOException e
) {
1803 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1808 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1809 File contactsFile
= IOUtils
.createTempFile();
1812 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1813 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1814 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1815 VerifiedMessage verifiedMessage
= null;
1816 ThreadInfo info
= account
.getThreadStore().getThread(record.number
);
1817 if (getIdentities().containsKey(record.number
)) {
1818 JsonIdentityKeyStore
.Identity currentIdentity
= null;
1819 for (JsonIdentityKeyStore
.Identity id
: getIdentities().get(record.number
)) {
1820 if (currentIdentity
== null || id
.getDateAdded().after(currentIdentity
.getDateAdded())) {
1821 currentIdentity
= id
;
1824 if (currentIdentity
!= null) {
1825 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1829 ProfileKey profileKey
= null;
1831 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1832 } catch (InvalidInputException ignored
) {
1834 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1835 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1836 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1837 Optional
.fromNullable(info
!= null ? info
.messageExpirationTime
: null),
1838 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1841 if (account
.getProfileKey() != null) {
1842 // Send our own profile key as well
1843 out
.write(new DeviceContact(account
.getSelfAddress(),
1844 Optional
.absent(), Optional
.absent(),
1845 Optional
.absent(), Optional
.absent(),
1846 Optional
.of(account
.getProfileKey()),
1847 false, Optional
.absent(), Optional
.absent(), false));
1851 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1852 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1853 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1854 .withStream(contactsFileStream
)
1855 .withContentType("application/octet-stream")
1856 .withLength(contactsFile
.length())
1859 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1864 Files
.delete(contactsFile
.toPath());
1865 } catch (IOException e
) {
1866 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1871 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1872 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1873 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1874 if (record.blocked
) {
1875 addresses
.add(record.getAddress());
1878 List
<byte[]> groupIds
= new ArrayList
<>();
1879 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1880 if (record.blocked
) {
1881 groupIds
.add(record.groupId
);
1884 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1887 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1888 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1889 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1892 public List
<ContactInfo
> getContacts() {
1893 return account
.getContactStore().getContacts();
1896 public ContactInfo
getContact(String number
) {
1897 return account
.getContactStore().getContact(number
);
1900 public GroupInfo
getGroup(byte[] groupId
) {
1901 return account
.getGroupStore().getGroup(groupId
);
1904 public Map
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities() {
1905 return account
.getSignalProtocolStore().getIdentities();
1908 public Pair
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities(String number
) throws InvalidNumberException
{
1909 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
1910 return new Pair
<>(canonicalizedNumber
, account
.getSignalProtocolStore().getIdentities(canonicalizedNumber
));
1914 * Trust this the identity with this fingerprint
1916 * @param name username of the identity
1917 * @param fingerprint Fingerprint
1919 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) {
1920 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1924 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1925 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1929 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1931 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1932 } catch (IOException
| UntrustedIdentityException e
) {
1933 e
.printStackTrace();
1942 * Trust this the identity with this safety number
1944 * @param name username of the identity
1945 * @param safetyNumber Safety number
1947 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) {
1948 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1952 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1953 if (!safetyNumber
.equals(computeSafetyNumber(name
, id
.getIdentityKey()))) {
1957 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1959 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1960 } catch (IOException
| UntrustedIdentityException e
) {
1961 e
.printStackTrace();
1970 * Trust all keys of this identity without verification
1972 * @param name username of the identity
1974 public boolean trustIdentityAllKeys(String name
) {
1975 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1979 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1980 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
1981 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1983 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1984 } catch (IOException
| UntrustedIdentityException e
) {
1985 e
.printStackTrace();
1993 public String
computeSafetyNumber(String theirUsername
, IdentityKey theirIdentityKey
) {
1994 return Utils
.computeSafetyNumber(account
.getUsername(), getIdentity(), theirUsername
, theirIdentityKey
);
1997 public interface ReceiveMessageHandler
{
1999 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);