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 public SignalServiceAddress
getSelfAddress() {
175 return account
.getSelfAddress();
178 private SignalServiceAccountManager
getSignalServiceAccountManager() {
179 return new SignalServiceAccountManager(BaseConfig
.serviceConfiguration
, null, account
.getUsername(), account
.getPassword(), account
.getDeviceId(), BaseConfig
.USER_AGENT
, timer
);
182 private IdentityKey
getIdentity() {
183 return account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
186 public int getDeviceId() {
187 return account
.getDeviceId();
190 private String
getMessageCachePath() {
191 return this.dataPath
+ "/" + username
+ ".d/msg-cache";
194 private String
getMessageCachePath(String sender
) {
195 return getMessageCachePath() + "/" + sender
.replace("/", "_");
198 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
199 String cachePath
= getMessageCachePath(sender
);
200 IOUtils
.createPrivateDirectories(cachePath
);
201 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
204 public boolean userHasKeys() {
205 return account
!= null && account
.getSignalProtocolStore() != null;
208 public void init() throws IOException
{
209 if (!SignalAccount
.userExists(dataPath
, username
)) {
212 account
= SignalAccount
.load(dataPath
, username
);
214 migrateLegacyConfigs();
216 accountManager
= getSignalServiceAccountManager();
218 if (account
.isRegistered() && accountManager
.getPreKeysCount() < BaseConfig
.PREKEY_MINIMUM_COUNT
) {
222 } catch (AuthorizationFailedException e
) {
223 System
.err
.println("Authorization failed, was the number registered elsewhere?");
228 private void migrateLegacyConfigs() {
229 // Copy group avatars that were previously stored in the attachments folder
230 // to the new avatar folder
231 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
232 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
233 File avatarFile
= getGroupAvatarFile(g
.groupId
);
234 File attachmentFile
= getAttachmentFile(g
.getAvatarId());
235 if (!avatarFile
.exists() && attachmentFile
.exists()) {
237 IOUtils
.createPrivateDirectories(avatarsPath
);
238 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
239 } catch (Exception e
) {
244 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
247 if (account
.getProfileKey() == null) {
248 // Old config file, creating new profile key
249 account
.setProfileKey(KeyUtils
.createProfileKey());
254 private void createNewIdentity() throws IOException
{
255 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
256 int registrationId
= KeyHelper
.generateRegistrationId(false);
257 if (username
== null) {
258 account
= SignalAccount
.createTemporaryAccount(identityKey
, registrationId
);
260 ProfileKey profileKey
= KeyUtils
.createProfileKey();
261 account
= SignalAccount
.create(dataPath
, username
, identityKey
, registrationId
, profileKey
);
266 public boolean isRegistered() {
267 return account
!= null && account
.isRegistered();
270 public void register(boolean voiceVerification
) throws IOException
{
271 if (account
== null) {
274 account
.setPassword(KeyUtils
.createPassword());
275 accountManager
= getSignalServiceAccountManager();
277 if (voiceVerification
) {
278 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
280 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
283 account
.setRegistered(false);
287 public void updateAccountAttributes() throws IOException
{
288 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities
);
291 public void setProfileName(String name
) throws IOException
{
292 accountManager
.setProfileName(account
.getProfileKey(), name
);
295 public void setProfileAvatar(File avatar
) throws IOException
{
296 final StreamDetails streamDetails
= Utils
.createStreamDetailsFromFile(avatar
);
297 accountManager
.setProfileAvatar(account
.getProfileKey(), streamDetails
);
298 streamDetails
.getStream().close();
301 public void removeProfileAvatar() throws IOException
{
302 accountManager
.setProfileAvatar(account
.getProfileKey(), null);
305 public void unregister() throws IOException
{
306 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
307 // If this is the master device, other users can't send messages to this number anymore.
308 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
309 accountManager
.setGcmId(Optional
.absent());
311 account
.setRegistered(false);
315 public String
getDeviceLinkUri() throws TimeoutException
, IOException
{
316 if (account
== null) {
319 account
.setPassword(KeyUtils
.createPassword());
320 accountManager
= getSignalServiceAccountManager();
321 String uuid
= accountManager
.getNewDeviceUuid();
323 return Utils
.createDeviceLinkUri(new Utils
.DeviceLinkInfo(uuid
, getIdentity().getPublicKey()));
326 public void finishDeviceLink(String deviceName
) throws IOException
, InvalidKeyException
, TimeoutException
, UserAlreadyExists
{
327 account
.setSignalingKey(KeyUtils
.createSignalingKey());
328 SignalServiceAccountManager
.NewDeviceRegistrationReturn ret
= accountManager
.finishNewDeviceRegistration(account
.getSignalProtocolStore().getIdentityKeyPair(), account
.getSignalingKey(), false, true, account
.getSignalProtocolStore().getLocalRegistrationId(), deviceName
);
330 username
= ret
.getNumber();
331 // TODO do this check before actually registering
332 if (SignalAccount
.userExists(dataPath
, username
)) {
333 throw new UserAlreadyExists(username
, SignalAccount
.getFileName(dataPath
, username
));
336 // Create new account with the synced identity
337 byte[] profileKeyBytes
= ret
.getProfileKey();
338 ProfileKey profileKey
;
339 if (profileKeyBytes
== null) {
340 profileKey
= KeyUtils
.createProfileKey();
343 profileKey
= new ProfileKey(profileKeyBytes
);
344 } catch (InvalidInputException e
) {
345 throw new IOException("Received invalid profileKey", e
);
348 account
= SignalAccount
.createLinkedAccount(dataPath
, username
, account
.getPassword(), ret
.getDeviceId(), ret
.getIdentity(), account
.getSignalProtocolStore().getLocalRegistrationId(), account
.getSignalingKey(), profileKey
);
353 requestSyncContacts();
354 requestSyncBlocked();
355 requestSyncConfiguration();
360 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
361 List
<DeviceInfo
> devices
= accountManager
.getDevices();
362 account
.setMultiDevice(devices
.size() > 1);
367 public void removeLinkedDevices(int deviceId
) throws IOException
{
368 accountManager
.removeDevice(deviceId
);
369 List
<DeviceInfo
> devices
= accountManager
.getDevices();
370 account
.setMultiDevice(devices
.size() > 1);
374 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
375 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
377 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
380 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
381 IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
382 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
384 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
385 account
.setMultiDevice(true);
389 private List
<PreKeyRecord
> generatePreKeys() {
390 List
<PreKeyRecord
> records
= new ArrayList
<>(BaseConfig
.PREKEY_BATCH_SIZE
);
392 final int offset
= account
.getPreKeyIdOffset();
393 for (int i
= 0; i
< BaseConfig
.PREKEY_BATCH_SIZE
; i
++) {
394 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
395 ECKeyPair keyPair
= Curve
.generateKeyPair();
396 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
401 account
.addPreKeys(records
);
407 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
409 ECKeyPair keyPair
= Curve
.generateKeyPair();
410 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
411 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
413 account
.addSignedPreKey(record);
417 } catch (InvalidKeyException e
) {
418 throw new AssertionError(e
);
422 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
423 verificationCode
= verificationCode
.replace("-", "");
424 account
.setSignalingKey(KeyUtils
.createSignalingKey());
425 // TODO make unrestricted unidentified access configurable
426 accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, capabilities
);
428 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
429 account
.setRegistered(true);
430 account
.setRegistrationLockPin(pin
);
436 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
437 if (pin
.isPresent()) {
438 account
.setRegistrationLockPin(pin
.get());
439 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
441 account
.setRegistrationLockPin(null);
442 accountManager
.removeV1Pin();
447 private void refreshPreKeys() throws IOException
{
448 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
449 final IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
450 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
452 accountManager
.setPreKeys(getIdentity(), signedPreKeyRecord
, oneTimePreKeys
);
455 private SignalServiceMessageReceiver
getMessageReceiver() {
456 return new SignalServiceMessageReceiver(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), BaseConfig
.USER_AGENT
, null, timer
);
459 private SignalServiceMessageSender
getMessageSender() {
460 return new SignalServiceMessageSender(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
461 account
.getDeviceId(), account
.getSignalProtocolStore(), BaseConfig
.USER_AGENT
, account
.isMultiDevice(), Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent());
464 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
465 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
470 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
471 } catch (IOException ignored
) {
475 SignalServiceMessageReceiver receiver
= getMessageReceiver();
477 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
478 } catch (VerificationFailedException e
) {
479 throw new AssertionError(e
);
483 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
484 File file
= getGroupAvatarFile(groupId
);
485 if (!file
.exists()) {
486 return Optional
.absent();
489 return Optional
.of(Utils
.createAttachment(file
));
492 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
493 File file
= getContactAvatarFile(number
);
494 if (!file
.exists()) {
495 return Optional
.absent();
498 return Optional
.of(Utils
.createAttachment(file
));
501 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
502 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
504 throw new GroupNotFoundException(groupId
);
506 if (!g
.isMember(account
.getSelfAddress())) {
507 throw new NotAGroupMemberException(groupId
, g
.name
);
512 public List
<GroupInfo
> getGroups() {
513 return account
.getGroupStore().getGroups();
517 public void sendGroupMessage(String messageText
, List
<String
> attachments
,
519 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
520 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
521 if (attachments
!= null) {
522 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
524 if (groupId
!= null) {
525 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
528 messageBuilder
.asGroupMessage(group
);
530 ThreadInfo thread
= account
.getThreadStore().getThread(Base64
.encodeBytes(groupId
));
531 if (thread
!= null) {
532 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
535 final GroupInfo g
= getGroupForSending(groupId
);
537 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
540 public void sendGroupMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
541 long targetSentTimestamp
, byte[] groupId
)
542 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
{
543 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
544 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
545 .withReaction(reaction
)
546 .withProfileKey(account
.getProfileKey().serialize());
547 if (groupId
!= null) {
548 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
551 messageBuilder
.asGroupMessage(group
);
553 final GroupInfo g
= getGroupForSending(groupId
);
554 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
557 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
{
558 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
562 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
563 .asGroupMessage(group
);
565 final GroupInfo g
= getGroupForSending(groupId
);
566 g
.removeMember(account
.getSelfAddress());
567 account
.getGroupStore().updateGroup(g
);
569 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
572 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
574 if (groupId
== null) {
576 g
= new GroupInfo(KeyUtils
.createGroupId());
577 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
579 g
= getGroupForSending(groupId
);
586 if (members
!= null) {
587 final Set
<String
> newE164Members
= new HashSet
<>();
588 for (SignalServiceAddress member
: members
) {
589 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
592 newE164Members
.add(member
.getNumber().get());
595 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
596 if (contacts
.size() != newE164Members
.size()) {
597 // Some of the new members are not registered on Signal
598 for (ContactTokenDetails contact
: contacts
) {
599 newE164Members
.remove(contact
.getNumber());
601 System
.err
.println("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
602 System
.err
.println("Aborting…");
606 g
.addMembers(members
);
609 if (avatarFile
!= null) {
610 IOUtils
.createPrivateDirectories(avatarsPath
);
611 File aFile
= getGroupAvatarFile(g
.groupId
);
612 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
615 account
.getGroupStore().updateGroup(g
);
617 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
619 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
623 private void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
624 if (groupId
== null) {
627 GroupInfo g
= getGroupForSending(groupId
);
629 if (!g
.isMember(recipient
)) {
633 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
635 // Send group message only to the recipient who requested it
636 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
639 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
640 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
643 .withMembers(new ArrayList
<>(g
.getMembers()));
645 File aFile
= getGroupAvatarFile(g
.groupId
);
646 if (aFile
.exists()) {
648 group
.withAvatar(Utils
.createAttachment(aFile
));
649 } catch (IOException e
) {
650 throw new AttachmentInvalidException(aFile
.toString(), e
);
654 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
655 .asGroupMessage(group
.build());
657 ThreadInfo thread
= account
.getThreadStore().getThread(Base64
.encodeBytes(g
.groupId
));
658 if (thread
!= null) {
659 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
662 return messageBuilder
;
665 private void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
666 if (groupId
== null) {
670 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
673 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
674 .asGroupMessage(group
.build());
676 ThreadInfo thread
= account
.getThreadStore().getThread(Base64
.encodeBytes(groupId
));
677 if (thread
!= null) {
678 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
681 // Send group info request message to the recipient who sent us a message with this groupId
682 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
686 public void sendMessage(String message
, List
<String
> attachments
, String recipient
)
687 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
, InvalidNumberException
{
688 List
<String
> recipients
= new ArrayList
<>(1);
689 recipients
.add(recipient
);
690 sendMessage(message
, attachments
, recipients
);
694 public void sendMessage(String messageText
, List
<String
> attachments
,
695 List
<String
> recipients
)
696 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
697 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
698 if (attachments
!= null) {
699 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
701 // Upload attachments here, so we only upload once even for multiple recipients
702 SignalServiceMessageSender messageSender
= getMessageSender();
703 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
704 for (SignalServiceAttachment attachment
: attachmentStreams
) {
705 if (attachment
.isStream()) {
706 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
707 } else if (attachment
.isPointer()) {
708 attachmentPointers
.add(attachment
.asPointer());
712 messageBuilder
.withAttachments(attachmentPointers
);
714 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
715 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
718 public void sendMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
719 long targetSentTimestamp
, List
<String
> recipients
)
720 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
721 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
722 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
723 .withReaction(reaction
)
724 .withProfileKey(account
.getProfileKey().serialize());
725 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
729 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
730 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
731 .asEndSessionMessage();
733 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
737 public String
getContactName(String number
) throws InvalidNumberException
{
738 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
739 ContactInfo contact
= account
.getContactStore().getContact(new SignalServiceAddress(null, canonicalizedNumber
));
740 if (contact
== null) {
748 public void setContactName(String number
, String name
) throws InvalidNumberException
{
749 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
750 final SignalServiceAddress address
= new SignalServiceAddress(null, canonicalizedNumber
);
751 ContactInfo contact
= account
.getContactStore().getContact(address
);
752 if (contact
== null) {
753 contact
= new ContactInfo(address
);
754 System
.err
.println("Add contact " + canonicalizedNumber
+ " named " + name
);
756 System
.err
.println("Updating contact " + canonicalizedNumber
+ " name " + contact
.name
+ " -> " + name
);
759 account
.getContactStore().updateContact(contact
);
764 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
765 number
= Utils
.canonicalizeNumber(number
, account
.getUsername());
766 final SignalServiceAddress address
= new SignalServiceAddress(null, number
);
767 ContactInfo contact
= account
.getContactStore().getContact(address
);
768 if (contact
== null) {
769 contact
= new ContactInfo(address
);
770 System
.err
.println("Adding and " + (blocked ?
"blocking" : "unblocking") + " contact " + number
);
772 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " contact " + number
);
774 contact
.blocked
= blocked
;
775 account
.getContactStore().updateContact(contact
);
780 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
781 GroupInfo group
= getGroup(groupId
);
783 throw new GroupNotFoundException(groupId
);
785 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " group " + Base64
.encodeBytes(groupId
));
786 group
.blocked
= blocked
;
787 account
.getGroupStore().updateGroup(group
);
793 public List
<byte[]> getGroupIds() {
794 List
<GroupInfo
> groups
= getGroups();
795 List
<byte[]> ids
= new ArrayList
<>(groups
.size());
796 for (GroupInfo group
: groups
) {
797 ids
.add(group
.groupId
);
803 public String
getGroupName(byte[] groupId
) {
804 GroupInfo group
= getGroup(groupId
);
813 public List
<String
> getGroupMembers(byte[] groupId
) {
814 GroupInfo group
= getGroup(groupId
);
816 return Collections
.emptyList();
818 return new ArrayList
<>(group
.getMembersE164());
823 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
824 if (groupId
.length
== 0) {
827 if (name
.isEmpty()) {
830 if (members
.size() == 0) {
833 if (avatar
.isEmpty()) {
836 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
840 * Change the expiration timer for a thread (number of groupId)
842 public void setExpirationTimer(String numberOrGroupId
, int messageExpirationTimer
) {
843 ThreadInfo thread
= account
.getThreadStore().getThread(numberOrGroupId
);
844 thread
.messageExpirationTime
= messageExpirationTimer
;
845 account
.getThreadStore().updateThread(thread
);
849 * Upload the sticker pack from path.
851 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
852 * @return if successful, returns the URL to install the sticker pack in the signal app
854 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
855 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
857 SignalServiceMessageSender messageSender
= getMessageSender();
859 byte[] packKey
= KeyUtils
.createStickerUploadKey();
860 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
863 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, "utf-8") + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), "utf-8"))
865 } catch (URISyntaxException e
) {
866 throw new AssertionError(e
);
870 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
872 String rootPath
= null;
874 final File file
= new File(path
);
875 if (file
.getName().endsWith(".zip")) {
876 zip
= new ZipFile(file
);
877 } else if (file
.getName().equals("manifest.json")) {
878 rootPath
= file
.getParent();
880 throw new StickerPackInvalidException("Could not find manifest.json");
883 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
885 if (pack
.stickers
== null) {
886 throw new StickerPackInvalidException("Must set a 'stickers' field.");
889 if (pack
.stickers
.isEmpty()) {
890 throw new StickerPackInvalidException("Must include stickers.");
893 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
894 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
895 if (sticker
.file
== null) {
896 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
899 Pair
<InputStream
, Long
> data
;
901 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
902 } catch (IOException ignored
) {
903 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
906 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""));
907 stickers
.add(stickerInfo
);
910 StickerInfo cover
= null;
911 if (pack
.cover
!= null) {
912 if (pack
.cover
.file
== null) {
913 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
916 Pair
<InputStream
, Long
> data
;
918 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
919 } catch (IOException ignored
) {
920 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
923 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""));
926 return new SignalServiceStickerManifestUpload(
933 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
934 InputStream inputStream
;
936 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
938 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
940 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
943 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
945 final ZipEntry entry
= zip
.getEntry(subfile
);
946 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
948 final File file
= new File(rootPath
, subfile
);
949 return new Pair
<>(new FileInputStream(file
), file
.length());
953 private void requestSyncGroups() throws IOException
{
954 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
955 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
957 sendSyncMessage(message
);
958 } catch (UntrustedIdentityException e
) {
963 private void requestSyncContacts() throws IOException
{
964 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
965 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
967 sendSyncMessage(message
);
968 } catch (UntrustedIdentityException e
) {
973 private void requestSyncBlocked() throws IOException
{
974 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
975 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
977 sendSyncMessage(message
);
978 } catch (UntrustedIdentityException e
) {
983 private void requestSyncConfiguration() throws IOException
{
984 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
985 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
987 sendSyncMessage(message
);
988 } catch (UntrustedIdentityException e
) {
993 private byte[] getSenderCertificate() throws IOException
{
994 byte[] certificate
= accountManager
.getSenderCertificate();
995 // TODO cache for a day
999 private byte[] getSelfUnidentifiedAccessKey() {
1000 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
1003 private static SignalProfile
decryptProfile(SignalServiceProfile encryptedProfile
, ProfileKey profileKey
) throws IOException
{
1004 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
1006 return new SignalProfile(
1007 encryptedProfile
.getIdentityKey(),
1008 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
1009 encryptedProfile
.getAvatar(),
1010 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
1011 encryptedProfile
.isUnrestrictedUnidentifiedAccess()
1013 } catch (InvalidCiphertextException e
) {
1018 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) throws IOException
{
1019 ContactInfo contact
= account
.getContactStore().getContact(recipient
);
1020 if (contact
== null || contact
.profileKey
== null) {
1023 ProfileKey theirProfileKey
;
1025 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
1026 } catch (InvalidInputException e
) {
1027 throw new AssertionError(e
);
1029 SignalProfile targetProfile
= decryptProfile(getRecipientProfile(recipient
, Optional
.absent()), theirProfileKey
);
1031 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1035 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1036 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1039 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1042 private Optional
<UnidentifiedAccessPair
> getAccessForSync() throws IOException
{
1043 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1044 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1046 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1047 return Optional
.absent();
1051 return Optional
.of(new UnidentifiedAccessPair(
1052 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1053 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1055 } catch (InvalidCertificateException e
) {
1056 return Optional
.absent();
1060 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) throws IOException
{
1061 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1062 for (SignalServiceAddress recipient
: recipients
) {
1063 result
.add(getAccessFor(recipient
));
1068 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) throws IOException
{
1069 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1070 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1071 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1073 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1074 return Optional
.absent();
1078 return Optional
.of(new UnidentifiedAccessPair(
1079 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1080 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1082 } catch (InvalidCertificateException e
) {
1083 return Optional
.absent();
1087 private void sendSyncMessage(SignalServiceSyncMessage message
)
1088 throws IOException
, UntrustedIdentityException
{
1089 SignalServiceMessageSender messageSender
= getMessageSender();
1091 messageSender
.sendMessage(message
, getAccessForSync());
1092 } catch (UntrustedIdentityException e
) {
1093 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1099 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1101 private void sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1102 throws EncapsulatedExceptions
, IOException
{
1103 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1105 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1106 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1107 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1109 for (SendMessageResult result
: results
) {
1110 if (result
.isUnregisteredFailure()) {
1111 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getNumber().get(), null));
1112 } else if (result
.isNetworkFailure()) {
1113 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getNumber().get(), null));
1114 } else if (result
.getIdentityFailure() != null) {
1115 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getNumber().get(), result
.getIdentityFailure().getIdentityKey()));
1118 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1119 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1123 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1124 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1125 final String username
= account
.getUsername();
1127 for (String number
: numbers
) {
1128 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, username
);
1129 if (canonicalizedNumber
.equals(username
)) {
1130 signalServiceAddresses
.add(account
.getSelfAddress());
1132 // TODO get corresponding uuid
1133 signalServiceAddresses
.add(new SignalServiceAddress(null, canonicalizedNumber
));
1136 return signalServiceAddresses
;
1139 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1140 throws IOException
{
1141 if (messagePipe
== null) {
1142 messagePipe
= getMessageReceiver().createMessagePipe();
1144 if (unidentifiedMessagePipe
== null) {
1145 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1147 SignalServiceDataMessage message
= null;
1149 SignalServiceMessageSender messageSender
= getMessageSender();
1151 message
= messageBuilder
.build();
1152 if (message
.getGroupInfo().isPresent()) {
1154 final boolean isRecipientUpdate
= false;
1155 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1156 for (SendMessageResult r
: result
) {
1157 if (r
.getIdentityFailure() != null) {
1158 account
.getSignalProtocolStore().saveIdentity(r
.getAddress().getNumber().get(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1162 } catch (UntrustedIdentityException e
) {
1163 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1164 return Collections
.emptyList();
1166 } else if (recipients
.size() == 1 && recipients
.contains(account
.getSelfAddress())) {
1167 SignalServiceAddress recipient
= account
.getSelfAddress();
1168 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1169 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1170 message
.getTimestamp(),
1172 message
.getExpiresInSeconds(),
1173 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1175 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1177 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1179 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1180 } catch (UntrustedIdentityException e
) {
1181 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1182 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1186 // Send to all individually, so sync messages are sent correctly
1187 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1188 for (SignalServiceAddress address
: recipients
) {
1189 ThreadInfo thread
= account
.getThreadStore().getThread(address
.getNumber().get());
1190 if (thread
!= null) {
1191 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
1193 messageBuilder
.withExpiration(0);
1195 message
= messageBuilder
.build();
1197 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1198 results
.add(result
);
1199 } catch (UntrustedIdentityException e
) {
1200 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1201 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1207 if (message
!= null && message
.isEndSession()) {
1208 for (SignalServiceAddress recipient
: recipients
) {
1209 handleEndSession(recipient
.getNumber().get());
1216 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, ProtocolUntrustedIdentityException
, SelfSendException
, UnsupportedDataMessageException
{
1217 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1219 return cipher
.decrypt(envelope
);
1220 } catch (ProtocolUntrustedIdentityException e
) {
1221 // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else
1222 // account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
1227 private void handleEndSession(String source
) {
1228 account
.getSignalProtocolStore().deleteAllSessions(source
);
1231 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1233 if (message
.getGroupInfo().isPresent()) {
1234 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1235 threadId
= Base64
.encodeBytes(groupInfo
.getGroupId());
1236 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1237 switch (groupInfo
.getType()) {
1239 if (group
== null) {
1240 group
= new GroupInfo(groupInfo
.getGroupId());
1243 if (groupInfo
.getAvatar().isPresent()) {
1244 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1245 if (avatar
.isPointer()) {
1247 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1248 } catch (IOException
| InvalidMessageException e
) {
1249 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getId() + "): " + e
.getMessage());
1254 if (groupInfo
.getName().isPresent()) {
1255 group
.name
= groupInfo
.getName().get();
1258 if (groupInfo
.getMembers().isPresent()) {
1259 group
.addMembers(groupInfo
.getMembers().get());
1262 account
.getGroupStore().updateGroup(group
);
1265 if (group
== null) {
1267 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1268 } catch (IOException
| EncapsulatedExceptions e
) {
1269 e
.printStackTrace();
1274 if (group
== null) {
1276 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1277 } catch (IOException
| EncapsulatedExceptions e
) {
1278 e
.printStackTrace();
1281 group
.removeMember(source
);
1282 account
.getGroupStore().updateGroup(group
);
1286 if (group
!= null) {
1288 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1289 } catch (IOException
| EncapsulatedExceptions e
) {
1290 e
.printStackTrace();
1291 } catch (NotAGroupMemberException e
) {
1292 // We have left this group, so don't send a group update message
1299 threadId
= destination
.getNumber().get();
1301 threadId
= source
.getNumber().get();
1304 if (message
.isEndSession()) {
1305 handleEndSession(isSync ? destination
.getNumber().get() : source
.getNumber().get());
1307 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1308 ThreadInfo thread
= account
.getThreadStore().getThread(threadId
);
1309 if (thread
== null) {
1310 thread
= new ThreadInfo();
1311 thread
.id
= threadId
;
1313 if (thread
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1314 thread
.messageExpirationTime
= message
.getExpiresInSeconds();
1315 account
.getThreadStore().updateThread(thread
);
1318 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1319 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1320 if (attachment
.isPointer()) {
1322 retrieveAttachment(attachment
.asPointer());
1323 } catch (IOException
| InvalidMessageException e
) {
1324 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getId() + "): " + e
.getMessage());
1329 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1330 if (source
.equals(account
.getSelfAddress())) {
1332 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1333 } catch (InvalidInputException ignored
) {
1336 ContactInfo contact
= account
.getContactStore().getContact(source
);
1337 if (contact
== null) {
1338 contact
= new ContactInfo(source
);
1340 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1341 account
.getContactStore().updateContact(contact
);
1343 if (message
.getPreviews().isPresent()) {
1344 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1345 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1346 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1347 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1349 retrieveAttachment(attachment
);
1350 } catch (IOException
| InvalidMessageException e
) {
1351 System
.err
.println("Failed to retrieve attachment (" + attachment
.getId() + "): " + e
.getMessage());
1358 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1359 final File cachePath
= new File(getMessageCachePath());
1360 if (!cachePath
.exists()) {
1363 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1364 if (!dir
.isDirectory()) {
1368 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1369 if (!fileEntry
.isFile()) {
1372 SignalServiceEnvelope envelope
;
1374 envelope
= Utils
.loadEnvelope(fileEntry
);
1375 if (envelope
== null) {
1378 } catch (IOException e
) {
1379 e
.printStackTrace();
1382 SignalServiceContent content
= null;
1383 if (!envelope
.isReceipt()) {
1385 content
= decryptMessage(envelope
);
1386 } catch (Exception e
) {
1389 handleMessage(envelope
, content
, ignoreAttachments
);
1392 handler
.handleMessage(envelope
, content
, null);
1394 Files
.delete(fileEntry
.toPath());
1395 } catch (IOException e
) {
1396 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1399 // Try to delete directory if empty
1404 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1405 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1406 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1409 if (messagePipe
== null) {
1410 messagePipe
= messageReceiver
.createMessagePipe();
1414 SignalServiceEnvelope envelope
;
1415 SignalServiceContent content
= null;
1416 Exception exception
= null;
1417 final long now
= new Date().getTime();
1419 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1420 // store message on disk, before acknowledging receipt to the server
1422 File cacheFile
= getMessageCacheFile(envelope1
.getSourceE164().get(), now
, envelope1
.getTimestamp());
1423 Utils
.storeEnvelope(envelope1
, cacheFile
);
1424 } catch (IOException e
) {
1425 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1428 } catch (TimeoutException e
) {
1429 if (returnOnTimeout
)
1432 } catch (InvalidVersionException e
) {
1433 System
.err
.println("Ignoring error: " + e
.getMessage());
1436 if (!envelope
.isReceipt()) {
1438 content
= decryptMessage(envelope
);
1439 } catch (Exception e
) {
1442 handleMessage(envelope
, content
, ignoreAttachments
);
1445 if (!isMessageBlocked(envelope
, content
)) {
1446 handler
.handleMessage(envelope
, content
, exception
);
1448 if (!(exception
instanceof ProtocolUntrustedIdentityException
)) {
1449 File cacheFile
= null;
1451 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1452 Files
.delete(cacheFile
.toPath());
1453 // Try to delete directory if empty
1454 new File(getMessageCachePath()).delete();
1455 } catch (IOException e
) {
1456 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1461 if (messagePipe
!= null) {
1462 messagePipe
.shutdown();
1468 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1469 SignalServiceAddress source
;
1470 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1471 source
= envelope
.getSourceAddress();
1472 } else if (content
!= null) {
1473 source
= content
.getSender();
1477 ContactInfo sourceContact
= getContact(source
.getNumber().get());
1478 if (sourceContact
!= null && sourceContact
.blocked
) {
1482 if (content
!= null && content
.getDataMessage().isPresent()) {
1483 SignalServiceDataMessage message
= content
.getDataMessage().get();
1484 if (message
.getGroupInfo().isPresent()) {
1485 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1486 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1487 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1495 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1496 if (content
!= null) {
1497 SignalServiceAddress sender
;
1498 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1499 sender
= envelope
.getSourceAddress();
1501 sender
= content
.getSender();
1503 if (content
.getDataMessage().isPresent()) {
1504 SignalServiceDataMessage message
= content
.getDataMessage().get();
1505 handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
);
1507 if (content
.getSyncMessage().isPresent()) {
1508 account
.setMultiDevice(true);
1509 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1510 if (syncMessage
.getSent().isPresent()) {
1511 SentTranscriptMessage message
= syncMessage
.getSent().get();
1512 handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
);
1514 if (syncMessage
.getRequest().isPresent()) {
1515 RequestMessage rm
= syncMessage
.getRequest().get();
1516 if (rm
.isContactsRequest()) {
1519 } catch (UntrustedIdentityException
| IOException e
) {
1520 e
.printStackTrace();
1523 if (rm
.isGroupsRequest()) {
1526 } catch (UntrustedIdentityException
| IOException e
) {
1527 e
.printStackTrace();
1530 if (rm
.isBlockedListRequest()) {
1533 } catch (UntrustedIdentityException
| IOException e
) {
1534 e
.printStackTrace();
1537 // TODO Handle rm.isConfigurationRequest();
1539 if (syncMessage
.getGroups().isPresent()) {
1540 File tmpFile
= null;
1542 tmpFile
= IOUtils
.createTempFile();
1543 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1544 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1546 while ((g
= s
.read()) != null) {
1547 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1548 if (syncGroup
== null) {
1549 syncGroup
= new GroupInfo(g
.getId());
1551 if (g
.getName().isPresent()) {
1552 syncGroup
.name
= g
.getName().get();
1554 syncGroup
.addMembers(g
.getMembers());
1555 if (!g
.isActive()) {
1556 syncGroup
.removeMember(account
.getSelfAddress());
1558 // Add ourself to the member set as it's marked as active
1559 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1561 syncGroup
.blocked
= g
.isBlocked();
1562 if (g
.getColor().isPresent()) {
1563 syncGroup
.color
= g
.getColor().get();
1566 if (g
.getAvatar().isPresent()) {
1567 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1569 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1570 syncGroup
.archived
= g
.isArchived();
1571 account
.getGroupStore().updateGroup(syncGroup
);
1574 } catch (Exception e
) {
1575 e
.printStackTrace();
1577 if (tmpFile
!= null) {
1579 Files
.delete(tmpFile
.toPath());
1580 } catch (IOException e
) {
1581 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1586 if (syncMessage
.getBlockedList().isPresent()) {
1587 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1588 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1589 if (address
.getNumber().isPresent()) {
1591 setContactBlocked(address
.getNumber().get(), true);
1592 } catch (InvalidNumberException e
) {
1593 e
.printStackTrace();
1597 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1599 setGroupBlocked(groupId
, true);
1600 } catch (GroupNotFoundException e
) {
1601 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1605 if (syncMessage
.getContacts().isPresent()) {
1606 File tmpFile
= null;
1608 tmpFile
= IOUtils
.createTempFile();
1609 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1610 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1611 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1612 if (contactsMessage
.isComplete()) {
1613 account
.getContactStore().clear();
1616 while ((c
= s
.read()) != null) {
1617 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1618 account
.setProfileKey(c
.getProfileKey().get());
1620 ContactInfo contact
= account
.getContactStore().getContact(c
.getAddress());
1621 if (contact
== null) {
1622 contact
= new ContactInfo(c
.getAddress());
1624 if (c
.getName().isPresent()) {
1625 contact
.name
= c
.getName().get();
1627 if (c
.getColor().isPresent()) {
1628 contact
.color
= c
.getColor().get();
1630 if (c
.getProfileKey().isPresent()) {
1631 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1633 if (c
.getVerified().isPresent()) {
1634 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1635 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1637 if (c
.getExpirationTimer().isPresent()) {
1638 ThreadInfo thread
= account
.getThreadStore().getThread(c
.getAddress().getNumber().get());
1639 if (thread
== null) {
1640 thread
= new ThreadInfo();
1641 thread
.id
= c
.getAddress().getNumber().get();
1643 thread
.messageExpirationTime
= c
.getExpirationTimer().get();
1644 account
.getThreadStore().updateThread(thread
);
1646 contact
.blocked
= c
.isBlocked();
1647 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1648 contact
.archived
= c
.isArchived();
1649 account
.getContactStore().updateContact(contact
);
1651 if (c
.getAvatar().isPresent()) {
1652 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1656 } catch (Exception e
) {
1657 e
.printStackTrace();
1659 if (tmpFile
!= null) {
1661 Files
.delete(tmpFile
.toPath());
1662 } catch (IOException e
) {
1663 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1668 if (syncMessage
.getVerified().isPresent()) {
1669 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1670 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1672 if (syncMessage
.getConfiguration().isPresent()) {
1679 private File
getContactAvatarFile(String number
) {
1680 return new File(avatarsPath
, "contact-" + number
);
1683 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
{
1684 IOUtils
.createPrivateDirectories(avatarsPath
);
1685 if (attachment
.isPointer()) {
1686 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1687 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1689 SignalServiceAttachmentStream stream
= attachment
.asStream();
1690 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1694 private File
getGroupAvatarFile(byte[] groupId
) {
1695 return new File(avatarsPath
, "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1698 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
{
1699 IOUtils
.createPrivateDirectories(avatarsPath
);
1700 if (attachment
.isPointer()) {
1701 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1702 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1704 SignalServiceAttachmentStream stream
= attachment
.asStream();
1705 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1709 public File
getAttachmentFile(long attachmentId
) {
1710 return new File(attachmentsPath
, attachmentId
+ "");
1713 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
{
1714 IOUtils
.createPrivateDirectories(attachmentsPath
);
1715 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getId()), true);
1718 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
{
1719 if (storePreview
&& pointer
.getPreview().isPresent()) {
1720 File previewFile
= new File(outputFile
+ ".preview");
1721 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1722 byte[] preview
= pointer
.getPreview().get();
1723 output
.write(preview
, 0, preview
.length
);
1724 } catch (FileNotFoundException e
) {
1725 e
.printStackTrace();
1730 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1732 File tmpFile
= IOUtils
.createTempFile();
1733 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
)) {
1734 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1735 byte[] buffer
= new byte[4096];
1738 while ((read
= input
.read(buffer
)) != -1) {
1739 output
.write(buffer
, 0, read
);
1741 } catch (FileNotFoundException e
) {
1742 e
.printStackTrace();
1747 Files
.delete(tmpFile
.toPath());
1748 } catch (IOException e
) {
1749 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1755 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
{
1756 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1757 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
);
1761 public boolean isRemote() {
1765 private void sendGroups() throws IOException
, UntrustedIdentityException
{
1766 File groupsFile
= IOUtils
.createTempFile();
1769 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1770 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1771 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1772 ThreadInfo info
= account
.getThreadStore().getThread(Base64
.encodeBytes(record.groupId
));
1773 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1774 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1775 record.isMember(account
.getSelfAddress()), Optional
.fromNullable(info
!= null ? info
.messageExpirationTime
: null),
1776 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1780 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1781 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1782 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1783 .withStream(groupsFileStream
)
1784 .withContentType("application/octet-stream")
1785 .withLength(groupsFile
.length())
1788 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1793 Files
.delete(groupsFile
.toPath());
1794 } catch (IOException e
) {
1795 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1800 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1801 File contactsFile
= IOUtils
.createTempFile();
1804 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1805 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1806 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1807 VerifiedMessage verifiedMessage
= null;
1808 ThreadInfo info
= account
.getThreadStore().getThread(record.number
);
1809 if (getIdentities().containsKey(record.number
)) {
1810 JsonIdentityKeyStore
.Identity currentIdentity
= null;
1811 for (JsonIdentityKeyStore
.Identity id
: getIdentities().get(record.number
)) {
1812 if (currentIdentity
== null || id
.getDateAdded().after(currentIdentity
.getDateAdded())) {
1813 currentIdentity
= id
;
1816 if (currentIdentity
!= null) {
1817 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1821 ProfileKey profileKey
= null;
1823 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1824 } catch (InvalidInputException ignored
) {
1826 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1827 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1828 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1829 Optional
.fromNullable(info
!= null ? info
.messageExpirationTime
: null),
1830 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1833 if (account
.getProfileKey() != null) {
1834 // Send our own profile key as well
1835 out
.write(new DeviceContact(account
.getSelfAddress(),
1836 Optional
.absent(), Optional
.absent(),
1837 Optional
.absent(), Optional
.absent(),
1838 Optional
.of(account
.getProfileKey()),
1839 false, Optional
.absent(), Optional
.absent(), false));
1843 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1844 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1845 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1846 .withStream(contactsFileStream
)
1847 .withContentType("application/octet-stream")
1848 .withLength(contactsFile
.length())
1851 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1856 Files
.delete(contactsFile
.toPath());
1857 } catch (IOException e
) {
1858 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1863 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1864 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1865 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1866 if (record.blocked
) {
1867 addresses
.add(record.getAddress());
1870 List
<byte[]> groupIds
= new ArrayList
<>();
1871 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1872 if (record.blocked
) {
1873 groupIds
.add(record.groupId
);
1876 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1879 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1880 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1881 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1884 public List
<ContactInfo
> getContacts() {
1885 return account
.getContactStore().getContacts();
1888 public ContactInfo
getContact(String number
) {
1889 return account
.getContactStore().getContact(new SignalServiceAddress(null, number
));
1892 public GroupInfo
getGroup(byte[] groupId
) {
1893 return account
.getGroupStore().getGroup(groupId
);
1896 public Map
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities() {
1897 return account
.getSignalProtocolStore().getIdentities();
1900 public Pair
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities(String number
) throws InvalidNumberException
{
1901 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
1902 return new Pair
<>(canonicalizedNumber
, account
.getSignalProtocolStore().getIdentities(canonicalizedNumber
));
1906 * Trust this the identity with this fingerprint
1908 * @param name username of the identity
1909 * @param fingerprint Fingerprint
1911 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) {
1912 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1916 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1917 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1921 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1923 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1924 } catch (IOException
| UntrustedIdentityException e
) {
1925 e
.printStackTrace();
1934 * Trust this the identity with this safety number
1936 * @param name username of the identity
1937 * @param safetyNumber Safety number
1939 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) {
1940 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1944 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1945 if (!safetyNumber
.equals(computeSafetyNumber(name
, id
.getIdentityKey()))) {
1949 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1951 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1952 } catch (IOException
| UntrustedIdentityException e
) {
1953 e
.printStackTrace();
1962 * Trust all keys of this identity without verification
1964 * @param name username of the identity
1966 public boolean trustIdentityAllKeys(String name
) {
1967 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1971 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1972 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
1973 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1975 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1976 } catch (IOException
| UntrustedIdentityException e
) {
1977 e
.printStackTrace();
1985 public String
computeSafetyNumber(String theirUsername
, IdentityKey theirIdentityKey
) {
1986 return Utils
.computeSafetyNumber(account
.getUsername(), getIdentity(), theirUsername
, theirIdentityKey
);
1989 public interface ReceiveMessageHandler
{
1991 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);