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
.AuthorizationFailedException
;
103 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.EncapsulatedExceptions
;
104 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NetworkFailureException
;
105 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.UnregisteredUserException
;
106 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
107 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
108 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
109 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
110 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
111 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
112 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
113 import org
.whispersystems
.util
.Base64
;
116 import java
.io
.FileInputStream
;
117 import java
.io
.FileNotFoundException
;
118 import java
.io
.FileOutputStream
;
119 import java
.io
.IOException
;
120 import java
.io
.InputStream
;
121 import java
.io
.OutputStream
;
123 import java
.net
.URISyntaxException
;
124 import java
.net
.URLEncoder
;
125 import java
.nio
.file
.Files
;
126 import java
.nio
.file
.Paths
;
127 import java
.nio
.file
.StandardCopyOption
;
128 import java
.util
.ArrayList
;
129 import java
.util
.Arrays
;
130 import java
.util
.Collection
;
131 import java
.util
.Collections
;
132 import java
.util
.Date
;
133 import java
.util
.HashSet
;
134 import java
.util
.LinkedList
;
135 import java
.util
.List
;
136 import java
.util
.Locale
;
137 import java
.util
.Map
;
138 import java
.util
.Objects
;
139 import java
.util
.Set
;
140 import java
.util
.concurrent
.TimeUnit
;
141 import java
.util
.concurrent
.TimeoutException
;
142 import java
.util
.zip
.ZipEntry
;
143 import java
.util
.zip
.ZipFile
;
145 public class Manager
implements Signal
{
147 private static final SignalServiceProfile
.Capabilities capabilities
= new SignalServiceProfile
.Capabilities(false, false);
149 private final String settingsPath
;
150 private final String dataPath
;
151 private final String attachmentsPath
;
152 private final String avatarsPath
;
153 private final SleepTimer timer
= new UptimeSleepTimer();
155 private SignalAccount account
;
156 private String username
;
157 private SignalServiceAccountManager accountManager
;
158 private SignalServiceMessagePipe messagePipe
= null;
159 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
161 public Manager(String username
, String settingsPath
) {
162 this.username
= username
;
163 this.settingsPath
= settingsPath
;
164 this.dataPath
= this.settingsPath
+ "/data";
165 this.attachmentsPath
= this.settingsPath
+ "/attachments";
166 this.avatarsPath
= this.settingsPath
+ "/avatars";
170 public String
getUsername() {
174 public SignalServiceAddress
getSelfAddress() {
175 return account
.getSelfAddress();
178 private SignalServiceAccountManager
getSignalServiceAccountManager() {
179 return new SignalServiceAccountManager(BaseConfig
.serviceConfiguration
, null, account
.getUsername(), account
.getPassword(), account
.getDeviceId(), BaseConfig
.USER_AGENT
, timer
);
182 private IdentityKey
getIdentity() {
183 return account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
186 public int getDeviceId() {
187 return account
.getDeviceId();
190 private String
getMessageCachePath() {
191 return this.dataPath
+ "/" + username
+ ".d/msg-cache";
194 private String
getMessageCachePath(String sender
) {
195 return getMessageCachePath() + "/" + sender
.replace("/", "_");
198 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
199 String cachePath
= getMessageCachePath(sender
);
200 IOUtils
.createPrivateDirectories(cachePath
);
201 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
204 public boolean userHasKeys() {
205 return account
!= null && account
.getSignalProtocolStore() != null;
208 public void init() throws IOException
{
209 if (!SignalAccount
.userExists(dataPath
, username
)) {
212 account
= SignalAccount
.load(dataPath
, username
);
214 migrateLegacyConfigs();
216 accountManager
= getSignalServiceAccountManager();
218 if (account
.isRegistered() && accountManager
.getPreKeysCount() < BaseConfig
.PREKEY_MINIMUM_COUNT
) {
222 } catch (AuthorizationFailedException e
) {
223 System
.err
.println("Authorization failed, was the number registered elsewhere?");
228 private void migrateLegacyConfigs() {
229 // Copy group avatars that were previously stored in the attachments folder
230 // to the new avatar folder
231 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
232 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
233 File avatarFile
= getGroupAvatarFile(g
.groupId
);
234 File attachmentFile
= getAttachmentFile(g
.getAvatarId());
235 if (!avatarFile
.exists() && attachmentFile
.exists()) {
237 IOUtils
.createPrivateDirectories(avatarsPath
);
238 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
239 } catch (Exception e
) {
244 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
247 if (account
.getProfileKey() == null) {
248 // Old config file, creating new profile key
249 account
.setProfileKey(KeyUtils
.createProfileKey());
254 private void createNewIdentity() throws IOException
{
255 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
256 int registrationId
= KeyHelper
.generateRegistrationId(false);
257 if (username
== null) {
258 account
= SignalAccount
.createTemporaryAccount(identityKey
, registrationId
);
260 ProfileKey profileKey
= KeyUtils
.createProfileKey();
261 account
= SignalAccount
.create(dataPath
, username
, identityKey
, registrationId
, profileKey
);
266 public boolean isRegistered() {
267 return account
!= null && account
.isRegistered();
270 public void register(boolean voiceVerification
) throws IOException
{
271 if (account
== null) {
274 account
.setPassword(KeyUtils
.createPassword());
275 accountManager
= getSignalServiceAccountManager();
277 if (voiceVerification
) {
278 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
280 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
283 account
.setRegistered(false);
287 public void updateAccountAttributes() throws IOException
{
288 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities
);
291 public void setProfileName(String name
) throws IOException
{
292 accountManager
.setProfileName(account
.getProfileKey(), name
);
295 public void setProfileAvatar(File avatar
) throws IOException
{
296 final StreamDetails streamDetails
= Utils
.createStreamDetailsFromFile(avatar
);
297 accountManager
.setProfileAvatar(account
.getProfileKey(), streamDetails
);
298 streamDetails
.getStream().close();
301 public void removeProfileAvatar() throws IOException
{
302 accountManager
.setProfileAvatar(account
.getProfileKey(), null);
305 public void unregister() throws IOException
{
306 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
307 // If this is the master device, other users can't send messages to this number anymore.
308 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
309 accountManager
.setGcmId(Optional
.absent());
311 account
.setRegistered(false);
315 public String
getDeviceLinkUri() throws TimeoutException
, IOException
{
316 if (account
== null) {
319 account
.setPassword(KeyUtils
.createPassword());
320 accountManager
= getSignalServiceAccountManager();
321 String uuid
= accountManager
.getNewDeviceUuid();
323 return Utils
.createDeviceLinkUri(new Utils
.DeviceLinkInfo(uuid
, getIdentity().getPublicKey()));
326 public void finishDeviceLink(String deviceName
) throws IOException
, InvalidKeyException
, TimeoutException
, UserAlreadyExists
{
327 account
.setSignalingKey(KeyUtils
.createSignalingKey());
328 SignalServiceAccountManager
.NewDeviceRegistrationReturn ret
= accountManager
.finishNewDeviceRegistration(account
.getSignalProtocolStore().getIdentityKeyPair(), account
.getSignalingKey(), false, true, account
.getSignalProtocolStore().getLocalRegistrationId(), deviceName
);
330 username
= ret
.getNumber();
331 // TODO do this check before actually registering
332 if (SignalAccount
.userExists(dataPath
, username
)) {
333 throw new UserAlreadyExists(username
, SignalAccount
.getFileName(dataPath
, username
));
336 // Create new account with the synced identity
337 byte[] profileKeyBytes
= ret
.getProfileKey();
338 ProfileKey profileKey
;
339 if (profileKeyBytes
== null) {
340 profileKey
= KeyUtils
.createProfileKey();
343 profileKey
= new ProfileKey(profileKeyBytes
);
344 } catch (InvalidInputException e
) {
345 throw new IOException("Received invalid profileKey", e
);
348 account
= SignalAccount
.createLinkedAccount(dataPath
, username
, account
.getPassword(), ret
.getDeviceId(), ret
.getIdentity(), account
.getSignalProtocolStore().getLocalRegistrationId(), account
.getSignalingKey(), profileKey
);
353 requestSyncContacts();
354 requestSyncBlocked();
355 requestSyncConfiguration();
360 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
361 List
<DeviceInfo
> devices
= accountManager
.getDevices();
362 account
.setMultiDevice(devices
.size() > 1);
367 public void removeLinkedDevices(int deviceId
) throws IOException
{
368 accountManager
.removeDevice(deviceId
);
369 List
<DeviceInfo
> devices
= accountManager
.getDevices();
370 account
.setMultiDevice(devices
.size() > 1);
374 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
375 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
377 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
380 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
381 IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
382 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
384 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
385 account
.setMultiDevice(true);
389 private List
<PreKeyRecord
> generatePreKeys() {
390 List
<PreKeyRecord
> records
= new ArrayList
<>(BaseConfig
.PREKEY_BATCH_SIZE
);
392 final int offset
= account
.getPreKeyIdOffset();
393 for (int i
= 0; i
< BaseConfig
.PREKEY_BATCH_SIZE
; i
++) {
394 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
395 ECKeyPair keyPair
= Curve
.generateKeyPair();
396 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
401 account
.addPreKeys(records
);
407 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
409 ECKeyPair keyPair
= Curve
.generateKeyPair();
410 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
411 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
413 account
.addSignedPreKey(record);
417 } catch (InvalidKeyException e
) {
418 throw new AssertionError(e
);
422 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
423 verificationCode
= verificationCode
.replace("-", "");
424 account
.setSignalingKey(KeyUtils
.createSignalingKey());
425 // TODO make unrestricted unidentified access configurable
426 accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, capabilities
);
428 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
429 account
.setRegistered(true);
430 account
.setRegistrationLockPin(pin
);
436 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
437 if (pin
.isPresent()) {
438 account
.setRegistrationLockPin(pin
.get());
439 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
441 account
.setRegistrationLockPin(null);
442 accountManager
.removeV1Pin();
447 private void refreshPreKeys() throws IOException
{
448 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
449 final IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
450 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
452 accountManager
.setPreKeys(getIdentity(), signedPreKeyRecord
, oneTimePreKeys
);
455 private SignalServiceMessageReceiver
getMessageReceiver() {
456 return new SignalServiceMessageReceiver(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), BaseConfig
.USER_AGENT
, null, timer
);
459 private SignalServiceMessageSender
getMessageSender() {
460 return new SignalServiceMessageSender(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
461 account
.getDeviceId(), account
.getSignalProtocolStore(), BaseConfig
.USER_AGENT
, account
.isMultiDevice(), Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent());
464 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
465 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
470 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
471 } catch (IOException ignored
) {
475 SignalServiceMessageReceiver receiver
= getMessageReceiver();
477 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
478 } catch (VerificationFailedException e
) {
479 throw new AssertionError(e
);
483 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
484 File file
= getGroupAvatarFile(groupId
);
485 if (!file
.exists()) {
486 return Optional
.absent();
489 return Optional
.of(Utils
.createAttachment(file
));
492 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
493 File file
= getContactAvatarFile(number
);
494 if (!file
.exists()) {
495 return Optional
.absent();
498 return Optional
.of(Utils
.createAttachment(file
));
501 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
502 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
504 throw new GroupNotFoundException(groupId
);
506 if (!g
.isMember(account
.getSelfAddress())) {
507 throw new NotAGroupMemberException(groupId
, g
.name
);
512 public List
<GroupInfo
> getGroups() {
513 return account
.getGroupStore().getGroups();
517 public void sendGroupMessage(String messageText
, List
<String
> attachments
,
519 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
520 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
521 if (attachments
!= null) {
522 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
524 if (groupId
!= null) {
525 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
528 messageBuilder
.asGroupMessage(group
);
531 final GroupInfo g
= getGroupForSending(groupId
);
533 messageBuilder
.withExpiration(g
.messageExpirationTime
);
535 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
538 public void sendGroupMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
539 long targetSentTimestamp
, byte[] groupId
)
540 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
{
541 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
542 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
543 .withReaction(reaction
)
544 .withProfileKey(account
.getProfileKey().serialize());
545 if (groupId
!= null) {
546 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
549 messageBuilder
.asGroupMessage(group
);
551 final GroupInfo g
= getGroupForSending(groupId
);
552 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
555 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
{
556 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
560 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
561 .asGroupMessage(group
);
563 final GroupInfo g
= getGroupForSending(groupId
);
564 g
.removeMember(account
.getSelfAddress());
565 account
.getGroupStore().updateGroup(g
);
567 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
570 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
572 if (groupId
== null) {
574 g
= new GroupInfo(KeyUtils
.createGroupId());
575 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
577 g
= getGroupForSending(groupId
);
584 if (members
!= null) {
585 final Set
<String
> newE164Members
= new HashSet
<>();
586 for (SignalServiceAddress member
: members
) {
587 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
590 newE164Members
.add(member
.getNumber().get());
593 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
594 if (contacts
.size() != newE164Members
.size()) {
595 // Some of the new members are not registered on Signal
596 for (ContactTokenDetails contact
: contacts
) {
597 newE164Members
.remove(contact
.getNumber());
599 System
.err
.println("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
600 System
.err
.println("Aborting…");
604 g
.addMembers(members
);
607 if (avatarFile
!= null) {
608 IOUtils
.createPrivateDirectories(avatarsPath
);
609 File aFile
= getGroupAvatarFile(g
.groupId
);
610 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
613 account
.getGroupStore().updateGroup(g
);
615 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
617 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
621 private void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
622 if (groupId
== null) {
625 GroupInfo g
= getGroupForSending(groupId
);
627 if (!g
.isMember(recipient
)) {
631 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
633 // Send group message only to the recipient who requested it
634 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
637 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
638 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
641 .withMembers(new ArrayList
<>(g
.getMembers()));
643 File aFile
= getGroupAvatarFile(g
.groupId
);
644 if (aFile
.exists()) {
646 group
.withAvatar(Utils
.createAttachment(aFile
));
647 } catch (IOException e
) {
648 throw new AttachmentInvalidException(aFile
.toString(), e
);
652 return SignalServiceDataMessage
.newBuilder()
653 .asGroupMessage(group
.build())
654 .withExpiration(g
.messageExpirationTime
);
657 private void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
658 if (groupId
== null) {
662 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
665 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
666 .asGroupMessage(group
.build());
668 // Send group info request message to the recipient who sent us a message with this groupId
669 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
672 private void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
673 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
674 Collections
.singletonList(messageId
),
675 System
.currentTimeMillis());
677 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
681 public void sendMessage(String message
, List
<String
> attachments
, String recipient
)
682 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
, InvalidNumberException
{
683 List
<String
> recipients
= new ArrayList
<>(1);
684 recipients
.add(recipient
);
685 sendMessage(message
, attachments
, recipients
);
689 public void sendMessage(String messageText
, List
<String
> attachments
,
690 List
<String
> recipients
)
691 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
692 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
693 if (attachments
!= null) {
694 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
696 // Upload attachments here, so we only upload once even for multiple recipients
697 SignalServiceMessageSender messageSender
= getMessageSender();
698 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
699 for (SignalServiceAttachment attachment
: attachmentStreams
) {
700 if (attachment
.isStream()) {
701 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
702 } else if (attachment
.isPointer()) {
703 attachmentPointers
.add(attachment
.asPointer());
707 messageBuilder
.withAttachments(attachmentPointers
);
709 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
710 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
713 public void sendMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
714 long targetSentTimestamp
, List
<String
> recipients
)
715 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
716 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
717 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
718 .withReaction(reaction
)
719 .withProfileKey(account
.getProfileKey().serialize());
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 // TODO get corresponding uuid
1137 signalServiceAddresses
.add(new SignalServiceAddress(null, canonicalizedNumber
));
1140 return signalServiceAddresses
;
1143 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1144 throws IOException
{
1145 if (messagePipe
== null) {
1146 messagePipe
= getMessageReceiver().createMessagePipe();
1148 if (unidentifiedMessagePipe
== null) {
1149 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1151 SignalServiceDataMessage message
= null;
1153 SignalServiceMessageSender messageSender
= getMessageSender();
1155 message
= messageBuilder
.build();
1156 if (message
.getGroupInfo().isPresent()) {
1158 final boolean isRecipientUpdate
= false;
1159 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1160 for (SendMessageResult r
: result
) {
1161 if (r
.getIdentityFailure() != null) {
1162 account
.getSignalProtocolStore().saveIdentity(r
.getAddress().getNumber().get(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1166 } catch (UntrustedIdentityException e
) {
1167 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1168 return Collections
.emptyList();
1170 } else if (recipients
.size() == 1 && recipients
.contains(account
.getSelfAddress())) {
1171 SignalServiceAddress recipient
= account
.getSelfAddress();
1172 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1173 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1174 message
.getTimestamp(),
1176 message
.getExpiresInSeconds(),
1177 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1179 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1181 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1183 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1184 } catch (UntrustedIdentityException e
) {
1185 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1186 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1190 // Send to all individually, so sync messages are sent correctly
1191 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1192 for (SignalServiceAddress address
: recipients
) {
1193 ContactInfo contact
= account
.getContactStore().getContact(address
);
1194 if (contact
!= null) {
1195 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1197 messageBuilder
.withExpiration(0);
1199 message
= messageBuilder
.build();
1201 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1202 results
.add(result
);
1203 } catch (UntrustedIdentityException e
) {
1204 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1205 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1211 if (message
!= null && message
.isEndSession()) {
1212 for (SignalServiceAddress recipient
: recipients
) {
1213 handleEndSession(recipient
.getNumber().get());
1220 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, ProtocolUntrustedIdentityException
, SelfSendException
, UnsupportedDataMessageException
{
1221 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1223 return cipher
.decrypt(envelope
);
1224 } catch (ProtocolUntrustedIdentityException e
) {
1225 // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else
1226 // account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
1231 private void handleEndSession(String source
) {
1232 account
.getSignalProtocolStore().deleteAllSessions(source
);
1235 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1236 if (message
.getGroupInfo().isPresent()) {
1237 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1238 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1239 switch (groupInfo
.getType()) {
1241 if (group
== null) {
1242 group
= new GroupInfo(groupInfo
.getGroupId());
1245 if (groupInfo
.getAvatar().isPresent()) {
1246 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1247 if (avatar
.isPointer()) {
1249 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1250 } catch (IOException
| InvalidMessageException e
) {
1251 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getId() + "): " + e
.getMessage());
1256 if (groupInfo
.getName().isPresent()) {
1257 group
.name
= groupInfo
.getName().get();
1260 if (groupInfo
.getMembers().isPresent()) {
1261 group
.addMembers(groupInfo
.getMembers().get());
1264 account
.getGroupStore().updateGroup(group
);
1267 if (group
== null) {
1269 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1270 } catch (IOException
| EncapsulatedExceptions e
) {
1271 e
.printStackTrace();
1276 if (group
== null) {
1278 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1279 } catch (IOException
| EncapsulatedExceptions e
) {
1280 e
.printStackTrace();
1283 group
.removeMember(source
);
1284 account
.getGroupStore().updateGroup(group
);
1288 if (group
!= null) {
1290 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1291 } catch (IOException
| EncapsulatedExceptions e
) {
1292 e
.printStackTrace();
1293 } catch (NotAGroupMemberException e
) {
1294 // We have left this group, so don't send a group update message
1300 if (message
.isEndSession()) {
1301 handleEndSession(isSync ? destination
.getNumber().get() : source
.getNumber().get());
1303 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1304 if (message
.getGroupInfo().isPresent()) {
1305 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1306 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1307 if (group
== null) {
1308 group
= new GroupInfo(groupInfo
.getGroupId());
1310 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1311 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1312 account
.getGroupStore().updateGroup(group
);
1315 ContactInfo contact
= account
.getContactStore().getContact(isSync ? destination
: source
);
1316 if (contact
== null) {
1317 contact
= new ContactInfo(isSync ? destination
: source
);
1319 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1320 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1321 account
.getContactStore().updateContact(contact
);
1325 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1326 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1327 if (attachment
.isPointer()) {
1329 retrieveAttachment(attachment
.asPointer());
1330 } catch (IOException
| InvalidMessageException e
) {
1331 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getId() + "): " + e
.getMessage());
1336 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1337 if (source
.equals(account
.getSelfAddress())) {
1339 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1340 } catch (InvalidInputException ignored
) {
1343 ContactInfo contact
= account
.getContactStore().getContact(source
);
1344 if (contact
== null) {
1345 contact
= new ContactInfo(source
);
1347 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1348 account
.getContactStore().updateContact(contact
);
1350 if (message
.getPreviews().isPresent()) {
1351 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1352 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1353 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1354 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1356 retrieveAttachment(attachment
);
1357 } catch (IOException
| InvalidMessageException e
) {
1358 System
.err
.println("Failed to retrieve attachment (" + attachment
.getId() + "): " + e
.getMessage());
1365 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1366 final File cachePath
= new File(getMessageCachePath());
1367 if (!cachePath
.exists()) {
1370 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1371 if (!dir
.isDirectory()) {
1375 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1376 if (!fileEntry
.isFile()) {
1379 SignalServiceEnvelope envelope
;
1381 envelope
= Utils
.loadEnvelope(fileEntry
);
1382 if (envelope
== null) {
1385 } catch (IOException e
) {
1386 e
.printStackTrace();
1389 SignalServiceContent content
= null;
1390 if (!envelope
.isReceipt()) {
1392 content
= decryptMessage(envelope
);
1393 } catch (Exception e
) {
1396 handleMessage(envelope
, content
, ignoreAttachments
);
1399 handler
.handleMessage(envelope
, content
, null);
1401 Files
.delete(fileEntry
.toPath());
1402 } catch (IOException e
) {
1403 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1406 // Try to delete directory if empty
1411 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1412 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1413 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1416 if (messagePipe
== null) {
1417 messagePipe
= messageReceiver
.createMessagePipe();
1421 SignalServiceEnvelope envelope
;
1422 SignalServiceContent content
= null;
1423 Exception exception
= null;
1424 final long now
= new Date().getTime();
1426 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1427 // store message on disk, before acknowledging receipt to the server
1429 File cacheFile
= getMessageCacheFile(envelope1
.getSourceE164().get(), now
, envelope1
.getTimestamp());
1430 Utils
.storeEnvelope(envelope1
, cacheFile
);
1431 } catch (IOException e
) {
1432 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1435 } catch (TimeoutException e
) {
1436 if (returnOnTimeout
)
1439 } catch (InvalidVersionException e
) {
1440 System
.err
.println("Ignoring error: " + e
.getMessage());
1443 if (!envelope
.isReceipt()) {
1445 content
= decryptMessage(envelope
);
1446 } catch (Exception e
) {
1449 handleMessage(envelope
, content
, ignoreAttachments
);
1452 if (!isMessageBlocked(envelope
, content
)) {
1453 handler
.handleMessage(envelope
, content
, exception
);
1455 if (!(exception
instanceof ProtocolUntrustedIdentityException
)) {
1456 File cacheFile
= null;
1458 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1459 Files
.delete(cacheFile
.toPath());
1460 // Try to delete directory if empty
1461 new File(getMessageCachePath()).delete();
1462 } catch (IOException e
) {
1463 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1468 if (messagePipe
!= null) {
1469 messagePipe
.shutdown();
1475 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1476 SignalServiceAddress source
;
1477 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1478 source
= envelope
.getSourceAddress();
1479 } else if (content
!= null) {
1480 source
= content
.getSender();
1484 ContactInfo sourceContact
= getContact(source
.getNumber().get());
1485 if (sourceContact
!= null && sourceContact
.blocked
) {
1489 if (content
!= null && content
.getDataMessage().isPresent()) {
1490 SignalServiceDataMessage message
= content
.getDataMessage().get();
1491 if (message
.getGroupInfo().isPresent()) {
1492 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1493 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1494 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1502 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1503 if (content
!= null) {
1504 SignalServiceAddress sender
;
1505 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1506 sender
= envelope
.getSourceAddress();
1508 sender
= content
.getSender();
1510 if (content
.getDataMessage().isPresent()) {
1511 SignalServiceDataMessage message
= content
.getDataMessage().get();
1513 if (content
.isNeedsReceipt()) {
1515 sendReceipt(sender
, message
.getTimestamp());
1516 } catch (IOException
| UntrustedIdentityException e
) {
1517 e
.printStackTrace();
1521 handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
);
1523 if (content
.getSyncMessage().isPresent()) {
1524 account
.setMultiDevice(true);
1525 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1526 if (syncMessage
.getSent().isPresent()) {
1527 SentTranscriptMessage message
= syncMessage
.getSent().get();
1528 handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
);
1530 if (syncMessage
.getRequest().isPresent()) {
1531 RequestMessage rm
= syncMessage
.getRequest().get();
1532 if (rm
.isContactsRequest()) {
1535 } catch (UntrustedIdentityException
| IOException e
) {
1536 e
.printStackTrace();
1539 if (rm
.isGroupsRequest()) {
1542 } catch (UntrustedIdentityException
| IOException e
) {
1543 e
.printStackTrace();
1546 if (rm
.isBlockedListRequest()) {
1549 } catch (UntrustedIdentityException
| IOException e
) {
1550 e
.printStackTrace();
1553 // TODO Handle rm.isConfigurationRequest();
1555 if (syncMessage
.getGroups().isPresent()) {
1556 File tmpFile
= null;
1558 tmpFile
= IOUtils
.createTempFile();
1559 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1560 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1562 while ((g
= s
.read()) != null) {
1563 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1564 if (syncGroup
== null) {
1565 syncGroup
= new GroupInfo(g
.getId());
1567 if (g
.getName().isPresent()) {
1568 syncGroup
.name
= g
.getName().get();
1570 syncGroup
.addMembers(g
.getMembers());
1571 if (!g
.isActive()) {
1572 syncGroup
.removeMember(account
.getSelfAddress());
1574 // Add ourself to the member set as it's marked as active
1575 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1577 syncGroup
.blocked
= g
.isBlocked();
1578 if (g
.getColor().isPresent()) {
1579 syncGroup
.color
= g
.getColor().get();
1582 if (g
.getAvatar().isPresent()) {
1583 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1585 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1586 syncGroup
.archived
= g
.isArchived();
1587 account
.getGroupStore().updateGroup(syncGroup
);
1590 } catch (Exception e
) {
1591 e
.printStackTrace();
1593 if (tmpFile
!= null) {
1595 Files
.delete(tmpFile
.toPath());
1596 } catch (IOException e
) {
1597 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1602 if (syncMessage
.getBlockedList().isPresent()) {
1603 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1604 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1605 if (address
.getNumber().isPresent()) {
1607 setContactBlocked(address
.getNumber().get(), true);
1608 } catch (InvalidNumberException e
) {
1609 e
.printStackTrace();
1613 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1615 setGroupBlocked(groupId
, true);
1616 } catch (GroupNotFoundException e
) {
1617 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1621 if (syncMessage
.getContacts().isPresent()) {
1622 File tmpFile
= null;
1624 tmpFile
= IOUtils
.createTempFile();
1625 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1626 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1627 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1628 if (contactsMessage
.isComplete()) {
1629 account
.getContactStore().clear();
1632 while ((c
= s
.read()) != null) {
1633 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1634 account
.setProfileKey(c
.getProfileKey().get());
1636 ContactInfo contact
= account
.getContactStore().getContact(c
.getAddress());
1637 if (contact
== null) {
1638 contact
= new ContactInfo(c
.getAddress());
1640 if (c
.getName().isPresent()) {
1641 contact
.name
= c
.getName().get();
1643 if (c
.getColor().isPresent()) {
1644 contact
.color
= c
.getColor().get();
1646 if (c
.getProfileKey().isPresent()) {
1647 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1649 if (c
.getVerified().isPresent()) {
1650 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1651 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1653 if (c
.getExpirationTimer().isPresent()) {
1654 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1656 contact
.blocked
= c
.isBlocked();
1657 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1658 contact
.archived
= c
.isArchived();
1659 account
.getContactStore().updateContact(contact
);
1661 if (c
.getAvatar().isPresent()) {
1662 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1666 } catch (Exception e
) {
1667 e
.printStackTrace();
1669 if (tmpFile
!= null) {
1671 Files
.delete(tmpFile
.toPath());
1672 } catch (IOException e
) {
1673 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1678 if (syncMessage
.getVerified().isPresent()) {
1679 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1680 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1682 if (syncMessage
.getConfiguration().isPresent()) {
1689 private File
getContactAvatarFile(String number
) {
1690 return new File(avatarsPath
, "contact-" + number
);
1693 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
{
1694 IOUtils
.createPrivateDirectories(avatarsPath
);
1695 if (attachment
.isPointer()) {
1696 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1697 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1699 SignalServiceAttachmentStream stream
= attachment
.asStream();
1700 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1704 private File
getGroupAvatarFile(byte[] groupId
) {
1705 return new File(avatarsPath
, "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1708 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
{
1709 IOUtils
.createPrivateDirectories(avatarsPath
);
1710 if (attachment
.isPointer()) {
1711 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1712 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1714 SignalServiceAttachmentStream stream
= attachment
.asStream();
1715 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1719 public File
getAttachmentFile(long attachmentId
) {
1720 return new File(attachmentsPath
, attachmentId
+ "");
1723 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
{
1724 IOUtils
.createPrivateDirectories(attachmentsPath
);
1725 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getId()), true);
1728 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
{
1729 if (storePreview
&& pointer
.getPreview().isPresent()) {
1730 File previewFile
= new File(outputFile
+ ".preview");
1731 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1732 byte[] preview
= pointer
.getPreview().get();
1733 output
.write(preview
, 0, preview
.length
);
1734 } catch (FileNotFoundException e
) {
1735 e
.printStackTrace();
1740 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1742 File tmpFile
= IOUtils
.createTempFile();
1743 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
)) {
1744 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1745 byte[] buffer
= new byte[4096];
1748 while ((read
= input
.read(buffer
)) != -1) {
1749 output
.write(buffer
, 0, read
);
1751 } catch (FileNotFoundException e
) {
1752 e
.printStackTrace();
1757 Files
.delete(tmpFile
.toPath());
1758 } catch (IOException e
) {
1759 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1765 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
{
1766 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1767 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
);
1771 public boolean isRemote() {
1775 private void sendGroups() throws IOException
, UntrustedIdentityException
{
1776 File groupsFile
= IOUtils
.createTempFile();
1779 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1780 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1781 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1782 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1783 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1784 record.isMember(account
.getSelfAddress()), Optional
.of(record.messageExpirationTime
),
1785 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1789 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1790 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1791 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1792 .withStream(groupsFileStream
)
1793 .withContentType("application/octet-stream")
1794 .withLength(groupsFile
.length())
1797 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1802 Files
.delete(groupsFile
.toPath());
1803 } catch (IOException e
) {
1804 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1809 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1810 File contactsFile
= IOUtils
.createTempFile();
1813 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1814 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1815 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1816 VerifiedMessage verifiedMessage
= null;
1817 if (getIdentities().containsKey(record.number
)) {
1818 JsonIdentityKeyStore
.Identity currentIdentity
= null;
1819 for (JsonIdentityKeyStore
.Identity id
: getIdentities().get(record.number
)) {
1820 if (currentIdentity
== null || id
.getDateAdded().after(currentIdentity
.getDateAdded())) {
1821 currentIdentity
= id
;
1824 if (currentIdentity
!= null) {
1825 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1829 ProfileKey profileKey
= null;
1831 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1832 } catch (InvalidInputException ignored
) {
1834 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1835 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1836 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1837 Optional
.of(record.messageExpirationTime
),
1838 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1841 if (account
.getProfileKey() != null) {
1842 // Send our own profile key as well
1843 out
.write(new DeviceContact(account
.getSelfAddress(),
1844 Optional
.absent(), Optional
.absent(),
1845 Optional
.absent(), Optional
.absent(),
1846 Optional
.of(account
.getProfileKey()),
1847 false, Optional
.absent(), Optional
.absent(), false));
1851 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1852 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1853 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1854 .withStream(contactsFileStream
)
1855 .withContentType("application/octet-stream")
1856 .withLength(contactsFile
.length())
1859 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1864 Files
.delete(contactsFile
.toPath());
1865 } catch (IOException e
) {
1866 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1871 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1872 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1873 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1874 if (record.blocked
) {
1875 addresses
.add(record.getAddress());
1878 List
<byte[]> groupIds
= new ArrayList
<>();
1879 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1880 if (record.blocked
) {
1881 groupIds
.add(record.groupId
);
1884 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1887 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1888 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1889 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1892 public List
<ContactInfo
> getContacts() {
1893 return account
.getContactStore().getContacts();
1896 public ContactInfo
getContact(String number
) {
1897 return account
.getContactStore().getContact(new SignalServiceAddress(null, number
));
1900 public GroupInfo
getGroup(byte[] groupId
) {
1901 return account
.getGroupStore().getGroup(groupId
);
1904 public Map
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities() {
1905 return account
.getSignalProtocolStore().getIdentities();
1908 public Pair
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities(String number
) throws InvalidNumberException
{
1909 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
1910 return new Pair
<>(canonicalizedNumber
, account
.getSignalProtocolStore().getIdentities(canonicalizedNumber
));
1914 * Trust this the identity with this fingerprint
1916 * @param name username of the identity
1917 * @param fingerprint Fingerprint
1919 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) {
1920 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1924 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1925 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1929 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1931 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1932 } catch (IOException
| UntrustedIdentityException e
) {
1933 e
.printStackTrace();
1942 * Trust this the identity with this safety number
1944 * @param name username of the identity
1945 * @param safetyNumber Safety number
1947 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) {
1948 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1952 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1953 if (!safetyNumber
.equals(computeSafetyNumber(name
, id
.getIdentityKey()))) {
1957 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1959 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1960 } catch (IOException
| UntrustedIdentityException e
) {
1961 e
.printStackTrace();
1970 * Trust all keys of this identity without verification
1972 * @param name username of the identity
1974 public boolean trustIdentityAllKeys(String name
) {
1975 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1979 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1980 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
1981 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1983 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1984 } catch (IOException
| UntrustedIdentityException e
) {
1985 e
.printStackTrace();
1993 public String
computeSafetyNumber(String theirUsername
, IdentityKey theirIdentityKey
) {
1994 return Utils
.computeSafetyNumber(account
.getUsername(), getIdentity(), theirUsername
, theirIdentityKey
);
1997 public interface ReceiveMessageHandler
{
1999 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);