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
.util
.IOUtils
;
35 import org
.asamk
.signal
.util
.Util
;
36 import org
.signal
.libsignal
.metadata
.InvalidMetadataMessageException
;
37 import org
.signal
.libsignal
.metadata
.InvalidMetadataVersionException
;
38 import org
.signal
.libsignal
.metadata
.ProtocolDuplicateMessageException
;
39 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyException
;
40 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyIdException
;
41 import org
.signal
.libsignal
.metadata
.ProtocolInvalidMessageException
;
42 import org
.signal
.libsignal
.metadata
.ProtocolInvalidVersionException
;
43 import org
.signal
.libsignal
.metadata
.ProtocolLegacyMessageException
;
44 import org
.signal
.libsignal
.metadata
.ProtocolNoSessionException
;
45 import org
.signal
.libsignal
.metadata
.ProtocolUntrustedIdentityException
;
46 import org
.signal
.libsignal
.metadata
.SelfSendException
;
47 import org
.signal
.libsignal
.metadata
.certificate
.InvalidCertificateException
;
48 import org
.signal
.zkgroup
.InvalidInputException
;
49 import org
.signal
.zkgroup
.VerificationFailedException
;
50 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
51 import org
.whispersystems
.libsignal
.IdentityKey
;
52 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
53 import org
.whispersystems
.libsignal
.InvalidKeyException
;
54 import org
.whispersystems
.libsignal
.InvalidMessageException
;
55 import org
.whispersystems
.libsignal
.InvalidVersionException
;
56 import org
.whispersystems
.libsignal
.ecc
.Curve
;
57 import org
.whispersystems
.libsignal
.ecc
.ECKeyPair
;
58 import org
.whispersystems
.libsignal
.ecc
.ECPublicKey
;
59 import org
.whispersystems
.libsignal
.state
.PreKeyRecord
;
60 import org
.whispersystems
.libsignal
.state
.SignedPreKeyRecord
;
61 import org
.whispersystems
.libsignal
.util
.KeyHelper
;
62 import org
.whispersystems
.libsignal
.util
.Medium
;
63 import org
.whispersystems
.libsignal
.util
.Pair
;
64 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
65 import org
.whispersystems
.signalservice
.api
.SignalServiceAccountManager
;
66 import org
.whispersystems
.signalservice
.api
.SignalServiceMessagePipe
;
67 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageReceiver
;
68 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
69 import org
.whispersystems
.signalservice
.api
.crypto
.InvalidCiphertextException
;
70 import org
.whispersystems
.signalservice
.api
.crypto
.ProfileCipher
;
71 import org
.whispersystems
.signalservice
.api
.crypto
.SignalServiceCipher
;
72 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccess
;
73 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
74 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
75 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
76 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
77 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentPointer
;
78 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
79 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceContent
;
80 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
81 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceEnvelope
;
82 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
83 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
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
.EncapsulatedExceptions
;
103 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NetworkFailureException
;
104 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
105 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
106 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
107 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
108 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
109 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
110 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
111 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
112 import org
.whispersystems
.util
.Base64
;
115 import java
.io
.FileInputStream
;
116 import java
.io
.FileNotFoundException
;
117 import java
.io
.FileOutputStream
;
118 import java
.io
.IOException
;
119 import java
.io
.InputStream
;
120 import java
.io
.OutputStream
;
122 import java
.net
.URISyntaxException
;
123 import java
.net
.URLEncoder
;
124 import java
.nio
.file
.Files
;
125 import java
.nio
.file
.Paths
;
126 import java
.nio
.file
.StandardCopyOption
;
127 import java
.util
.ArrayList
;
128 import java
.util
.Arrays
;
129 import java
.util
.Collection
;
130 import java
.util
.Collections
;
131 import java
.util
.Date
;
132 import java
.util
.HashSet
;
133 import java
.util
.LinkedList
;
134 import java
.util
.List
;
135 import java
.util
.Locale
;
136 import java
.util
.Map
;
137 import java
.util
.Objects
;
138 import java
.util
.Set
;
139 import java
.util
.UUID
;
140 import java
.util
.concurrent
.TimeUnit
;
141 import java
.util
.concurrent
.TimeoutException
;
142 import java
.util
.zip
.ZipEntry
;
143 import java
.util
.zip
.ZipFile
;
145 public class Manager
implements Signal
{
147 private static final SignalServiceProfile
.Capabilities capabilities
= new SignalServiceProfile
.Capabilities(false, false);
149 private final String settingsPath
;
150 private final String dataPath
;
151 private final String attachmentsPath
;
152 private final String avatarsPath
;
153 private final SleepTimer timer
= new UptimeSleepTimer();
155 private SignalAccount account
;
156 private String username
;
157 private SignalServiceAccountManager accountManager
;
158 private SignalServiceMessagePipe messagePipe
= null;
159 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
161 public Manager(String username
, String settingsPath
) {
162 this.username
= username
;
163 this.settingsPath
= settingsPath
;
164 this.dataPath
= this.settingsPath
+ "/data";
165 this.attachmentsPath
= this.settingsPath
+ "/attachments";
166 this.avatarsPath
= this.settingsPath
+ "/avatars";
170 public String
getUsername() {
174 public SignalServiceAddress
getSelfAddress() {
175 return account
.getSelfAddress();
178 private SignalServiceAccountManager
getSignalServiceAccountManager() {
179 return new SignalServiceAccountManager(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), BaseConfig
.USER_AGENT
, timer
);
182 private IdentityKey
getIdentity() {
183 return account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
186 public int getDeviceId() {
187 return account
.getDeviceId();
190 private String
getMessageCachePath() {
191 return this.dataPath
+ "/" + username
+ ".d/msg-cache";
194 private String
getMessageCachePath(String sender
) {
195 return getMessageCachePath() + "/" + sender
.replace("/", "_");
198 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
199 String cachePath
= getMessageCachePath(sender
);
200 IOUtils
.createPrivateDirectories(cachePath
);
201 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
204 public boolean userHasKeys() {
205 return account
!= null && account
.getSignalProtocolStore() != null;
208 public void init() throws IOException
{
209 if (!SignalAccount
.userExists(dataPath
, username
)) {
212 account
= SignalAccount
.load(dataPath
, username
);
214 migrateLegacyConfigs();
216 accountManager
= getSignalServiceAccountManager();
217 if (account
.isRegistered()) {
218 if (accountManager
.getPreKeysCount() < BaseConfig
.PREKEY_MINIMUM_COUNT
) {
222 if (account
.getUuid() == null) {
223 account
.setUuid(accountManager
.getOwnUuid());
229 private void migrateLegacyConfigs() {
230 // Copy group avatars that were previously stored in the attachments folder
231 // to the new avatar folder
232 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
233 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
234 File avatarFile
= getGroupAvatarFile(g
.groupId
);
235 File attachmentFile
= getAttachmentFile(g
.getAvatarId());
236 if (!avatarFile
.exists() && attachmentFile
.exists()) {
238 IOUtils
.createPrivateDirectories(avatarsPath
);
239 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
240 } catch (Exception e
) {
245 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
248 if (account
.getProfileKey() == null) {
249 // Old config file, creating new profile key
250 account
.setProfileKey(KeyUtils
.createProfileKey());
255 private void createNewIdentity() throws IOException
{
256 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
257 int registrationId
= KeyHelper
.generateRegistrationId(false);
258 if (username
== null) {
259 account
= SignalAccount
.createTemporaryAccount(identityKey
, registrationId
);
261 ProfileKey profileKey
= KeyUtils
.createProfileKey();
262 account
= SignalAccount
.create(dataPath
, username
, identityKey
, registrationId
, profileKey
);
267 public boolean isRegistered() {
268 return account
!= null && account
.isRegistered();
271 public void register(boolean voiceVerification
) throws IOException
{
272 if (account
== null) {
275 account
.setPassword(KeyUtils
.createPassword());
276 account
.setUuid(null);
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
, ret
.getUuid(), 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 UUID uuid
= 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
.setUuid(uuid
);
433 account
.setRegistrationLockPin(pin
);
439 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
440 if (pin
.isPresent()) {
441 account
.setRegistrationLockPin(pin
.get());
442 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
444 account
.setRegistrationLockPin(null);
445 accountManager
.removeV1Pin();
450 private void refreshPreKeys() throws IOException
{
451 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
452 final IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
453 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
455 accountManager
.setPreKeys(getIdentity(), signedPreKeyRecord
, oneTimePreKeys
);
458 private SignalServiceMessageReceiver
getMessageReceiver() {
459 return new SignalServiceMessageReceiver(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), BaseConfig
.USER_AGENT
, null, timer
);
462 private SignalServiceMessageSender
getMessageSender() {
463 return new SignalServiceMessageSender(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
464 account
.getDeviceId(), account
.getSignalProtocolStore(), BaseConfig
.USER_AGENT
, account
.isMultiDevice(), Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent());
467 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
468 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
473 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
474 } catch (IOException ignored
) {
478 SignalServiceMessageReceiver receiver
= getMessageReceiver();
480 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
481 } catch (VerificationFailedException e
) {
482 throw new AssertionError(e
);
486 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
487 File file
= getGroupAvatarFile(groupId
);
488 if (!file
.exists()) {
489 return Optional
.absent();
492 return Optional
.of(Utils
.createAttachment(file
));
495 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
496 File file
= getContactAvatarFile(number
);
497 if (!file
.exists()) {
498 return Optional
.absent();
501 return Optional
.of(Utils
.createAttachment(file
));
504 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
505 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
507 throw new GroupNotFoundException(groupId
);
509 if (!g
.isMember(account
.getSelfAddress())) {
510 throw new NotAGroupMemberException(groupId
, g
.name
);
515 public List
<GroupInfo
> getGroups() {
516 return account
.getGroupStore().getGroups();
520 public void sendGroupMessage(String messageText
, List
<String
> attachments
,
522 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
523 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
524 if (attachments
!= null) {
525 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
527 if (groupId
!= null) {
528 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
531 messageBuilder
.asGroupMessage(group
);
534 final GroupInfo g
= getGroupForSending(groupId
);
536 messageBuilder
.withExpiration(g
.messageExpirationTime
);
538 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
541 public void sendGroupMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
542 long targetSentTimestamp
, byte[] groupId
)
543 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
{
544 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
545 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
546 .withReaction(reaction
);
547 if (groupId
!= null) {
548 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
551 messageBuilder
.asGroupMessage(group
);
553 final GroupInfo g
= getGroupForSending(groupId
);
554 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
557 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
{
558 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
562 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
563 .asGroupMessage(group
);
565 final GroupInfo g
= getGroupForSending(groupId
);
566 g
.removeMember(account
.getSelfAddress());
567 account
.getGroupStore().updateGroup(g
);
569 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
572 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
574 if (groupId
== null) {
576 g
= new GroupInfo(KeyUtils
.createGroupId());
577 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
579 g
= getGroupForSending(groupId
);
586 if (members
!= null) {
587 final Set
<String
> newE164Members
= new HashSet
<>();
588 for (SignalServiceAddress member
: members
) {
589 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
592 newE164Members
.add(member
.getNumber().get());
595 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
596 if (contacts
.size() != newE164Members
.size()) {
597 // Some of the new members are not registered on Signal
598 for (ContactTokenDetails contact
: contacts
) {
599 newE164Members
.remove(contact
.getNumber());
601 System
.err
.println("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
602 System
.err
.println("Aborting…");
606 g
.addMembers(members
);
609 if (avatarFile
!= null) {
610 IOUtils
.createPrivateDirectories(avatarsPath
);
611 File aFile
= getGroupAvatarFile(g
.groupId
);
612 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
615 account
.getGroupStore().updateGroup(g
);
617 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
619 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
623 private void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
624 if (groupId
== null) {
627 GroupInfo g
= getGroupForSending(groupId
);
629 if (!g
.isMember(recipient
)) {
633 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
635 // Send group message only to the recipient who requested it
636 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
639 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
640 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
643 .withMembers(new ArrayList
<>(g
.getMembers()));
645 File aFile
= getGroupAvatarFile(g
.groupId
);
646 if (aFile
.exists()) {
648 group
.withAvatar(Utils
.createAttachment(aFile
));
649 } catch (IOException e
) {
650 throw new AttachmentInvalidException(aFile
.toString(), e
);
654 return SignalServiceDataMessage
.newBuilder()
655 .asGroupMessage(group
.build())
656 .withExpiration(g
.messageExpirationTime
);
659 private void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
660 if (groupId
== null) {
664 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
667 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
668 .asGroupMessage(group
.build());
670 // Send group info request message to the recipient who sent us a message with this groupId
671 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
674 private void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
675 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
676 Collections
.singletonList(messageId
),
677 System
.currentTimeMillis());
679 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
683 public void sendMessage(String message
, List
<String
> attachments
, String recipient
)
684 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
, InvalidNumberException
{
685 List
<String
> recipients
= new ArrayList
<>(1);
686 recipients
.add(recipient
);
687 sendMessage(message
, attachments
, recipients
);
691 public void sendMessage(String messageText
, List
<String
> attachments
,
692 List
<String
> recipients
)
693 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
694 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
695 if (attachments
!= null) {
696 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
698 // Upload attachments here, so we only upload once even for multiple recipients
699 SignalServiceMessageSender messageSender
= getMessageSender();
700 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
701 for (SignalServiceAttachment attachment
: attachmentStreams
) {
702 if (attachment
.isStream()) {
703 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
704 } else if (attachment
.isPointer()) {
705 attachmentPointers
.add(attachment
.asPointer());
709 messageBuilder
.withAttachments(attachmentPointers
);
711 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
714 public void sendMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
715 long targetSentTimestamp
, List
<String
> recipients
)
716 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
717 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
718 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
719 .withReaction(reaction
);
720 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
724 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
725 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
726 .asEndSessionMessage();
728 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
732 public String
getContactName(String number
) throws InvalidNumberException
{
733 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
734 ContactInfo contact
= account
.getContactStore().getContact(new SignalServiceAddress(null, canonicalizedNumber
));
735 if (contact
== null) {
743 public void setContactName(String number
, String name
) throws InvalidNumberException
{
744 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
745 final SignalServiceAddress address
= new SignalServiceAddress(null, canonicalizedNumber
);
746 ContactInfo contact
= account
.getContactStore().getContact(address
);
747 if (contact
== null) {
748 contact
= new ContactInfo(address
);
749 System
.err
.println("Add contact " + canonicalizedNumber
+ " named " + name
);
751 System
.err
.println("Updating contact " + canonicalizedNumber
+ " name " + contact
.name
+ " -> " + name
);
754 account
.getContactStore().updateContact(contact
);
759 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
760 number
= Utils
.canonicalizeNumber(number
, account
.getUsername());
761 final SignalServiceAddress address
= new SignalServiceAddress(null, number
);
762 ContactInfo contact
= account
.getContactStore().getContact(address
);
763 if (contact
== null) {
764 contact
= new ContactInfo(address
);
765 System
.err
.println("Adding and " + (blocked ?
"blocking" : "unblocking") + " contact " + number
);
767 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " contact " + number
);
769 contact
.blocked
= blocked
;
770 account
.getContactStore().updateContact(contact
);
775 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
776 GroupInfo group
= getGroup(groupId
);
778 throw new GroupNotFoundException(groupId
);
780 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " group " + Base64
.encodeBytes(groupId
));
781 group
.blocked
= blocked
;
782 account
.getGroupStore().updateGroup(group
);
788 public List
<byte[]> getGroupIds() {
789 List
<GroupInfo
> groups
= getGroups();
790 List
<byte[]> ids
= new ArrayList
<>(groups
.size());
791 for (GroupInfo group
: groups
) {
792 ids
.add(group
.groupId
);
798 public String
getGroupName(byte[] groupId
) {
799 GroupInfo group
= getGroup(groupId
);
808 public List
<String
> getGroupMembers(byte[] groupId
) {
809 GroupInfo group
= getGroup(groupId
);
811 return Collections
.emptyList();
813 return new ArrayList
<>(group
.getMembersE164());
818 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
819 if (groupId
.length
== 0) {
822 if (name
.isEmpty()) {
825 if (members
.size() == 0) {
828 if (avatar
.isEmpty()) {
831 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
835 * Change the expiration timer for a contact
837 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) {
838 ContactInfo c
= account
.getContactStore().getContact(address
);
839 c
.messageExpirationTime
= messageExpirationTimer
;
840 account
.getContactStore().updateContact(c
);
844 * Change the expiration timer for a group
846 public void setExpirationTimer(byte[] groupId
, int messageExpirationTimer
) {
847 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
848 g
.messageExpirationTime
= messageExpirationTimer
;
849 account
.getGroupStore().updateGroup(g
);
853 * Upload the sticker pack from path.
855 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
856 * @return if successful, returns the URL to install the sticker pack in the signal app
858 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
859 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
861 SignalServiceMessageSender messageSender
= getMessageSender();
863 byte[] packKey
= KeyUtils
.createStickerUploadKey();
864 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
867 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, "utf-8") + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), "utf-8"))
869 } catch (URISyntaxException e
) {
870 throw new AssertionError(e
);
874 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
876 String rootPath
= null;
878 final File file
= new File(path
);
879 if (file
.getName().endsWith(".zip")) {
880 zip
= new ZipFile(file
);
881 } else if (file
.getName().equals("manifest.json")) {
882 rootPath
= file
.getParent();
884 throw new StickerPackInvalidException("Could not find manifest.json");
887 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
889 if (pack
.stickers
== null) {
890 throw new StickerPackInvalidException("Must set a 'stickers' field.");
893 if (pack
.stickers
.isEmpty()) {
894 throw new StickerPackInvalidException("Must include stickers.");
897 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
898 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
899 if (sticker
.file
== null) {
900 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
903 Pair
<InputStream
, Long
> data
;
905 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
906 } catch (IOException ignored
) {
907 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
910 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""));
911 stickers
.add(stickerInfo
);
914 StickerInfo cover
= null;
915 if (pack
.cover
!= null) {
916 if (pack
.cover
.file
== null) {
917 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
920 Pair
<InputStream
, Long
> data
;
922 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
923 } catch (IOException ignored
) {
924 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
927 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""));
930 return new SignalServiceStickerManifestUpload(
937 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
938 InputStream inputStream
;
940 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
942 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
944 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
947 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
949 final ZipEntry entry
= zip
.getEntry(subfile
);
950 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
952 final File file
= new File(rootPath
, subfile
);
953 return new Pair
<>(new FileInputStream(file
), file
.length());
957 private void requestSyncGroups() throws IOException
{
958 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
959 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
961 sendSyncMessage(message
);
962 } catch (UntrustedIdentityException e
) {
967 private void requestSyncContacts() throws IOException
{
968 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
969 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
971 sendSyncMessage(message
);
972 } catch (UntrustedIdentityException e
) {
977 private void requestSyncBlocked() throws IOException
{
978 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
979 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
981 sendSyncMessage(message
);
982 } catch (UntrustedIdentityException e
) {
987 private void requestSyncConfiguration() throws IOException
{
988 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
989 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
991 sendSyncMessage(message
);
992 } catch (UntrustedIdentityException e
) {
997 private byte[] getSenderCertificate() throws IOException
{
998 byte[] certificate
= accountManager
.getSenderCertificate();
999 // TODO cache for a day
1003 private byte[] getSelfUnidentifiedAccessKey() {
1004 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
1007 private static SignalProfile
decryptProfile(SignalServiceProfile encryptedProfile
, ProfileKey profileKey
) throws IOException
{
1008 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
1010 return new SignalProfile(
1011 encryptedProfile
.getIdentityKey(),
1012 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
1013 encryptedProfile
.getAvatar(),
1014 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
1015 encryptedProfile
.isUnrestrictedUnidentifiedAccess()
1017 } catch (InvalidCiphertextException e
) {
1022 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) throws IOException
{
1023 ContactInfo contact
= account
.getContactStore().getContact(recipient
);
1024 if (contact
== null || contact
.profileKey
== null) {
1027 ProfileKey theirProfileKey
;
1029 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
1030 } catch (InvalidInputException e
) {
1031 throw new AssertionError(e
);
1033 SignalProfile targetProfile
= decryptProfile(getRecipientProfile(recipient
, Optional
.absent()), theirProfileKey
);
1035 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1039 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1040 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1043 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1046 private Optional
<UnidentifiedAccessPair
> getAccessForSync() throws IOException
{
1047 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1048 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1050 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1051 return Optional
.absent();
1055 return Optional
.of(new UnidentifiedAccessPair(
1056 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1057 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1059 } catch (InvalidCertificateException e
) {
1060 return Optional
.absent();
1064 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) throws IOException
{
1065 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1066 for (SignalServiceAddress recipient
: recipients
) {
1067 result
.add(getAccessFor(recipient
));
1072 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) throws IOException
{
1073 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1074 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1075 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1077 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1078 return Optional
.absent();
1082 return Optional
.of(new UnidentifiedAccessPair(
1083 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1084 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1086 } catch (InvalidCertificateException e
) {
1087 return Optional
.absent();
1091 private void sendSyncMessage(SignalServiceSyncMessage message
)
1092 throws IOException
, UntrustedIdentityException
{
1093 SignalServiceMessageSender messageSender
= getMessageSender();
1095 messageSender
.sendMessage(message
, getAccessForSync());
1096 } catch (UntrustedIdentityException e
) {
1097 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1103 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1105 private void sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1106 throws EncapsulatedExceptions
, IOException
{
1107 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1109 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1110 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1111 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1113 for (SendMessageResult result
: results
) {
1114 if (result
.isUnregisteredFailure()) {
1115 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getNumber().get(), null));
1116 } else if (result
.isNetworkFailure()) {
1117 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getNumber().get(), null));
1118 } else if (result
.getIdentityFailure() != null) {
1119 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getNumber().get(), result
.getIdentityFailure().getIdentityKey()));
1122 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1123 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1127 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1128 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1129 final String username
= account
.getUsername();
1131 for (String number
: numbers
) {
1132 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, username
);
1133 if (canonicalizedNumber
.equals(username
)) {
1134 signalServiceAddresses
.add(account
.getSelfAddress());
1136 SignalServiceAddress address
= new SignalServiceAddress(null, canonicalizedNumber
);
1137 ContactInfo contact
= account
.getContactStore().getContact(address
);
1138 signalServiceAddresses
.add(contact
== null
1140 : contact
.getAddress());
1143 return signalServiceAddresses
;
1146 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1147 throws IOException
{
1148 if (messagePipe
== null) {
1149 messagePipe
= getMessageReceiver().createMessagePipe();
1151 if (unidentifiedMessagePipe
== null) {
1152 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1154 SignalServiceDataMessage message
= null;
1156 SignalServiceMessageSender messageSender
= getMessageSender();
1158 message
= messageBuilder
.build();
1159 if (message
.getGroupInfo().isPresent()) {
1161 final boolean isRecipientUpdate
= false;
1162 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1163 for (SendMessageResult r
: result
) {
1164 if (r
.getIdentityFailure() != null) {
1165 account
.getSignalProtocolStore().saveIdentity(r
.getAddress().getNumber().get(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1169 } catch (UntrustedIdentityException e
) {
1170 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1171 return Collections
.emptyList();
1173 } else if (recipients
.size() == 1 && recipients
.contains(account
.getSelfAddress())) {
1174 SignalServiceAddress recipient
= account
.getSelfAddress();
1175 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1176 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1177 message
.getTimestamp(),
1179 message
.getExpiresInSeconds(),
1180 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1182 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1184 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1186 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1187 } catch (UntrustedIdentityException e
) {
1188 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1189 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1193 // Send to all individually, so sync messages are sent correctly
1194 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1195 for (SignalServiceAddress address
: recipients
) {
1196 ContactInfo contact
= account
.getContactStore().getContact(address
);
1197 if (contact
!= null) {
1198 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1199 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1201 messageBuilder
.withExpiration(0);
1202 messageBuilder
.withProfileKey(null);
1204 message
= messageBuilder
.build();
1206 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1207 results
.add(result
);
1208 } catch (UntrustedIdentityException e
) {
1209 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1210 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1216 if (message
!= null && message
.isEndSession()) {
1217 for (SignalServiceAddress recipient
: recipients
) {
1218 handleEndSession(recipient
.getNumber().get());
1225 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, ProtocolUntrustedIdentityException
, SelfSendException
, UnsupportedDataMessageException
{
1226 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1228 return cipher
.decrypt(envelope
);
1229 } catch (ProtocolUntrustedIdentityException e
) {
1230 // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else
1231 // account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
1236 private void handleEndSession(String source
) {
1237 account
.getSignalProtocolStore().deleteAllSessions(source
);
1240 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1241 if (message
.getGroupInfo().isPresent()) {
1242 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1243 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1244 switch (groupInfo
.getType()) {
1246 if (group
== null) {
1247 group
= new GroupInfo(groupInfo
.getGroupId());
1250 if (groupInfo
.getAvatar().isPresent()) {
1251 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1252 if (avatar
.isPointer()) {
1254 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1255 } catch (IOException
| InvalidMessageException e
) {
1256 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getId() + "): " + e
.getMessage());
1261 if (groupInfo
.getName().isPresent()) {
1262 group
.name
= groupInfo
.getName().get();
1265 if (groupInfo
.getMembers().isPresent()) {
1266 group
.addMembers(groupInfo
.getMembers().get());
1269 account
.getGroupStore().updateGroup(group
);
1272 if (group
== null) {
1274 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1275 } catch (IOException
| EncapsulatedExceptions e
) {
1276 e
.printStackTrace();
1281 if (group
== null) {
1283 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1284 } catch (IOException
| EncapsulatedExceptions e
) {
1285 e
.printStackTrace();
1288 group
.removeMember(source
);
1289 account
.getGroupStore().updateGroup(group
);
1293 if (group
!= null) {
1295 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1296 } catch (IOException
| EncapsulatedExceptions e
) {
1297 e
.printStackTrace();
1298 } catch (NotAGroupMemberException e
) {
1299 // We have left this group, so don't send a group update message
1305 if (message
.isEndSession()) {
1306 handleEndSession(isSync ? destination
.getNumber().get() : source
.getNumber().get());
1308 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1309 if (message
.getGroupInfo().isPresent()) {
1310 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1311 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1312 if (group
== null) {
1313 group
= new GroupInfo(groupInfo
.getGroupId());
1315 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1316 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1317 account
.getGroupStore().updateGroup(group
);
1320 ContactInfo contact
= account
.getContactStore().getContact(isSync ? destination
: source
);
1321 if (contact
== null) {
1322 contact
= new ContactInfo(isSync ? destination
: source
);
1324 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1325 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1326 account
.getContactStore().updateContact(contact
);
1330 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1331 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1332 if (attachment
.isPointer()) {
1334 retrieveAttachment(attachment
.asPointer());
1335 } catch (IOException
| InvalidMessageException e
) {
1336 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getId() + "): " + e
.getMessage());
1341 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1342 if (source
.matches(account
.getSelfAddress())) {
1344 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1345 } catch (InvalidInputException ignored
) {
1347 ContactInfo contact
= account
.getContactStore().getContact(source
);
1348 if (contact
!= null) {
1349 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1350 account
.getContactStore().updateContact(contact
);
1353 ContactInfo contact
= account
.getContactStore().getContact(source
);
1354 if (contact
== null) {
1355 contact
= new ContactInfo(source
);
1357 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1358 account
.getContactStore().updateContact(contact
);
1361 if (message
.getPreviews().isPresent()) {
1362 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1363 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1364 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1365 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1367 retrieveAttachment(attachment
);
1368 } catch (IOException
| InvalidMessageException e
) {
1369 System
.err
.println("Failed to retrieve attachment (" + attachment
.getId() + "): " + e
.getMessage());
1376 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1377 final File cachePath
= new File(getMessageCachePath());
1378 if (!cachePath
.exists()) {
1381 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1382 if (!dir
.isDirectory()) {
1386 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1387 if (!fileEntry
.isFile()) {
1390 SignalServiceEnvelope envelope
;
1392 envelope
= Utils
.loadEnvelope(fileEntry
);
1393 if (envelope
== null) {
1396 } catch (IOException e
) {
1397 e
.printStackTrace();
1400 SignalServiceContent content
= null;
1401 if (!envelope
.isReceipt()) {
1403 content
= decryptMessage(envelope
);
1404 } catch (Exception e
) {
1407 handleMessage(envelope
, content
, ignoreAttachments
);
1410 handler
.handleMessage(envelope
, content
, null);
1412 Files
.delete(fileEntry
.toPath());
1413 } catch (IOException e
) {
1414 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1417 // Try to delete directory if empty
1422 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1423 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1424 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1427 if (messagePipe
== null) {
1428 messagePipe
= messageReceiver
.createMessagePipe();
1432 SignalServiceEnvelope envelope
;
1433 SignalServiceContent content
= null;
1434 Exception exception
= null;
1435 final long now
= new Date().getTime();
1437 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1438 // store message on disk, before acknowledging receipt to the server
1440 File cacheFile
= getMessageCacheFile(envelope1
.getSourceE164().get(), now
, envelope1
.getTimestamp());
1441 Utils
.storeEnvelope(envelope1
, cacheFile
);
1442 } catch (IOException e
) {
1443 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1446 } catch (TimeoutException e
) {
1447 if (returnOnTimeout
)
1450 } catch (InvalidVersionException e
) {
1451 System
.err
.println("Ignoring error: " + e
.getMessage());
1454 if (!envelope
.isReceipt()) {
1456 content
= decryptMessage(envelope
);
1457 } catch (Exception e
) {
1460 handleMessage(envelope
, content
, ignoreAttachments
);
1463 if (!isMessageBlocked(envelope
, content
)) {
1464 handler
.handleMessage(envelope
, content
, exception
);
1466 if (!(exception
instanceof ProtocolUntrustedIdentityException
)) {
1467 File cacheFile
= null;
1469 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1470 Files
.delete(cacheFile
.toPath());
1471 // Try to delete directory if empty
1472 new File(getMessageCachePath()).delete();
1473 } catch (IOException e
) {
1474 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1479 if (messagePipe
!= null) {
1480 messagePipe
.shutdown();
1486 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1487 SignalServiceAddress source
;
1488 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1489 source
= envelope
.getSourceAddress();
1490 } else if (content
!= null) {
1491 source
= content
.getSender();
1495 ContactInfo sourceContact
= getContact(source
.getNumber().get());
1496 if (sourceContact
!= null && sourceContact
.blocked
) {
1500 if (content
!= null && content
.getDataMessage().isPresent()) {
1501 SignalServiceDataMessage message
= content
.getDataMessage().get();
1502 if (message
.getGroupInfo().isPresent()) {
1503 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1504 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1505 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1513 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1514 if (content
!= null) {
1515 SignalServiceAddress sender
;
1516 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1517 sender
= envelope
.getSourceAddress();
1519 sender
= content
.getSender();
1521 if (content
.getDataMessage().isPresent()) {
1522 SignalServiceDataMessage message
= content
.getDataMessage().get();
1524 if (content
.isNeedsReceipt()) {
1526 sendReceipt(sender
, message
.getTimestamp());
1527 } catch (IOException
| UntrustedIdentityException e
) {
1528 e
.printStackTrace();
1532 handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
);
1534 if (content
.getSyncMessage().isPresent()) {
1535 account
.setMultiDevice(true);
1536 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1537 if (syncMessage
.getSent().isPresent()) {
1538 SentTranscriptMessage message
= syncMessage
.getSent().get();
1539 handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
);
1541 if (syncMessage
.getRequest().isPresent()) {
1542 RequestMessage rm
= syncMessage
.getRequest().get();
1543 if (rm
.isContactsRequest()) {
1546 } catch (UntrustedIdentityException
| IOException e
) {
1547 e
.printStackTrace();
1550 if (rm
.isGroupsRequest()) {
1553 } catch (UntrustedIdentityException
| IOException e
) {
1554 e
.printStackTrace();
1557 if (rm
.isBlockedListRequest()) {
1560 } catch (UntrustedIdentityException
| IOException e
) {
1561 e
.printStackTrace();
1564 // TODO Handle rm.isConfigurationRequest();
1566 if (syncMessage
.getGroups().isPresent()) {
1567 File tmpFile
= null;
1569 tmpFile
= IOUtils
.createTempFile();
1570 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1571 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1573 while ((g
= s
.read()) != null) {
1574 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1575 if (syncGroup
== null) {
1576 syncGroup
= new GroupInfo(g
.getId());
1578 if (g
.getName().isPresent()) {
1579 syncGroup
.name
= g
.getName().get();
1581 syncGroup
.addMembers(g
.getMembers());
1582 if (!g
.isActive()) {
1583 syncGroup
.removeMember(account
.getSelfAddress());
1585 // Add ourself to the member set as it's marked as active
1586 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1588 syncGroup
.blocked
= g
.isBlocked();
1589 if (g
.getColor().isPresent()) {
1590 syncGroup
.color
= g
.getColor().get();
1593 if (g
.getAvatar().isPresent()) {
1594 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1596 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1597 syncGroup
.archived
= g
.isArchived();
1598 account
.getGroupStore().updateGroup(syncGroup
);
1601 } catch (Exception e
) {
1602 e
.printStackTrace();
1604 if (tmpFile
!= null) {
1606 Files
.delete(tmpFile
.toPath());
1607 } catch (IOException e
) {
1608 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1613 if (syncMessage
.getBlockedList().isPresent()) {
1614 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1615 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1616 if (address
.getNumber().isPresent()) {
1618 setContactBlocked(address
.getNumber().get(), true);
1619 } catch (InvalidNumberException e
) {
1620 e
.printStackTrace();
1624 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1626 setGroupBlocked(groupId
, true);
1627 } catch (GroupNotFoundException e
) {
1628 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1632 if (syncMessage
.getContacts().isPresent()) {
1633 File tmpFile
= null;
1635 tmpFile
= IOUtils
.createTempFile();
1636 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1637 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1638 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1639 if (contactsMessage
.isComplete()) {
1640 account
.getContactStore().clear();
1643 while ((c
= s
.read()) != null) {
1644 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1645 account
.setProfileKey(c
.getProfileKey().get());
1647 ContactInfo contact
= account
.getContactStore().getContact(c
.getAddress());
1648 if (contact
== null) {
1649 contact
= new ContactInfo(c
.getAddress());
1651 if (c
.getName().isPresent()) {
1652 contact
.name
= c
.getName().get();
1654 if (c
.getColor().isPresent()) {
1655 contact
.color
= c
.getColor().get();
1657 if (c
.getProfileKey().isPresent()) {
1658 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1660 if (c
.getVerified().isPresent()) {
1661 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1662 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1664 if (c
.getExpirationTimer().isPresent()) {
1665 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1667 contact
.blocked
= c
.isBlocked();
1668 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1669 contact
.archived
= c
.isArchived();
1670 account
.getContactStore().updateContact(contact
);
1672 if (c
.getAvatar().isPresent()) {
1673 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1677 } catch (Exception e
) {
1678 e
.printStackTrace();
1680 if (tmpFile
!= null) {
1682 Files
.delete(tmpFile
.toPath());
1683 } catch (IOException e
) {
1684 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1689 if (syncMessage
.getVerified().isPresent()) {
1690 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1691 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1693 if (syncMessage
.getConfiguration().isPresent()) {
1700 private File
getContactAvatarFile(String number
) {
1701 return new File(avatarsPath
, "contact-" + number
);
1704 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
{
1705 IOUtils
.createPrivateDirectories(avatarsPath
);
1706 if (attachment
.isPointer()) {
1707 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1708 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1710 SignalServiceAttachmentStream stream
= attachment
.asStream();
1711 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1715 private File
getGroupAvatarFile(byte[] groupId
) {
1716 return new File(avatarsPath
, "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1719 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
{
1720 IOUtils
.createPrivateDirectories(avatarsPath
);
1721 if (attachment
.isPointer()) {
1722 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1723 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1725 SignalServiceAttachmentStream stream
= attachment
.asStream();
1726 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1730 public File
getAttachmentFile(long attachmentId
) {
1731 return new File(attachmentsPath
, attachmentId
+ "");
1734 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
{
1735 IOUtils
.createPrivateDirectories(attachmentsPath
);
1736 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getId()), true);
1739 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
{
1740 if (storePreview
&& pointer
.getPreview().isPresent()) {
1741 File previewFile
= new File(outputFile
+ ".preview");
1742 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1743 byte[] preview
= pointer
.getPreview().get();
1744 output
.write(preview
, 0, preview
.length
);
1745 } catch (FileNotFoundException e
) {
1746 e
.printStackTrace();
1751 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1753 File tmpFile
= IOUtils
.createTempFile();
1754 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
)) {
1755 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1756 byte[] buffer
= new byte[4096];
1759 while ((read
= input
.read(buffer
)) != -1) {
1760 output
.write(buffer
, 0, read
);
1762 } catch (FileNotFoundException e
) {
1763 e
.printStackTrace();
1768 Files
.delete(tmpFile
.toPath());
1769 } catch (IOException e
) {
1770 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1776 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
{
1777 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1778 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
);
1782 public boolean isRemote() {
1786 private void sendGroups() throws IOException
, UntrustedIdentityException
{
1787 File groupsFile
= IOUtils
.createTempFile();
1790 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1791 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1792 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1793 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1794 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1795 record.isMember(account
.getSelfAddress()), Optional
.of(record.messageExpirationTime
),
1796 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1800 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1801 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1802 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1803 .withStream(groupsFileStream
)
1804 .withContentType("application/octet-stream")
1805 .withLength(groupsFile
.length())
1808 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1813 Files
.delete(groupsFile
.toPath());
1814 } catch (IOException e
) {
1815 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1820 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1821 File contactsFile
= IOUtils
.createTempFile();
1824 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1825 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1826 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1827 VerifiedMessage verifiedMessage
= null;
1828 if (getIdentities().containsKey(record.number
)) {
1829 JsonIdentityKeyStore
.Identity currentIdentity
= null;
1830 for (JsonIdentityKeyStore
.Identity id
: getIdentities().get(record.number
)) {
1831 if (currentIdentity
== null || id
.getDateAdded().after(currentIdentity
.getDateAdded())) {
1832 currentIdentity
= id
;
1835 if (currentIdentity
!= null) {
1836 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1840 ProfileKey profileKey
= null;
1842 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1843 } catch (InvalidInputException ignored
) {
1845 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1846 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1847 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1848 Optional
.of(record.messageExpirationTime
),
1849 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1852 if (account
.getProfileKey() != null) {
1853 // Send our own profile key as well
1854 out
.write(new DeviceContact(account
.getSelfAddress(),
1855 Optional
.absent(), Optional
.absent(),
1856 Optional
.absent(), Optional
.absent(),
1857 Optional
.of(account
.getProfileKey()),
1858 false, Optional
.absent(), Optional
.absent(), false));
1862 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1863 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1864 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1865 .withStream(contactsFileStream
)
1866 .withContentType("application/octet-stream")
1867 .withLength(contactsFile
.length())
1870 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1875 Files
.delete(contactsFile
.toPath());
1876 } catch (IOException e
) {
1877 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1882 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1883 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1884 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1885 if (record.blocked
) {
1886 addresses
.add(record.getAddress());
1889 List
<byte[]> groupIds
= new ArrayList
<>();
1890 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1891 if (record.blocked
) {
1892 groupIds
.add(record.groupId
);
1895 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1898 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1899 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1900 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1903 public List
<ContactInfo
> getContacts() {
1904 return account
.getContactStore().getContacts();
1907 public ContactInfo
getContact(String number
) {
1908 return account
.getContactStore().getContact(new SignalServiceAddress(null, number
));
1911 public GroupInfo
getGroup(byte[] groupId
) {
1912 return account
.getGroupStore().getGroup(groupId
);
1915 public Map
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities() {
1916 return account
.getSignalProtocolStore().getIdentities();
1919 public Pair
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities(String number
) throws InvalidNumberException
{
1920 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
1921 return new Pair
<>(canonicalizedNumber
, account
.getSignalProtocolStore().getIdentities(canonicalizedNumber
));
1925 * Trust this the identity with this fingerprint
1927 * @param name username of the identity
1928 * @param fingerprint Fingerprint
1930 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) {
1931 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1935 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1936 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1940 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1942 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1943 } catch (IOException
| UntrustedIdentityException e
) {
1944 e
.printStackTrace();
1953 * Trust this the identity with this safety number
1955 * @param name username of the identity
1956 * @param safetyNumber Safety number
1958 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) {
1959 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1963 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1964 if (!safetyNumber
.equals(computeSafetyNumber(name
, id
.getIdentityKey()))) {
1968 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1970 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1971 } catch (IOException
| UntrustedIdentityException e
) {
1972 e
.printStackTrace();
1981 * Trust all keys of this identity without verification
1983 * @param name username of the identity
1985 public boolean trustIdentityAllKeys(String name
) {
1986 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1990 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1991 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
1992 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1994 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1995 } catch (IOException
| UntrustedIdentityException e
) {
1996 e
.printStackTrace();
2004 public String
computeSafetyNumber(String theirUsername
, IdentityKey theirIdentityKey
) {
2005 return Utils
.computeSafetyNumber(account
.getUsername(), getIdentity(), theirUsername
, theirIdentityKey
);
2008 public interface ReceiveMessageHandler
{
2010 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);