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