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
.SignalServiceStickerManifest
;
85 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifest
.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
.StickerUploadAttributes
;
112 import org
.whispersystems
.signalservice
.internal
.push
.StickerUploadAttributesResponse
;
113 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
114 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
115 import org
.whispersystems
.util
.Base64
;
117 import java
.io
.ByteArrayInputStream
;
119 import java
.io
.FileInputStream
;
120 import java
.io
.FileNotFoundException
;
121 import java
.io
.FileOutputStream
;
122 import java
.io
.IOException
;
123 import java
.io
.InputStream
;
124 import java
.io
.OutputStream
;
126 import java
.nio
.file
.Files
;
127 import java
.nio
.file
.Paths
;
128 import java
.nio
.file
.StandardCopyOption
;
129 import java
.util
.ArrayList
;
130 import java
.util
.Arrays
;
131 import java
.util
.Collection
;
132 import java
.util
.Collections
;
133 import java
.util
.Date
;
134 import java
.util
.HashMap
;
135 import java
.util
.HashSet
;
136 import java
.util
.LinkedList
;
137 import java
.util
.List
;
138 import java
.util
.Locale
;
139 import java
.util
.Map
;
140 import java
.util
.Objects
;
141 import java
.util
.Set
;
142 import java
.util
.concurrent
.TimeUnit
;
143 import java
.util
.concurrent
.TimeoutException
;
144 import java
.util
.zip
.ZipEntry
;
145 import java
.util
.zip
.ZipFile
;
147 public class Manager
implements Signal
{
149 private static final SignalServiceProfile
.Capabilities capabilities
= new SignalServiceProfile
.Capabilities(false, false);
151 private final String settingsPath
;
152 private final String dataPath
;
153 private final String attachmentsPath
;
154 private final String avatarsPath
;
155 private final SleepTimer timer
= new UptimeSleepTimer();
157 private SignalAccount account
;
158 private String username
;
159 private SignalServiceAccountManager accountManager
;
160 private SignalServiceMessagePipe messagePipe
= null;
161 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
163 public Manager(String username
, String settingsPath
) {
164 this.username
= username
;
165 this.settingsPath
= settingsPath
;
166 this.dataPath
= this.settingsPath
+ "/data";
167 this.attachmentsPath
= this.settingsPath
+ "/attachments";
168 this.avatarsPath
= this.settingsPath
+ "/avatars";
172 public String
getUsername() {
176 private SignalServiceAddress
getSelfAddress() {
177 return new SignalServiceAddress(null, username
);
180 private SignalServiceAccountManager
getSignalServiceAccountManager() {
181 return new SignalServiceAccountManager(BaseConfig
.serviceConfiguration
, null, account
.getUsername(), account
.getPassword(), account
.getDeviceId(), BaseConfig
.USER_AGENT
, timer
);
184 private IdentityKey
getIdentity() {
185 return account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
188 public int getDeviceId() {
189 return account
.getDeviceId();
192 private String
getMessageCachePath() {
193 return this.dataPath
+ "/" + username
+ ".d/msg-cache";
196 private String
getMessageCachePath(String sender
) {
197 return getMessageCachePath() + "/" + sender
.replace("/", "_");
200 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
201 String cachePath
= getMessageCachePath(sender
);
202 IOUtils
.createPrivateDirectories(cachePath
);
203 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
206 public boolean userHasKeys() {
207 return account
!= null && account
.getSignalProtocolStore() != null;
210 public void init() throws IOException
{
211 if (!SignalAccount
.userExists(dataPath
, username
)) {
214 account
= SignalAccount
.load(dataPath
, username
);
216 migrateLegacyConfigs();
218 accountManager
= getSignalServiceAccountManager();
220 if (account
.isRegistered() && accountManager
.getPreKeysCount() < BaseConfig
.PREKEY_MINIMUM_COUNT
) {
224 } catch (AuthorizationFailedException e
) {
225 System
.err
.println("Authorization failed, was the number registered elsewhere?");
230 private void migrateLegacyConfigs() {
231 // Copy group avatars that were previously stored in the attachments folder
232 // to the new avatar folder
233 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
234 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
235 File avatarFile
= getGroupAvatarFile(g
.groupId
);
236 File attachmentFile
= getAttachmentFile(g
.getAvatarId());
237 if (!avatarFile
.exists() && attachmentFile
.exists()) {
239 IOUtils
.createPrivateDirectories(avatarsPath
);
240 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
241 } catch (Exception e
) {
246 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
249 if (account
.getProfileKey() == null) {
250 // Old config file, creating new profile key
251 account
.setProfileKey(KeyUtils
.createProfileKey());
256 private void createNewIdentity() throws IOException
{
257 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
258 int registrationId
= KeyHelper
.generateRegistrationId(false);
259 if (username
== null) {
260 account
= SignalAccount
.createTemporaryAccount(identityKey
, registrationId
);
262 ProfileKey profileKey
= KeyUtils
.createProfileKey();
263 account
= SignalAccount
.create(dataPath
, username
, identityKey
, registrationId
, profileKey
);
268 public boolean isRegistered() {
269 return account
!= null && account
.isRegistered();
272 public void register(boolean voiceVerification
) throws IOException
{
273 if (account
== null) {
276 account
.setPassword(KeyUtils
.createPassword());
277 accountManager
= getSignalServiceAccountManager();
279 if (voiceVerification
) {
280 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
282 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
285 account
.setRegistered(false);
289 public void updateAccountAttributes() throws IOException
{
290 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities
);
293 public void setProfileName(String name
) throws IOException
{
294 accountManager
.setProfileName(account
.getProfileKey(), name
);
297 public void setProfileAvatar(File avatar
) throws IOException
{
298 final StreamDetails streamDetails
= Utils
.createStreamDetailsFromFile(avatar
);
299 accountManager
.setProfileAvatar(account
.getProfileKey(), streamDetails
);
300 streamDetails
.getStream().close();
303 public void removeProfileAvatar() throws IOException
{
304 accountManager
.setProfileAvatar(account
.getProfileKey(), null);
307 public void unregister() throws IOException
{
308 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
309 // If this is the master device, other users can't send messages to this number anymore.
310 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
311 accountManager
.setGcmId(Optional
.absent());
313 account
.setRegistered(false);
317 public String
getDeviceLinkUri() throws TimeoutException
, IOException
{
318 if (account
== null) {
321 account
.setPassword(KeyUtils
.createPassword());
322 accountManager
= getSignalServiceAccountManager();
323 String uuid
= accountManager
.getNewDeviceUuid();
325 return Utils
.createDeviceLinkUri(new Utils
.DeviceLinkInfo(uuid
, getIdentity().getPublicKey()));
328 public void finishDeviceLink(String deviceName
) throws IOException
, InvalidKeyException
, TimeoutException
, UserAlreadyExists
{
329 account
.setSignalingKey(KeyUtils
.createSignalingKey());
330 SignalServiceAccountManager
.NewDeviceRegistrationReturn ret
= accountManager
.finishNewDeviceRegistration(account
.getSignalProtocolStore().getIdentityKeyPair(), account
.getSignalingKey(), false, true, account
.getSignalProtocolStore().getLocalRegistrationId(), deviceName
);
332 username
= ret
.getNumber();
333 // TODO do this check before actually registering
334 if (SignalAccount
.userExists(dataPath
, username
)) {
335 throw new UserAlreadyExists(username
, SignalAccount
.getFileName(dataPath
, username
));
338 // Create new account with the synced identity
339 byte[] profileKeyBytes
= ret
.getProfileKey();
340 ProfileKey profileKey
;
341 if (profileKeyBytes
== null) {
342 profileKey
= KeyUtils
.createProfileKey();
345 profileKey
= new ProfileKey(profileKeyBytes
);
346 } catch (InvalidInputException e
) {
347 throw new IOException("Received invalid profileKey", e
);
350 account
= SignalAccount
.createLinkedAccount(dataPath
, username
, account
.getPassword(), ret
.getDeviceId(), ret
.getIdentity(), account
.getSignalProtocolStore().getLocalRegistrationId(), account
.getSignalingKey(), profileKey
);
355 requestSyncContacts();
356 requestSyncBlocked();
357 requestSyncConfiguration();
362 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
363 List
<DeviceInfo
> devices
= accountManager
.getDevices();
364 account
.setMultiDevice(devices
.size() > 1);
369 public void removeLinkedDevices(int deviceId
) throws IOException
{
370 accountManager
.removeDevice(deviceId
);
371 List
<DeviceInfo
> devices
= accountManager
.getDevices();
372 account
.setMultiDevice(devices
.size() > 1);
376 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
377 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
379 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
382 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
383 IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
384 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
386 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
387 account
.setMultiDevice(true);
391 private List
<PreKeyRecord
> generatePreKeys() {
392 List
<PreKeyRecord
> records
= new ArrayList
<>(BaseConfig
.PREKEY_BATCH_SIZE
);
394 final int offset
= account
.getPreKeyIdOffset();
395 for (int i
= 0; i
< BaseConfig
.PREKEY_BATCH_SIZE
; i
++) {
396 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
397 ECKeyPair keyPair
= Curve
.generateKeyPair();
398 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
403 account
.addPreKeys(records
);
409 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
411 ECKeyPair keyPair
= Curve
.generateKeyPair();
412 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
413 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
415 account
.addSignedPreKey(record);
419 } catch (InvalidKeyException e
) {
420 throw new AssertionError(e
);
424 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
425 verificationCode
= verificationCode
.replace("-", "");
426 account
.setSignalingKey(KeyUtils
.createSignalingKey());
427 // TODO make unrestricted unidentified access configurable
428 accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, capabilities
);
430 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
431 account
.setRegistered(true);
432 account
.setRegistrationLockPin(pin
);
438 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
439 if (pin
.isPresent()) {
440 account
.setRegistrationLockPin(pin
.get());
441 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
443 account
.setRegistrationLockPin(null);
444 accountManager
.removeV1Pin();
449 private void refreshPreKeys() throws IOException
{
450 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
451 final IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
452 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
454 accountManager
.setPreKeys(getIdentity(), signedPreKeyRecord
, oneTimePreKeys
);
457 private SignalServiceMessageReceiver
getMessageReceiver() {
458 return new SignalServiceMessageReceiver(BaseConfig
.serviceConfiguration
, null, username
, account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), BaseConfig
.USER_AGENT
, null, timer
);
461 private SignalServiceMessageSender
getMessageSender() {
462 return new SignalServiceMessageSender(BaseConfig
.serviceConfiguration
, null, username
, account
.getPassword(),
463 account
.getDeviceId(), account
.getSignalProtocolStore(), BaseConfig
.USER_AGENT
, account
.isMultiDevice(), Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent());
466 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
467 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
472 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
473 } catch (IOException ignored
) {
477 SignalServiceMessageReceiver receiver
= getMessageReceiver();
479 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
480 } catch (VerificationFailedException e
) {
481 throw new AssertionError(e
);
485 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
486 File file
= getGroupAvatarFile(groupId
);
487 if (!file
.exists()) {
488 return Optional
.absent();
491 return Optional
.of(Utils
.createAttachment(file
));
494 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
495 File file
= getContactAvatarFile(number
);
496 if (!file
.exists()) {
497 return Optional
.absent();
500 return Optional
.of(Utils
.createAttachment(file
));
503 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
504 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
506 throw new GroupNotFoundException(groupId
);
508 for (String member
: g
.members
) {
509 if (member
.equals(this.username
)) {
513 throw new NotAGroupMemberException(groupId
, g
.name
);
516 public List
<GroupInfo
> getGroups() {
517 return account
.getGroupStore().getGroups();
521 public void sendGroupMessage(String messageText
, List
<String
> attachments
,
523 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
524 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
525 if (attachments
!= null) {
526 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
528 if (groupId
!= null) {
529 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
532 messageBuilder
.asGroupMessage(group
);
534 ThreadInfo thread
= account
.getThreadStore().getThread(Base64
.encodeBytes(groupId
));
535 if (thread
!= null) {
536 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
539 final GroupInfo g
= getGroupForSending(groupId
);
541 // Don't send group message to ourself
542 final List
<String
> membersSend
= new ArrayList
<>(g
.members
);
543 membersSend
.remove(this.username
);
544 sendMessageLegacy(messageBuilder
, membersSend
);
547 public void sendGroupMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
548 long targetSentTimestamp
, byte[] groupId
)
549 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
{
550 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
551 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
552 .withReaction(reaction
)
553 .withProfileKey(account
.getProfileKey().serialize());
554 if (groupId
!= null) {
555 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
558 messageBuilder
.asGroupMessage(group
);
560 final GroupInfo g
= getGroupForSending(groupId
);
561 // Don't send group message to ourself
562 final List
<String
> membersSend
= new ArrayList
<>(g
.members
);
563 membersSend
.remove(this.username
);
564 sendMessageLegacy(messageBuilder
, membersSend
);
567 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
{
568 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
572 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
573 .asGroupMessage(group
);
575 final GroupInfo g
= getGroupForSending(groupId
);
576 g
.members
.remove(this.username
);
577 account
.getGroupStore().updateGroup(g
);
579 sendMessageLegacy(messageBuilder
, g
.members
);
582 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<String
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
584 if (groupId
== null) {
586 g
= new GroupInfo(KeyUtils
.createGroupId());
587 g
.members
.add(username
);
589 g
= getGroupForSending(groupId
);
596 if (members
!= null) {
597 Set
<String
> newMembers
= new HashSet
<>();
598 for (String member
: members
) {
600 member
= Utils
.canonicalizeNumber(member
, username
);
601 } catch (InvalidNumberException e
) {
602 System
.err
.println("Failed to add member \"" + member
+ "\" to group: " + e
.getMessage());
603 System
.err
.println("Aborting…");
606 if (g
.members
.contains(member
)) {
609 newMembers
.add(member
);
610 g
.members
.add(member
);
612 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newMembers
);
613 if (contacts
.size() != newMembers
.size()) {
614 // Some of the new members are not registered on Signal
615 for (ContactTokenDetails contact
: contacts
) {
616 newMembers
.remove(contact
.getNumber());
618 System
.err
.println("Failed to add members " + Util
.join(", ", newMembers
) + " to group: Not registered on Signal");
619 System
.err
.println("Aborting…");
624 if (avatarFile
!= null) {
625 IOUtils
.createPrivateDirectories(avatarsPath
);
626 File aFile
= getGroupAvatarFile(g
.groupId
);
627 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
630 account
.getGroupStore().updateGroup(g
);
632 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
634 // Don't send group message to ourself
635 final List
<String
> membersSend
= new ArrayList
<>(g
.members
);
636 membersSend
.remove(this.username
);
637 sendMessageLegacy(messageBuilder
, membersSend
);
641 private void sendUpdateGroupMessage(byte[] groupId
, String recipient
) throws IOException
, EncapsulatedExceptions
{
642 if (groupId
== null) {
645 GroupInfo g
= getGroupForSending(groupId
);
647 if (!g
.members
.contains(recipient
)) {
651 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
653 // Send group message only to the recipient who requested it
654 final List
<String
> membersSend
= new ArrayList
<>();
655 membersSend
.add(recipient
);
656 sendMessageLegacy(messageBuilder
, membersSend
);
659 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
660 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
663 .withMembers(new ArrayList
<>(g
.getMembers()));
665 File aFile
= getGroupAvatarFile(g
.groupId
);
666 if (aFile
.exists()) {
668 group
.withAvatar(Utils
.createAttachment(aFile
));
669 } catch (IOException e
) {
670 throw new AttachmentInvalidException(aFile
.toString(), e
);
674 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
675 .asGroupMessage(group
.build());
677 ThreadInfo thread
= account
.getThreadStore().getThread(Base64
.encodeBytes(g
.groupId
));
678 if (thread
!= null) {
679 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
682 return messageBuilder
;
685 private void sendGroupInfoRequest(byte[] groupId
, String recipient
) throws IOException
, EncapsulatedExceptions
{
686 if (groupId
== null) {
690 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
693 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
694 .asGroupMessage(group
.build());
696 ThreadInfo thread
= account
.getThreadStore().getThread(Base64
.encodeBytes(groupId
));
697 if (thread
!= null) {
698 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
701 // Send group info request message to the recipient who sent us a message with this groupId
702 final List
<String
> membersSend
= new ArrayList
<>();
703 membersSend
.add(recipient
);
704 sendMessageLegacy(messageBuilder
, membersSend
);
708 public void sendMessage(String message
, List
<String
> attachments
, String recipient
)
709 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
{
710 List
<String
> recipients
= new ArrayList
<>(1);
711 recipients
.add(recipient
);
712 sendMessage(message
, attachments
, recipients
);
716 public void sendMessage(String messageText
, List
<String
> attachments
,
717 List
<String
> recipients
)
718 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
{
719 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
720 if (attachments
!= null) {
721 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
723 // Upload attachments here, so we only upload once even for multiple recipients
724 SignalServiceMessageSender messageSender
= getMessageSender();
725 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
726 for (SignalServiceAttachment attachment
: attachmentStreams
) {
727 if (attachment
.isStream()) {
728 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
729 } else if (attachment
.isPointer()) {
730 attachmentPointers
.add(attachment
.asPointer());
734 messageBuilder
.withAttachments(attachmentPointers
);
736 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
737 sendMessageLegacy(messageBuilder
, recipients
);
740 public void sendMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
741 long targetSentTimestamp
, List
<String
> recipients
)
742 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
{
743 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
744 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
745 .withReaction(reaction
)
746 .withProfileKey(account
.getProfileKey().serialize());
747 sendMessageLegacy(messageBuilder
, recipients
);
751 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
{
752 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
753 .asEndSessionMessage();
755 sendMessageLegacy(messageBuilder
, recipients
);
759 public String
getContactName(String number
) throws InvalidNumberException
{
760 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, username
);
761 ContactInfo contact
= account
.getContactStore().getContact(canonicalizedNumber
);
762 if (contact
== null) {
770 public void setContactName(String number
, String name
) throws InvalidNumberException
{
771 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, username
);
772 ContactInfo contact
= account
.getContactStore().getContact(canonicalizedNumber
);
773 if (contact
== null) {
774 contact
= new ContactInfo();
775 contact
.number
= canonicalizedNumber
;
776 System
.err
.println("Add contact " + canonicalizedNumber
+ " named " + name
);
778 System
.err
.println("Updating contact " + canonicalizedNumber
+ " name " + contact
.name
+ " -> " + name
);
781 account
.getContactStore().updateContact(contact
);
786 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
787 number
= Utils
.canonicalizeNumber(number
, username
);
788 ContactInfo contact
= account
.getContactStore().getContact(number
);
789 if (contact
== null) {
790 contact
= new ContactInfo();
791 contact
.number
= number
;
792 System
.err
.println("Adding and " + (blocked ?
"blocking" : "unblocking") + " contact " + number
);
794 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " contact " + number
);
796 contact
.blocked
= blocked
;
797 account
.getContactStore().updateContact(contact
);
802 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
803 GroupInfo group
= getGroup(groupId
);
805 throw new GroupNotFoundException(groupId
);
807 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " group " + Base64
.encodeBytes(groupId
));
808 group
.blocked
= blocked
;
809 account
.getGroupStore().updateGroup(group
);
815 public List
<byte[]> getGroupIds() {
816 List
<GroupInfo
> groups
= getGroups();
817 List
<byte[]> ids
= new ArrayList
<>(groups
.size());
818 for (GroupInfo group
: groups
) {
819 ids
.add(group
.groupId
);
825 public String
getGroupName(byte[] groupId
) {
826 GroupInfo group
= getGroup(groupId
);
835 public List
<String
> getGroupMembers(byte[] groupId
) {
836 GroupInfo group
= getGroup(groupId
);
838 return new ArrayList
<>();
840 return new ArrayList
<>(group
.members
);
845 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
846 if (groupId
.length
== 0) {
849 if (name
.isEmpty()) {
852 if (members
.size() == 0) {
855 if (avatar
.isEmpty()) {
858 return sendUpdateGroupMessage(groupId
, name
, members
, avatar
);
862 * Change the expiration timer for a thread (number of groupId)
864 * @param numberOrGroupId
865 * @param messageExpirationTimer
867 public void setExpirationTimer(String numberOrGroupId
, int messageExpirationTimer
) {
868 ThreadInfo thread
= account
.getThreadStore().getThread(numberOrGroupId
);
869 thread
.messageExpirationTime
= messageExpirationTimer
;
870 account
.getThreadStore().updateThread(thread
);
873 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
874 JsonStickerPack pack
= parseStickerPack(path
);
876 if (pack
.stickers
== null) {
877 throw new StickerPackInvalidException("Must set a 'stickers' field.");
880 if (pack
.stickers
.isEmpty()) {
881 throw new StickerPackInvalidException("Must include stickers.");
884 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
885 for (int i
= 0; i
< pack
.stickers
.size(); i
++) {
886 if (pack
.stickers
.get(i
).file
== null) {
887 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
889 if (!stickerDataContainsPath(path
, pack
.stickers
.get(i
).file
)) {
890 throw new StickerPackInvalidException("Could not find find " + pack
.stickers
.get(i
).file
);
893 StickerInfo stickerInfo
= new StickerInfo(i
, Optional
.fromNullable(pack
.stickers
.get(i
).emoji
).or(""));
894 stickers
.add(stickerInfo
);
897 boolean uniqueCover
= false;
898 StickerInfo cover
= stickers
.get(0);
899 if (pack
.cover
!= null) {
900 if (pack
.cover
.file
== null) {
901 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
903 if (!stickerDataContainsPath(path
, pack
.cover
.file
)) {
904 throw new StickerPackInvalidException("Could not find find cover " + pack
.cover
.file
);
908 cover
= new StickerInfo(pack
.stickers
.size(), Optional
.fromNullable(pack
.cover
.emoji
).or(""));
911 SignalServiceStickerManifest manifest
= new SignalServiceStickerManifest(
912 Optional
.fromNullable(pack
.title
).or(""),
913 Optional
.fromNullable(pack
.author
).or(""),
917 SignalServiceMessageSender messageSender
= new SignalServiceMessageSender(
918 BaseConfig
.serviceConfiguration
,
921 account
.getPassword(),
922 account
.getDeviceId(),
923 account
.getSignalProtocolStore(),
924 BaseConfig
.USER_AGENT
,
925 account
.isMultiDevice(),
926 Optional
.fromNullable(messagePipe
),
927 Optional
.fromNullable(unidentifiedMessagePipe
),
928 Optional
.<SignalServiceMessageSender
.EventListener
>absent());
930 System
.out
.println("Starting upload process...");
931 Pair
<byte[], StickerUploadAttributesResponse
> responsePair
= messageSender
.getStickerUploadAttributes(stickers
.size() + (uniqueCover ?
1 : 0));
932 byte[] packKey
= responsePair
.first();
933 StickerUploadAttributesResponse response
= responsePair
.second();
935 System
.out
.println("Uploading manifest...");
936 messageSender
.uploadStickerManifest(manifest
, packKey
, response
.getManifest());
938 Map
<Integer
, StickerUploadAttributes
> attrById
= new HashMap
<>();
940 for (StickerUploadAttributes attr
: response
.getStickers()) {
941 attrById
.put(attr
.getId(), attr
);
944 for (int i
= 0; i
< pack
.stickers
.size(); i
++) {
945 System
.out
.println("Uploading sticker " + (i
+1) + "/" + pack
.stickers
.size() + "...");
946 StickerUploadAttributes attr
= attrById
.get(i
);
948 throw new StickerPackInvalidException("Upload attributes missing for id " + i
);
951 byte[] data
= readStickerDataFromPath(path
, pack
.stickers
.get(i
).file
);
952 messageSender
.uploadSticker(new ByteArrayInputStream(data
), data
.length
, packKey
, attr
);
956 System
.out
.println("Uploading unique cover...");
957 StickerUploadAttributes attr
= attrById
.get(pack
.stickers
.size());
959 throw new StickerPackInvalidException("Upload attributes missing for cover with id " + pack
.stickers
.size());
962 byte[] data
= readStickerDataFromPath(path
, pack
.cover
.file
);
963 messageSender
.uploadSticker(new ByteArrayInputStream(data
), data
.length
, packKey
, attr
);
966 return "https://signal.art/addstickers/#pack_id=" + response
.getPackId() + "&pack_key=" + Hex
.toStringCondensed(packKey
).replaceAll(" ", "");
969 private static byte[] readStickerDataFromPath(String rootPath
, String subFile
) throws IOException
, StickerPackInvalidException
{
970 if (rootPath
.endsWith(".zip")) {
971 ZipFile zip
= new ZipFile(rootPath
);
972 ZipEntry entry
= zip
.getEntry(subFile
);
973 return IOUtils
.readFully(zip
.getInputStream(entry
));
974 } else if (rootPath
.endsWith(".json")) {
975 String dir
= new File(rootPath
).getParent();
976 FileInputStream fis
= new FileInputStream(new File(dir
, subFile
));
977 return IOUtils
.readFully(fis
);
979 throw new StickerPackInvalidException("Must point to either a ZIP or JSON file.");
983 private static boolean stickerDataContainsPath(String rootPath
, String subFile
) throws IOException
{
984 if (rootPath
.endsWith(".zip")) {
985 ZipFile zip
= new ZipFile(rootPath
);
986 return zip
.getEntry(subFile
) != null;
987 } else if (rootPath
.endsWith(".json")) {
988 String dir
= new File(rootPath
).getParent();
989 return new File(dir
, subFile
).exists();
995 private static JsonStickerPack
parseStickerPack(String rootPath
) throws IOException
, StickerPackInvalidException
{
996 if (!stickerDataContainsPath(rootPath
, "manifest.json")) {
997 throw new StickerPackInvalidException("Could not find manifest.json");
1000 String json
= new String(readStickerDataFromPath(rootPath
, "manifest.json"));
1002 return new ObjectMapper().readValue(json
, JsonStickerPack
.class);
1005 private void requestSyncGroups() throws IOException
{
1006 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
1007 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1009 sendSyncMessage(message
);
1010 } catch (UntrustedIdentityException e
) {
1011 e
.printStackTrace();
1015 private void requestSyncContacts() throws IOException
{
1016 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
1017 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1019 sendSyncMessage(message
);
1020 } catch (UntrustedIdentityException e
) {
1021 e
.printStackTrace();
1025 private void requestSyncBlocked() throws IOException
{
1026 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
1027 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1029 sendSyncMessage(message
);
1030 } catch (UntrustedIdentityException e
) {
1031 e
.printStackTrace();
1035 private void requestSyncConfiguration() throws IOException
{
1036 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
1037 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1039 sendSyncMessage(message
);
1040 } catch (UntrustedIdentityException e
) {
1041 e
.printStackTrace();
1045 private byte[] getSenderCertificate() throws IOException
{
1046 byte[] certificate
= accountManager
.getSenderCertificate();
1047 // TODO cache for a day
1051 private byte[] getSelfUnidentifiedAccessKey() {
1052 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
1055 private static SignalProfile
decryptProfile(SignalServiceProfile encryptedProfile
, ProfileKey profileKey
) throws IOException
{
1056 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
1058 return new SignalProfile(
1059 encryptedProfile
.getIdentityKey(),
1060 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
1061 encryptedProfile
.getAvatar(),
1062 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
1063 encryptedProfile
.isUnrestrictedUnidentifiedAccess()
1065 } catch (InvalidCiphertextException e
) {
1070 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) throws IOException
{
1071 ContactInfo contact
= account
.getContactStore().getContact(recipient
.getNumber().get());
1072 if (contact
== null || contact
.profileKey
== null) {
1075 ProfileKey theirProfileKey
;
1077 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
1078 } catch (InvalidInputException e
) {
1079 throw new AssertionError(e
);
1081 SignalProfile targetProfile
= decryptProfile(getRecipientProfile(recipient
, Optional
.absent()), theirProfileKey
);
1083 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1087 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1088 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1091 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1094 private Optional
<UnidentifiedAccessPair
> getAccessForSync() throws IOException
{
1095 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1096 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1098 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1099 return Optional
.absent();
1103 return Optional
.of(new UnidentifiedAccessPair(
1104 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1105 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1107 } catch (InvalidCertificateException e
) {
1108 return Optional
.absent();
1112 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) throws IOException
{
1113 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1114 for (SignalServiceAddress recipient
: recipients
) {
1115 result
.add(getAccessFor(recipient
));
1120 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) throws IOException
{
1121 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1122 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1123 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1125 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1126 return Optional
.absent();
1130 return Optional
.of(new UnidentifiedAccessPair(
1131 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1132 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1134 } catch (InvalidCertificateException e
) {
1135 return Optional
.absent();
1139 private void sendSyncMessage(SignalServiceSyncMessage message
)
1140 throws IOException
, UntrustedIdentityException
{
1141 SignalServiceMessageSender messageSender
= getMessageSender();
1143 messageSender
.sendMessage(message
, getAccessForSync());
1144 } catch (UntrustedIdentityException e
) {
1145 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1151 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1153 private void sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<String
> recipients
)
1154 throws EncapsulatedExceptions
, IOException
{
1155 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1157 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1158 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1159 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1161 for (SendMessageResult result
: results
) {
1162 if (result
.isUnregisteredFailure()) {
1163 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getNumber().get(), null));
1164 } else if (result
.isNetworkFailure()) {
1165 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getNumber().get(), null));
1166 } else if (result
.getIdentityFailure() != null) {
1167 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getNumber().get(), result
.getIdentityFailure().getIdentityKey()));
1170 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1171 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1175 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<String
> recipients
)
1176 throws IOException
{
1177 Set
<SignalServiceAddress
> recipientsTS
= Utils
.getSignalServiceAddresses(recipients
, username
);
1178 if (recipientsTS
== null) {
1180 return Collections
.emptyList();
1183 if (messagePipe
== null) {
1184 messagePipe
= getMessageReceiver().createMessagePipe();
1186 if (unidentifiedMessagePipe
== null) {
1187 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1189 SignalServiceDataMessage message
= null;
1191 SignalServiceMessageSender messageSender
= getMessageSender();
1193 message
= messageBuilder
.build();
1194 if (message
.getGroupInfo().isPresent()) {
1196 final boolean isRecipientUpdate
= false;
1197 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipientsTS
), getAccessFor(recipientsTS
), isRecipientUpdate
, message
);
1198 for (SendMessageResult r
: result
) {
1199 if (r
.getIdentityFailure() != null) {
1200 account
.getSignalProtocolStore().saveIdentity(r
.getAddress().getNumber().get(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1204 } catch (UntrustedIdentityException e
) {
1205 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1206 return Collections
.emptyList();
1208 } else if (recipientsTS
.size() == 1 && recipientsTS
.contains(getSelfAddress())) {
1209 SignalServiceAddress recipient
= getSelfAddress();
1210 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1211 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1212 message
.getTimestamp(),
1214 message
.getExpiresInSeconds(),
1215 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1217 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1219 List
<SendMessageResult
> results
= new ArrayList
<>(recipientsTS
.size());
1221 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1222 } catch (UntrustedIdentityException e
) {
1223 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1224 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1228 // Send to all individually, so sync messages are sent correctly
1229 List
<SendMessageResult
> results
= new ArrayList
<>(recipientsTS
.size());
1230 for (SignalServiceAddress address
: recipientsTS
) {
1231 ThreadInfo thread
= account
.getThreadStore().getThread(address
.getNumber().get());
1232 if (thread
!= null) {
1233 messageBuilder
.withExpiration(thread
.messageExpirationTime
);
1235 messageBuilder
.withExpiration(0);
1237 message
= messageBuilder
.build();
1239 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1240 results
.add(result
);
1241 } catch (UntrustedIdentityException e
) {
1242 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1243 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1249 if (message
!= null && message
.isEndSession()) {
1250 for (SignalServiceAddress recipient
: recipientsTS
) {
1251 handleEndSession(recipient
.getNumber().get());
1258 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, ProtocolUntrustedIdentityException
, SelfSendException
, UnsupportedDataMessageException
{
1259 SignalServiceCipher cipher
= new SignalServiceCipher(getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1261 return cipher
.decrypt(envelope
);
1262 } catch (ProtocolUntrustedIdentityException e
) {
1263 // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else
1264 // account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
1269 private void handleEndSession(String source
) {
1270 account
.getSignalProtocolStore().deleteAllSessions(source
);
1273 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, String source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1275 if (message
.getGroupInfo().isPresent()) {
1276 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1277 threadId
= Base64
.encodeBytes(groupInfo
.getGroupId());
1278 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1279 switch (groupInfo
.getType()) {
1281 if (group
== null) {
1282 group
= new GroupInfo(groupInfo
.getGroupId());
1285 if (groupInfo
.getAvatar().isPresent()) {
1286 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1287 if (avatar
.isPointer()) {
1289 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1290 } catch (IOException
| InvalidMessageException e
) {
1291 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getId() + "): " + e
.getMessage());
1296 if (groupInfo
.getName().isPresent()) {
1297 group
.name
= groupInfo
.getName().get();
1300 if (groupInfo
.getMembers().isPresent()) {
1301 group
.addMembers(groupInfo
.getMembers().get());
1304 account
.getGroupStore().updateGroup(group
);
1307 if (group
== null) {
1309 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1310 } catch (IOException
| EncapsulatedExceptions e
) {
1311 e
.printStackTrace();
1316 if (group
== null) {
1318 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1319 } catch (IOException
| EncapsulatedExceptions e
) {
1320 e
.printStackTrace();
1323 group
.members
.remove(source
);
1324 account
.getGroupStore().updateGroup(group
);
1328 if (group
!= null) {
1330 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1331 } catch (IOException
| EncapsulatedExceptions e
) {
1332 e
.printStackTrace();
1333 } catch (NotAGroupMemberException e
) {
1334 // We have left this group, so don't send a group update message
1341 threadId
= destination
.getNumber().get();
1346 if (message
.isEndSession()) {
1347 handleEndSession(isSync ? destination
.getNumber().get() : source
);
1349 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1350 ThreadInfo thread
= account
.getThreadStore().getThread(threadId
);
1351 if (thread
== null) {
1352 thread
= new ThreadInfo();
1353 thread
.id
= threadId
;
1355 if (thread
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1356 thread
.messageExpirationTime
= message
.getExpiresInSeconds();
1357 account
.getThreadStore().updateThread(thread
);
1360 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1361 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1362 if (attachment
.isPointer()) {
1364 retrieveAttachment(attachment
.asPointer());
1365 } catch (IOException
| InvalidMessageException e
) {
1366 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getId() + "): " + e
.getMessage());
1371 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1372 if (source
.equals(username
)) {
1374 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1375 } catch (InvalidInputException ignored
) {
1378 ContactInfo contact
= account
.getContactStore().getContact(source
);
1379 if (contact
== null) {
1380 contact
= new ContactInfo();
1381 contact
.number
= source
;
1383 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1384 account
.getContactStore().updateContact(contact
);
1386 if (message
.getPreviews().isPresent()) {
1387 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1388 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1389 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1390 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1392 retrieveAttachment(attachment
);
1393 } catch (IOException
| InvalidMessageException e
) {
1394 System
.err
.println("Failed to retrieve attachment (" + attachment
.getId() + "): " + e
.getMessage());
1401 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1402 final File cachePath
= new File(getMessageCachePath());
1403 if (!cachePath
.exists()) {
1406 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1407 if (!dir
.isDirectory()) {
1411 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1412 if (!fileEntry
.isFile()) {
1415 SignalServiceEnvelope envelope
;
1417 envelope
= Utils
.loadEnvelope(fileEntry
);
1418 if (envelope
== null) {
1421 } catch (IOException e
) {
1422 e
.printStackTrace();
1425 SignalServiceContent content
= null;
1426 if (!envelope
.isReceipt()) {
1428 content
= decryptMessage(envelope
);
1429 } catch (Exception e
) {
1432 handleMessage(envelope
, content
, ignoreAttachments
);
1435 handler
.handleMessage(envelope
, content
, null);
1437 Files
.delete(fileEntry
.toPath());
1438 } catch (IOException e
) {
1439 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1442 // Try to delete directory if empty
1447 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1448 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1449 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1452 if (messagePipe
== null) {
1453 messagePipe
= messageReceiver
.createMessagePipe();
1457 SignalServiceEnvelope envelope
;
1458 SignalServiceContent content
= null;
1459 Exception exception
= null;
1460 final long now
= new Date().getTime();
1462 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1463 // store message on disk, before acknowledging receipt to the server
1465 File cacheFile
= getMessageCacheFile(envelope1
.getSourceE164().get(), now
, envelope1
.getTimestamp());
1466 Utils
.storeEnvelope(envelope1
, cacheFile
);
1467 } catch (IOException e
) {
1468 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1471 } catch (TimeoutException e
) {
1472 if (returnOnTimeout
)
1475 } catch (InvalidVersionException e
) {
1476 System
.err
.println("Ignoring error: " + e
.getMessage());
1479 if (!envelope
.isReceipt()) {
1481 content
= decryptMessage(envelope
);
1482 } catch (Exception e
) {
1485 handleMessage(envelope
, content
, ignoreAttachments
);
1488 if (!isMessageBlocked(envelope
, content
)) {
1489 handler
.handleMessage(envelope
, content
, exception
);
1491 if (!(exception
instanceof ProtocolUntrustedIdentityException
)) {
1492 File cacheFile
= null;
1494 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1495 Files
.delete(cacheFile
.toPath());
1496 // Try to delete directory if empty
1497 new File(getMessageCachePath()).delete();
1498 } catch (IOException e
) {
1499 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1504 if (messagePipe
!= null) {
1505 messagePipe
.shutdown();
1511 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1512 SignalServiceAddress source
;
1513 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1514 source
= envelope
.getSourceAddress();
1515 } else if (content
!= null) {
1516 source
= content
.getSender();
1520 ContactInfo sourceContact
= getContact(source
.getNumber().get());
1521 if (sourceContact
!= null && sourceContact
.blocked
) {
1525 if (content
!= null && content
.getDataMessage().isPresent()) {
1526 SignalServiceDataMessage message
= content
.getDataMessage().get();
1527 if (message
.getGroupInfo().isPresent()) {
1528 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1529 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1530 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1538 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1539 if (content
!= null) {
1540 SignalServiceAddress sender
;
1541 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1542 sender
= envelope
.getSourceAddress();
1544 sender
= content
.getSender();
1546 if (content
.getDataMessage().isPresent()) {
1547 SignalServiceDataMessage message
= content
.getDataMessage().get();
1548 handleSignalServiceDataMessage(message
, false, sender
.getNumber().get(), getSelfAddress(), ignoreAttachments
);
1550 if (content
.getSyncMessage().isPresent()) {
1551 account
.setMultiDevice(true);
1552 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1553 if (syncMessage
.getSent().isPresent()) {
1554 SentTranscriptMessage message
= syncMessage
.getSent().get();
1555 handleSignalServiceDataMessage(message
.getMessage(), true, sender
.getNumber().get(), message
.getDestination().orNull(), ignoreAttachments
);
1557 if (syncMessage
.getRequest().isPresent()) {
1558 RequestMessage rm
= syncMessage
.getRequest().get();
1559 if (rm
.isContactsRequest()) {
1562 } catch (UntrustedIdentityException
| IOException e
) {
1563 e
.printStackTrace();
1566 if (rm
.isGroupsRequest()) {
1569 } catch (UntrustedIdentityException
| IOException e
) {
1570 e
.printStackTrace();
1573 if (rm
.isBlockedListRequest()) {
1576 } catch (UntrustedIdentityException
| IOException e
) {
1577 e
.printStackTrace();
1580 // TODO Handle rm.isConfigurationRequest();
1582 if (syncMessage
.getGroups().isPresent()) {
1583 File tmpFile
= null;
1585 tmpFile
= IOUtils
.createTempFile();
1586 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1587 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1589 while ((g
= s
.read()) != null) {
1590 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1591 if (syncGroup
== null) {
1592 syncGroup
= new GroupInfo(g
.getId());
1594 if (g
.getName().isPresent()) {
1595 syncGroup
.name
= g
.getName().get();
1597 syncGroup
.addMembers(g
.getMembers());
1598 if (!g
.isActive()) {
1599 syncGroup
.members
.remove(username
);
1601 syncGroup
.blocked
= g
.isBlocked();
1602 if (g
.getColor().isPresent()) {
1603 syncGroup
.color
= g
.getColor().get();
1606 if (g
.getAvatar().isPresent()) {
1607 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1609 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1610 syncGroup
.archived
= g
.isArchived();
1611 account
.getGroupStore().updateGroup(syncGroup
);
1614 } catch (Exception e
) {
1615 e
.printStackTrace();
1617 if (tmpFile
!= null) {
1619 Files
.delete(tmpFile
.toPath());
1620 } catch (IOException e
) {
1621 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1626 if (syncMessage
.getBlockedList().isPresent()) {
1627 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1628 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1629 if (address
.getNumber().isPresent()) {
1631 setContactBlocked(address
.getNumber().get(), true);
1632 } catch (InvalidNumberException e
) {
1633 e
.printStackTrace();
1637 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1639 setGroupBlocked(groupId
, true);
1640 } catch (GroupNotFoundException e
) {
1641 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1645 if (syncMessage
.getContacts().isPresent()) {
1646 File tmpFile
= null;
1648 tmpFile
= IOUtils
.createTempFile();
1649 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1650 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1651 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1652 if (contactsMessage
.isComplete()) {
1653 account
.getContactStore().clear();
1656 while ((c
= s
.read()) != null) {
1657 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1658 account
.setProfileKey(c
.getProfileKey().get());
1660 ContactInfo contact
= account
.getContactStore().getContact(c
.getAddress().getNumber().get());
1661 if (contact
== null) {
1662 contact
= new ContactInfo();
1663 contact
.number
= c
.getAddress().getNumber().get();
1665 if (c
.getName().isPresent()) {
1666 contact
.name
= c
.getName().get();
1668 if (c
.getColor().isPresent()) {
1669 contact
.color
= c
.getColor().get();
1671 if (c
.getProfileKey().isPresent()) {
1672 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1674 if (c
.getVerified().isPresent()) {
1675 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1676 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1678 if (c
.getExpirationTimer().isPresent()) {
1679 ThreadInfo thread
= account
.getThreadStore().getThread(c
.getAddress().getNumber().get());
1680 if (thread
== null) {
1681 thread
= new ThreadInfo();
1682 thread
.id
= c
.getAddress().getNumber().get();
1684 thread
.messageExpirationTime
= c
.getExpirationTimer().get();
1685 account
.getThreadStore().updateThread(thread
);
1687 contact
.blocked
= c
.isBlocked();
1688 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1689 contact
.archived
= c
.isArchived();
1690 account
.getContactStore().updateContact(contact
);
1692 if (c
.getAvatar().isPresent()) {
1693 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1697 } catch (Exception e
) {
1698 e
.printStackTrace();
1700 if (tmpFile
!= null) {
1702 Files
.delete(tmpFile
.toPath());
1703 } catch (IOException e
) {
1704 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1709 if (syncMessage
.getVerified().isPresent()) {
1710 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1711 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1713 if (syncMessage
.getConfiguration().isPresent()) {
1720 private File
getContactAvatarFile(String number
) {
1721 return new File(avatarsPath
, "contact-" + number
);
1724 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
{
1725 IOUtils
.createPrivateDirectories(avatarsPath
);
1726 if (attachment
.isPointer()) {
1727 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1728 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1730 SignalServiceAttachmentStream stream
= attachment
.asStream();
1731 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1735 private File
getGroupAvatarFile(byte[] groupId
) {
1736 return new File(avatarsPath
, "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1739 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
{
1740 IOUtils
.createPrivateDirectories(avatarsPath
);
1741 if (attachment
.isPointer()) {
1742 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1743 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1745 SignalServiceAttachmentStream stream
= attachment
.asStream();
1746 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1750 public File
getAttachmentFile(long attachmentId
) {
1751 return new File(attachmentsPath
, attachmentId
+ "");
1754 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
{
1755 IOUtils
.createPrivateDirectories(attachmentsPath
);
1756 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getId()), true);
1759 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
{
1760 if (storePreview
&& pointer
.getPreview().isPresent()) {
1761 File previewFile
= new File(outputFile
+ ".preview");
1762 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1763 byte[] preview
= pointer
.getPreview().get();
1764 output
.write(preview
, 0, preview
.length
);
1765 } catch (FileNotFoundException e
) {
1766 e
.printStackTrace();
1771 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1773 File tmpFile
= IOUtils
.createTempFile();
1774 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
)) {
1775 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1776 byte[] buffer
= new byte[4096];
1779 while ((read
= input
.read(buffer
)) != -1) {
1780 output
.write(buffer
, 0, read
);
1782 } catch (FileNotFoundException e
) {
1783 e
.printStackTrace();
1788 Files
.delete(tmpFile
.toPath());
1789 } catch (IOException e
) {
1790 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1796 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
{
1797 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1798 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
);
1802 public boolean isRemote() {
1806 private void sendGroups() throws IOException
, UntrustedIdentityException
{
1807 File groupsFile
= IOUtils
.createTempFile();
1810 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1811 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1812 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1813 ThreadInfo info
= account
.getThreadStore().getThread(Base64
.encodeBytes(record.groupId
));
1814 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1815 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1816 record.members
.contains(username
), Optional
.fromNullable(info
!= null ? info
.messageExpirationTime
: null),
1817 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1821 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1822 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1823 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1824 .withStream(groupsFileStream
)
1825 .withContentType("application/octet-stream")
1826 .withLength(groupsFile
.length())
1829 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1834 Files
.delete(groupsFile
.toPath());
1835 } catch (IOException e
) {
1836 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1841 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1842 File contactsFile
= IOUtils
.createTempFile();
1845 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1846 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1847 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1848 VerifiedMessage verifiedMessage
= null;
1849 ThreadInfo info
= account
.getThreadStore().getThread(record.number
);
1850 if (getIdentities().containsKey(record.number
)) {
1851 JsonIdentityKeyStore
.Identity currentIdentity
= null;
1852 for (JsonIdentityKeyStore
.Identity id
: getIdentities().get(record.number
)) {
1853 if (currentIdentity
== null || id
.getDateAdded().after(currentIdentity
.getDateAdded())) {
1854 currentIdentity
= id
;
1857 if (currentIdentity
!= null) {
1858 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1862 ProfileKey profileKey
= null;
1864 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1865 } catch (InvalidInputException ignored
) {
1867 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1868 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1869 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1870 Optional
.fromNullable(info
!= null ? info
.messageExpirationTime
: null),
1871 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1874 if (account
.getProfileKey() != null) {
1875 // Send our own profile key as well
1876 out
.write(new DeviceContact(account
.getSelfAddress(),
1877 Optional
.absent(), Optional
.absent(),
1878 Optional
.absent(), Optional
.absent(),
1879 Optional
.of(account
.getProfileKey()),
1880 false, Optional
.absent(), Optional
.absent(), false));
1884 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1885 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1886 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1887 .withStream(contactsFileStream
)
1888 .withContentType("application/octet-stream")
1889 .withLength(contactsFile
.length())
1892 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1897 Files
.delete(contactsFile
.toPath());
1898 } catch (IOException e
) {
1899 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1904 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1905 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1906 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1907 if (record.blocked
) {
1908 addresses
.add(record.getAddress());
1911 List
<byte[]> groupIds
= new ArrayList
<>();
1912 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1913 if (record.blocked
) {
1914 groupIds
.add(record.groupId
);
1917 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1920 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1921 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1922 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1925 public List
<ContactInfo
> getContacts() {
1926 return account
.getContactStore().getContacts();
1929 public ContactInfo
getContact(String number
) {
1930 return account
.getContactStore().getContact(number
);
1933 public GroupInfo
getGroup(byte[] groupId
) {
1934 return account
.getGroupStore().getGroup(groupId
);
1937 public Map
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities() {
1938 return account
.getSignalProtocolStore().getIdentities();
1941 public Pair
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities(String number
) throws InvalidNumberException
{
1942 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, username
);
1943 return new Pair
<>(canonicalizedNumber
, account
.getSignalProtocolStore().getIdentities(canonicalizedNumber
));
1947 * Trust this the identity with this fingerprint
1949 * @param name username of the identity
1950 * @param fingerprint Fingerprint
1952 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) {
1953 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1957 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1958 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1962 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1964 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1965 } catch (IOException
| UntrustedIdentityException e
) {
1966 e
.printStackTrace();
1975 * Trust this the identity with this safety number
1977 * @param name username of the identity
1978 * @param safetyNumber Safety number
1980 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) {
1981 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1985 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1986 if (!safetyNumber
.equals(computeSafetyNumber(name
, id
.getIdentityKey()))) {
1990 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1992 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1993 } catch (IOException
| UntrustedIdentityException e
) {
1994 e
.printStackTrace();
2003 * Trust all keys of this identity without verification
2005 * @param name username of the identity
2007 public boolean trustIdentityAllKeys(String name
) {
2008 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
2012 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2013 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2014 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2016 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2017 } catch (IOException
| UntrustedIdentityException e
) {
2018 e
.printStackTrace();
2026 public String
computeSafetyNumber(String theirUsername
, IdentityKey theirIdentityKey
) {
2027 return Utils
.computeSafetyNumber(username
, getIdentity(), theirUsername
, theirIdentityKey
);
2030 public interface ReceiveMessageHandler
{
2032 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);