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
.UUID
;
141 import java
.util
.concurrent
.TimeUnit
;
142 import java
.util
.concurrent
.TimeoutException
;
143 import java
.util
.zip
.ZipEntry
;
144 import java
.util
.zip
.ZipFile
;
146 public class Manager
implements Signal
{
148 private static final SignalServiceProfile
.Capabilities capabilities
= new SignalServiceProfile
.Capabilities(false, false);
150 private final String settingsPath
;
151 private final String dataPath
;
152 private final String attachmentsPath
;
153 private final String avatarsPath
;
154 private final SleepTimer timer
= new UptimeSleepTimer();
156 private SignalAccount account
;
157 private String username
;
158 private SignalServiceAccountManager accountManager
;
159 private SignalServiceMessagePipe messagePipe
= null;
160 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
162 public Manager(String username
, String settingsPath
) {
163 this.username
= username
;
164 this.settingsPath
= settingsPath
;
165 this.dataPath
= this.settingsPath
+ "/data";
166 this.attachmentsPath
= this.settingsPath
+ "/attachments";
167 this.avatarsPath
= this.settingsPath
+ "/avatars";
171 public String
getUsername() {
175 public SignalServiceAddress
getSelfAddress() {
176 return account
.getSelfAddress();
179 private SignalServiceAccountManager
getSignalServiceAccountManager() {
180 return new SignalServiceAccountManager(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), BaseConfig
.USER_AGENT
, timer
);
183 private IdentityKey
getIdentity() {
184 return account
.getSignalProtocolStore().getIdentityKeyPair().getPublicKey();
187 public int getDeviceId() {
188 return account
.getDeviceId();
191 private String
getMessageCachePath() {
192 return this.dataPath
+ "/" + username
+ ".d/msg-cache";
195 private String
getMessageCachePath(String sender
) {
196 return getMessageCachePath() + "/" + sender
.replace("/", "_");
199 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
200 String cachePath
= getMessageCachePath(sender
);
201 IOUtils
.createPrivateDirectories(cachePath
);
202 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
205 public boolean userHasKeys() {
206 return account
!= null && account
.getSignalProtocolStore() != null;
209 public void init() throws IOException
{
210 if (!SignalAccount
.userExists(dataPath
, username
)) {
213 account
= SignalAccount
.load(dataPath
, username
);
215 migrateLegacyConfigs();
217 accountManager
= getSignalServiceAccountManager();
219 if (account
.isRegistered()) {
220 if (accountManager
.getPreKeysCount() < BaseConfig
.PREKEY_MINIMUM_COUNT
) {
224 if (account
.getUuid() == null) {
225 account
.setUuid(accountManager
.getOwnUuid());
229 } catch (AuthorizationFailedException e
) {
230 System
.err
.println("Authorization failed, was the number registered elsewhere?");
235 private void migrateLegacyConfigs() {
236 // Copy group avatars that were previously stored in the attachments folder
237 // to the new avatar folder
238 if (JsonGroupStore
.groupsWithLegacyAvatarId
.size() > 0) {
239 for (GroupInfo g
: JsonGroupStore
.groupsWithLegacyAvatarId
) {
240 File avatarFile
= getGroupAvatarFile(g
.groupId
);
241 File attachmentFile
= getAttachmentFile(g
.getAvatarId());
242 if (!avatarFile
.exists() && attachmentFile
.exists()) {
244 IOUtils
.createPrivateDirectories(avatarsPath
);
245 Files
.copy(attachmentFile
.toPath(), avatarFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
246 } catch (Exception e
) {
251 JsonGroupStore
.groupsWithLegacyAvatarId
.clear();
254 if (account
.getProfileKey() == null) {
255 // Old config file, creating new profile key
256 account
.setProfileKey(KeyUtils
.createProfileKey());
261 private void createNewIdentity() throws IOException
{
262 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
263 int registrationId
= KeyHelper
.generateRegistrationId(false);
264 if (username
== null) {
265 account
= SignalAccount
.createTemporaryAccount(identityKey
, registrationId
);
267 ProfileKey profileKey
= KeyUtils
.createProfileKey();
268 account
= SignalAccount
.create(dataPath
, username
, identityKey
, registrationId
, profileKey
);
273 public boolean isRegistered() {
274 return account
!= null && account
.isRegistered();
277 public void register(boolean voiceVerification
) throws IOException
{
278 if (account
== null) {
281 account
.setPassword(KeyUtils
.createPassword());
282 accountManager
= getSignalServiceAccountManager();
284 if (voiceVerification
) {
285 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(), Optional
.absent(), Optional
.absent());
287 accountManager
.requestSmsVerificationCode(false, Optional
.absent(), Optional
.absent());
290 account
.setRegistered(false);
294 public void updateAccountAttributes() throws IOException
{
295 accountManager
.setAccountAttributes(account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, account
.getRegistrationLockPin(), account
.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities
);
298 public void setProfileName(String name
) throws IOException
{
299 accountManager
.setProfileName(account
.getProfileKey(), name
);
302 public void setProfileAvatar(File avatar
) throws IOException
{
303 final StreamDetails streamDetails
= Utils
.createStreamDetailsFromFile(avatar
);
304 accountManager
.setProfileAvatar(account
.getProfileKey(), streamDetails
);
305 streamDetails
.getStream().close();
308 public void removeProfileAvatar() throws IOException
{
309 accountManager
.setProfileAvatar(account
.getProfileKey(), null);
312 public void unregister() throws IOException
{
313 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
314 // If this is the master device, other users can't send messages to this number anymore.
315 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
316 accountManager
.setGcmId(Optional
.absent());
318 account
.setRegistered(false);
322 public String
getDeviceLinkUri() throws TimeoutException
, IOException
{
323 if (account
== null) {
326 account
.setPassword(KeyUtils
.createPassword());
327 accountManager
= getSignalServiceAccountManager();
328 String uuid
= accountManager
.getNewDeviceUuid();
330 return Utils
.createDeviceLinkUri(new Utils
.DeviceLinkInfo(uuid
, getIdentity().getPublicKey()));
333 public void finishDeviceLink(String deviceName
) throws IOException
, InvalidKeyException
, TimeoutException
, UserAlreadyExists
{
334 account
.setSignalingKey(KeyUtils
.createSignalingKey());
335 SignalServiceAccountManager
.NewDeviceRegistrationReturn ret
= accountManager
.finishNewDeviceRegistration(account
.getSignalProtocolStore().getIdentityKeyPair(), account
.getSignalingKey(), false, true, account
.getSignalProtocolStore().getLocalRegistrationId(), deviceName
);
337 username
= ret
.getNumber();
338 // TODO do this check before actually registering
339 if (SignalAccount
.userExists(dataPath
, username
)) {
340 throw new UserAlreadyExists(username
, SignalAccount
.getFileName(dataPath
, username
));
343 // Create new account with the synced identity
344 byte[] profileKeyBytes
= ret
.getProfileKey();
345 ProfileKey profileKey
;
346 if (profileKeyBytes
== null) {
347 profileKey
= KeyUtils
.createProfileKey();
350 profileKey
= new ProfileKey(profileKeyBytes
);
351 } catch (InvalidInputException e
) {
352 throw new IOException("Received invalid profileKey", e
);
355 account
= SignalAccount
.createLinkedAccount(dataPath
, username
, ret
.getUuid(), account
.getPassword(), ret
.getDeviceId(), ret
.getIdentity(), account
.getSignalProtocolStore().getLocalRegistrationId(), account
.getSignalingKey(), profileKey
);
360 requestSyncContacts();
361 requestSyncBlocked();
362 requestSyncConfiguration();
367 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
368 List
<DeviceInfo
> devices
= accountManager
.getDevices();
369 account
.setMultiDevice(devices
.size() > 1);
374 public void removeLinkedDevices(int deviceId
) throws IOException
{
375 accountManager
.removeDevice(deviceId
);
376 List
<DeviceInfo
> devices
= accountManager
.getDevices();
377 account
.setMultiDevice(devices
.size() > 1);
381 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
382 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
384 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
387 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
388 IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
389 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
391 accountManager
.addDevice(deviceIdentifier
, deviceKey
, identityKeyPair
, Optional
.of(account
.getProfileKey().serialize()), verificationCode
);
392 account
.setMultiDevice(true);
396 private List
<PreKeyRecord
> generatePreKeys() {
397 List
<PreKeyRecord
> records
= new ArrayList
<>(BaseConfig
.PREKEY_BATCH_SIZE
);
399 final int offset
= account
.getPreKeyIdOffset();
400 for (int i
= 0; i
< BaseConfig
.PREKEY_BATCH_SIZE
; i
++) {
401 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
402 ECKeyPair keyPair
= Curve
.generateKeyPair();
403 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
408 account
.addPreKeys(records
);
414 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
416 ECKeyPair keyPair
= Curve
.generateKeyPair();
417 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(), keyPair
.getPublicKey().serialize());
418 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(), System
.currentTimeMillis(), keyPair
, signature
);
420 account
.addSignedPreKey(record);
424 } catch (InvalidKeyException e
) {
425 throw new AssertionError(e
);
429 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
430 verificationCode
= verificationCode
.replace("-", "");
431 account
.setSignalingKey(KeyUtils
.createSignalingKey());
432 // TODO make unrestricted unidentified access configurable
433 UUID uuid
= accountManager
.verifyAccountWithCode(verificationCode
, account
.getSignalingKey(), account
.getSignalProtocolStore().getLocalRegistrationId(), true, pin
, null, getSelfUnidentifiedAccessKey(), false, capabilities
);
435 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
436 account
.setRegistered(true);
437 account
.setUuid(uuid
);
438 account
.setRegistrationLockPin(pin
);
444 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
445 if (pin
.isPresent()) {
446 account
.setRegistrationLockPin(pin
.get());
447 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
449 account
.setRegistrationLockPin(null);
450 accountManager
.removeV1Pin();
455 private void refreshPreKeys() throws IOException
{
456 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
457 final IdentityKeyPair identityKeyPair
= account
.getSignalProtocolStore().getIdentityKeyPair();
458 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
460 accountManager
.setPreKeys(getIdentity(), signedPreKeyRecord
, oneTimePreKeys
);
463 private SignalServiceMessageReceiver
getMessageReceiver() {
464 return new SignalServiceMessageReceiver(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(), account
.getDeviceId(), account
.getSignalingKey(), BaseConfig
.USER_AGENT
, null, timer
);
467 private SignalServiceMessageSender
getMessageSender() {
468 return new SignalServiceMessageSender(BaseConfig
.serviceConfiguration
, account
.getUuid(), account
.getUsername(), account
.getPassword(),
469 account
.getDeviceId(), account
.getSignalProtocolStore(), BaseConfig
.USER_AGENT
, account
.isMultiDevice(), Optional
.fromNullable(messagePipe
), Optional
.fromNullable(unidentifiedMessagePipe
), Optional
.absent());
472 private SignalServiceProfile
getRecipientProfile(SignalServiceAddress address
, Optional
<UnidentifiedAccess
> unidentifiedAccess
) throws IOException
{
473 SignalServiceMessagePipe pipe
= unidentifiedMessagePipe
!= null && unidentifiedAccess
.isPresent() ? unidentifiedMessagePipe
478 return pipe
.getProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
479 } catch (IOException ignored
) {
483 SignalServiceMessageReceiver receiver
= getMessageReceiver();
485 return receiver
.retrieveProfile(address
, Optional
.absent(), unidentifiedAccess
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
486 } catch (VerificationFailedException e
) {
487 throw new AssertionError(e
);
491 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(byte[] groupId
) throws IOException
{
492 File file
= getGroupAvatarFile(groupId
);
493 if (!file
.exists()) {
494 return Optional
.absent();
497 return Optional
.of(Utils
.createAttachment(file
));
500 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
501 File file
= getContactAvatarFile(number
);
502 if (!file
.exists()) {
503 return Optional
.absent();
506 return Optional
.of(Utils
.createAttachment(file
));
509 private GroupInfo
getGroupForSending(byte[] groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
510 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
512 throw new GroupNotFoundException(groupId
);
514 if (!g
.isMember(account
.getSelfAddress())) {
515 throw new NotAGroupMemberException(groupId
, g
.name
);
520 public List
<GroupInfo
> getGroups() {
521 return account
.getGroupStore().getGroups();
525 public void sendGroupMessage(String messageText
, List
<String
> attachments
,
527 throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
528 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
529 if (attachments
!= null) {
530 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
532 if (groupId
!= null) {
533 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
536 messageBuilder
.asGroupMessage(group
);
539 final GroupInfo g
= getGroupForSending(groupId
);
541 messageBuilder
.withExpiration(g
.messageExpirationTime
);
543 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
546 public void sendGroupMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
547 long targetSentTimestamp
, byte[] groupId
)
548 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
{
549 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
550 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
551 .withReaction(reaction
)
552 .withProfileKey(account
.getProfileKey().serialize());
553 if (groupId
!= null) {
554 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.DELIVER
)
557 messageBuilder
.asGroupMessage(group
);
559 final GroupInfo g
= getGroupForSending(groupId
);
560 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
563 public void sendQuitGroupMessage(byte[] groupId
) throws GroupNotFoundException
, IOException
, EncapsulatedExceptions
{
564 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
568 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
569 .asGroupMessage(group
);
571 final GroupInfo g
= getGroupForSending(groupId
);
572 g
.removeMember(account
.getSelfAddress());
573 account
.getGroupStore().updateGroup(g
);
575 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
578 private byte[] sendUpdateGroupMessage(byte[] groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
{
580 if (groupId
== null) {
582 g
= new GroupInfo(KeyUtils
.createGroupId());
583 g
.addMembers(Collections
.singleton(account
.getSelfAddress()));
585 g
= getGroupForSending(groupId
);
592 if (members
!= null) {
593 final Set
<String
> newE164Members
= new HashSet
<>();
594 for (SignalServiceAddress member
: members
) {
595 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
598 newE164Members
.add(member
.getNumber().get());
601 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
602 if (contacts
.size() != newE164Members
.size()) {
603 // Some of the new members are not registered on Signal
604 for (ContactTokenDetails contact
: contacts
) {
605 newE164Members
.remove(contact
.getNumber());
607 System
.err
.println("Failed to add members " + Util
.join(", ", newE164Members
) + " to group: Not registered on Signal");
608 System
.err
.println("Aborting…");
612 g
.addMembers(members
);
615 if (avatarFile
!= null) {
616 IOUtils
.createPrivateDirectories(avatarsPath
);
617 File aFile
= getGroupAvatarFile(g
.groupId
);
618 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
621 account
.getGroupStore().updateGroup(g
);
623 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
625 sendMessageLegacy(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
629 private void sendUpdateGroupMessage(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
630 if (groupId
== null) {
633 GroupInfo g
= getGroupForSending(groupId
);
635 if (!g
.isMember(recipient
)) {
639 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
641 // Send group message only to the recipient who requested it
642 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
645 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfo g
) {
646 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
649 .withMembers(new ArrayList
<>(g
.getMembers()));
651 File aFile
= getGroupAvatarFile(g
.groupId
);
652 if (aFile
.exists()) {
654 group
.withAvatar(Utils
.createAttachment(aFile
));
655 } catch (IOException e
) {
656 throw new AttachmentInvalidException(aFile
.toString(), e
);
660 return SignalServiceDataMessage
.newBuilder()
661 .asGroupMessage(group
.build())
662 .withExpiration(g
.messageExpirationTime
);
665 private void sendGroupInfoRequest(byte[] groupId
, SignalServiceAddress recipient
) throws IOException
, EncapsulatedExceptions
{
666 if (groupId
== null) {
670 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
673 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
674 .asGroupMessage(group
.build());
676 // Send group info request message to the recipient who sent us a message with this groupId
677 sendMessageLegacy(messageBuilder
, Collections
.singleton(recipient
));
680 private void sendReceipt(SignalServiceAddress remoteAddress
, long messageId
) throws IOException
, UntrustedIdentityException
{
681 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
682 Collections
.singletonList(messageId
),
683 System
.currentTimeMillis());
685 getMessageSender().sendReceipt(remoteAddress
, getAccessFor(remoteAddress
), receiptMessage
);
689 public void sendMessage(String message
, List
<String
> attachments
, String recipient
)
690 throws EncapsulatedExceptions
, AttachmentInvalidException
, IOException
, InvalidNumberException
{
691 List
<String
> recipients
= new ArrayList
<>(1);
692 recipients
.add(recipient
);
693 sendMessage(message
, attachments
, recipients
);
697 public void sendMessage(String messageText
, List
<String
> attachments
,
698 List
<String
> recipients
)
699 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
700 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().withBody(messageText
);
701 if (attachments
!= null) {
702 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
704 // Upload attachments here, so we only upload once even for multiple recipients
705 SignalServiceMessageSender messageSender
= getMessageSender();
706 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
707 for (SignalServiceAttachment attachment
: attachmentStreams
) {
708 if (attachment
.isStream()) {
709 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
710 } else if (attachment
.isPointer()) {
711 attachmentPointers
.add(attachment
.asPointer());
715 messageBuilder
.withAttachments(attachmentPointers
);
717 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
718 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
721 public void sendMessageReaction(String emoji
, boolean remove
, SignalServiceAddress targetAuthor
,
722 long targetSentTimestamp
, List
<String
> recipients
)
723 throws IOException
, EncapsulatedExceptions
, AttachmentInvalidException
, InvalidNumberException
{
724 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
, remove
, targetAuthor
, targetSentTimestamp
);
725 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
726 .withReaction(reaction
)
727 .withProfileKey(account
.getProfileKey().serialize());
728 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
732 public void sendEndSessionMessage(List
<String
> recipients
) throws IOException
, EncapsulatedExceptions
, InvalidNumberException
{
733 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
734 .asEndSessionMessage();
736 sendMessageLegacy(messageBuilder
, getSignalServiceAddresses(recipients
));
740 public String
getContactName(String number
) throws InvalidNumberException
{
741 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
742 ContactInfo contact
= account
.getContactStore().getContact(new SignalServiceAddress(null, canonicalizedNumber
));
743 if (contact
== null) {
751 public void setContactName(String number
, String name
) throws InvalidNumberException
{
752 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
753 final SignalServiceAddress address
= new SignalServiceAddress(null, canonicalizedNumber
);
754 ContactInfo contact
= account
.getContactStore().getContact(address
);
755 if (contact
== null) {
756 contact
= new ContactInfo(address
);
757 System
.err
.println("Add contact " + canonicalizedNumber
+ " named " + name
);
759 System
.err
.println("Updating contact " + canonicalizedNumber
+ " name " + contact
.name
+ " -> " + name
);
762 account
.getContactStore().updateContact(contact
);
767 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
768 number
= Utils
.canonicalizeNumber(number
, account
.getUsername());
769 final SignalServiceAddress address
= new SignalServiceAddress(null, number
);
770 ContactInfo contact
= account
.getContactStore().getContact(address
);
771 if (contact
== null) {
772 contact
= new ContactInfo(address
);
773 System
.err
.println("Adding and " + (blocked ?
"blocking" : "unblocking") + " contact " + number
);
775 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " contact " + number
);
777 contact
.blocked
= blocked
;
778 account
.getContactStore().updateContact(contact
);
783 public void setGroupBlocked(final byte[] groupId
, final boolean blocked
) throws GroupNotFoundException
{
784 GroupInfo group
= getGroup(groupId
);
786 throw new GroupNotFoundException(groupId
);
788 System
.err
.println((blocked ?
"Blocking" : "Unblocking") + " group " + Base64
.encodeBytes(groupId
));
789 group
.blocked
= blocked
;
790 account
.getGroupStore().updateGroup(group
);
796 public List
<byte[]> getGroupIds() {
797 List
<GroupInfo
> groups
= getGroups();
798 List
<byte[]> ids
= new ArrayList
<>(groups
.size());
799 for (GroupInfo group
: groups
) {
800 ids
.add(group
.groupId
);
806 public String
getGroupName(byte[] groupId
) {
807 GroupInfo group
= getGroup(groupId
);
816 public List
<String
> getGroupMembers(byte[] groupId
) {
817 GroupInfo group
= getGroup(groupId
);
819 return Collections
.emptyList();
821 return new ArrayList
<>(group
.getMembersE164());
826 public byte[] updateGroup(byte[] groupId
, String name
, List
<String
> members
, String avatar
) throws IOException
, EncapsulatedExceptions
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
{
827 if (groupId
.length
== 0) {
830 if (name
.isEmpty()) {
833 if (members
.size() == 0) {
836 if (avatar
.isEmpty()) {
839 return sendUpdateGroupMessage(groupId
, name
, members
== null ?
null : getSignalServiceAddresses(members
), avatar
);
843 * Change the expiration timer for a contact
845 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) {
846 ContactInfo c
= account
.getContactStore().getContact(address
);
847 c
.messageExpirationTime
= messageExpirationTimer
;
848 account
.getContactStore().updateContact(c
);
852 * Change the expiration timer for a group
854 public void setExpirationTimer(byte[] groupId
, int messageExpirationTimer
) {
855 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
856 g
.messageExpirationTime
= messageExpirationTimer
;
857 account
.getGroupStore().updateGroup(g
);
861 * Upload the sticker pack from path.
863 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
864 * @return if successful, returns the URL to install the sticker pack in the signal app
866 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
867 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
869 SignalServiceMessageSender messageSender
= getMessageSender();
871 byte[] packKey
= KeyUtils
.createStickerUploadKey();
872 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
875 return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder
.encode(packId
, "utf-8") + "&pack_key=" + URLEncoder
.encode(Hex
.toStringCondensed(packKey
), "utf-8"))
877 } catch (URISyntaxException e
) {
878 throw new AssertionError(e
);
882 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(final String path
) throws IOException
, StickerPackInvalidException
{
884 String rootPath
= null;
886 final File file
= new File(path
);
887 if (file
.getName().endsWith(".zip")) {
888 zip
= new ZipFile(file
);
889 } else if (file
.getName().equals("manifest.json")) {
890 rootPath
= file
.getParent();
892 throw new StickerPackInvalidException("Could not find manifest.json");
895 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
897 if (pack
.stickers
== null) {
898 throw new StickerPackInvalidException("Must set a 'stickers' field.");
901 if (pack
.stickers
.isEmpty()) {
902 throw new StickerPackInvalidException("Must include stickers.");
905 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
906 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
907 if (sticker
.file
== null) {
908 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
911 Pair
<InputStream
, Long
> data
;
913 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
914 } catch (IOException ignored
) {
915 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
918 StickerInfo stickerInfo
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(sticker
.emoji
).or(""));
919 stickers
.add(stickerInfo
);
922 StickerInfo cover
= null;
923 if (pack
.cover
!= null) {
924 if (pack
.cover
.file
== null) {
925 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
928 Pair
<InputStream
, Long
> data
;
930 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
931 } catch (IOException ignored
) {
932 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
935 cover
= new StickerInfo(data
.first(), data
.second(), Optional
.fromNullable(pack
.cover
.emoji
).or(""));
938 return new SignalServiceStickerManifestUpload(
945 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
946 InputStream inputStream
;
948 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
950 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
952 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
955 private static Pair
<InputStream
, Long
> getInputStreamAndLength(final String rootPath
, final ZipFile zip
, final String subfile
) throws IOException
{
957 final ZipEntry entry
= zip
.getEntry(subfile
);
958 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
960 final File file
= new File(rootPath
, subfile
);
961 return new Pair
<>(new FileInputStream(file
), file
.length());
965 private void requestSyncGroups() throws IOException
{
966 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
).build();
967 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
969 sendSyncMessage(message
);
970 } catch (UntrustedIdentityException e
) {
975 private void requestSyncContacts() throws IOException
{
976 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
).build();
977 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
979 sendSyncMessage(message
);
980 } catch (UntrustedIdentityException e
) {
985 private void requestSyncBlocked() throws IOException
{
986 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
).build();
987 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
989 sendSyncMessage(message
);
990 } catch (UntrustedIdentityException e
) {
995 private void requestSyncConfiguration() throws IOException
{
996 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder().setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
).build();
997 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
999 sendSyncMessage(message
);
1000 } catch (UntrustedIdentityException e
) {
1001 e
.printStackTrace();
1005 private byte[] getSenderCertificate() throws IOException
{
1006 byte[] certificate
= accountManager
.getSenderCertificate();
1007 // TODO cache for a day
1011 private byte[] getSelfUnidentifiedAccessKey() {
1012 return UnidentifiedAccess
.deriveAccessKeyFrom(account
.getProfileKey());
1015 private static SignalProfile
decryptProfile(SignalServiceProfile encryptedProfile
, ProfileKey profileKey
) throws IOException
{
1016 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
1018 return new SignalProfile(
1019 encryptedProfile
.getIdentityKey(),
1020 encryptedProfile
.getName() == null ?
null : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName()))),
1021 encryptedProfile
.getAvatar(),
1022 encryptedProfile
.getUnidentifiedAccess() == null || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess())) ?
null : encryptedProfile
.getUnidentifiedAccess(),
1023 encryptedProfile
.isUnrestrictedUnidentifiedAccess()
1025 } catch (InvalidCiphertextException e
) {
1030 private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient
) throws IOException
{
1031 ContactInfo contact
= account
.getContactStore().getContact(recipient
);
1032 if (contact
== null || contact
.profileKey
== null) {
1035 ProfileKey theirProfileKey
;
1037 theirProfileKey
= new ProfileKey(Base64
.decode(contact
.profileKey
));
1038 } catch (InvalidInputException e
) {
1039 throw new AssertionError(e
);
1041 SignalProfile targetProfile
= decryptProfile(getRecipientProfile(recipient
, Optional
.absent()), theirProfileKey
);
1043 if (targetProfile
== null || targetProfile
.getUnidentifiedAccess() == null) {
1047 if (targetProfile
.isUnrestrictedUnidentifiedAccess()) {
1048 return KeyUtils
.createUnrestrictedUnidentifiedAccess();
1051 return UnidentifiedAccess
.deriveAccessKeyFrom(theirProfileKey
);
1054 private Optional
<UnidentifiedAccessPair
> getAccessForSync() throws IOException
{
1055 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1056 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1058 if (selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1059 return Optional
.absent();
1063 return Optional
.of(new UnidentifiedAccessPair(
1064 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1065 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1067 } catch (InvalidCertificateException e
) {
1068 return Optional
.absent();
1072 private List
<Optional
<UnidentifiedAccessPair
>> getAccessFor(Collection
<SignalServiceAddress
> recipients
) throws IOException
{
1073 List
<Optional
<UnidentifiedAccessPair
>> result
= new ArrayList
<>(recipients
.size());
1074 for (SignalServiceAddress recipient
: recipients
) {
1075 result
.add(getAccessFor(recipient
));
1080 private Optional
<UnidentifiedAccessPair
> getAccessFor(SignalServiceAddress recipient
) throws IOException
{
1081 byte[] recipientUnidentifiedAccessKey
= getTargetUnidentifiedAccessKey(recipient
);
1082 byte[] selfUnidentifiedAccessKey
= getSelfUnidentifiedAccessKey();
1083 byte[] selfUnidentifiedAccessCertificate
= getSenderCertificate();
1085 if (recipientUnidentifiedAccessKey
== null || selfUnidentifiedAccessKey
== null || selfUnidentifiedAccessCertificate
== null) {
1086 return Optional
.absent();
1090 return Optional
.of(new UnidentifiedAccessPair(
1091 new UnidentifiedAccess(recipientUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
),
1092 new UnidentifiedAccess(selfUnidentifiedAccessKey
, selfUnidentifiedAccessCertificate
)
1094 } catch (InvalidCertificateException e
) {
1095 return Optional
.absent();
1099 private void sendSyncMessage(SignalServiceSyncMessage message
)
1100 throws IOException
, UntrustedIdentityException
{
1101 SignalServiceMessageSender messageSender
= getMessageSender();
1103 messageSender
.sendMessage(message
, getAccessForSync());
1104 } catch (UntrustedIdentityException e
) {
1105 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1111 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
1113 private void sendMessageLegacy(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1114 throws EncapsulatedExceptions
, IOException
{
1115 List
<SendMessageResult
> results
= sendMessage(messageBuilder
, recipients
);
1117 List
<UntrustedIdentityException
> untrustedIdentities
= new LinkedList
<>();
1118 List
<UnregisteredUserException
> unregisteredUsers
= new LinkedList
<>();
1119 List
<NetworkFailureException
> networkExceptions
= new LinkedList
<>();
1121 for (SendMessageResult result
: results
) {
1122 if (result
.isUnregisteredFailure()) {
1123 unregisteredUsers
.add(new UnregisteredUserException(result
.getAddress().getNumber().get(), null));
1124 } else if (result
.isNetworkFailure()) {
1125 networkExceptions
.add(new NetworkFailureException(result
.getAddress().getNumber().get(), null));
1126 } else if (result
.getIdentityFailure() != null) {
1127 untrustedIdentities
.add(new UntrustedIdentityException("Untrusted", result
.getAddress().getNumber().get(), result
.getIdentityFailure().getIdentityKey()));
1130 if (!untrustedIdentities
.isEmpty() || !unregisteredUsers
.isEmpty() || !networkExceptions
.isEmpty()) {
1131 throw new EncapsulatedExceptions(untrustedIdentities
, unregisteredUsers
, networkExceptions
);
1135 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1136 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1137 final String username
= account
.getUsername();
1139 for (String number
: numbers
) {
1140 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, username
);
1141 if (canonicalizedNumber
.equals(username
)) {
1142 signalServiceAddresses
.add(account
.getSelfAddress());
1144 // TODO get corresponding uuid
1145 signalServiceAddresses
.add(new SignalServiceAddress(null, canonicalizedNumber
));
1148 return signalServiceAddresses
;
1151 private List
<SendMessageResult
> sendMessage(SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
)
1152 throws IOException
{
1153 if (messagePipe
== null) {
1154 messagePipe
= getMessageReceiver().createMessagePipe();
1156 if (unidentifiedMessagePipe
== null) {
1157 unidentifiedMessagePipe
= getMessageReceiver().createUnidentifiedMessagePipe();
1159 SignalServiceDataMessage message
= null;
1161 SignalServiceMessageSender messageSender
= getMessageSender();
1163 message
= messageBuilder
.build();
1164 if (message
.getGroupInfo().isPresent()) {
1166 final boolean isRecipientUpdate
= false;
1167 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
), getAccessFor(recipients
), isRecipientUpdate
, message
);
1168 for (SendMessageResult r
: result
) {
1169 if (r
.getIdentityFailure() != null) {
1170 account
.getSignalProtocolStore().saveIdentity(r
.getAddress().getNumber().get(), r
.getIdentityFailure().getIdentityKey(), TrustLevel
.UNTRUSTED
);
1174 } catch (UntrustedIdentityException e
) {
1175 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1176 return Collections
.emptyList();
1178 } else if (recipients
.size() == 1 && recipients
.contains(account
.getSelfAddress())) {
1179 SignalServiceAddress recipient
= account
.getSelfAddress();
1180 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= getAccessFor(recipient
);
1181 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1182 message
.getTimestamp(),
1184 message
.getExpiresInSeconds(),
1185 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1187 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1189 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1191 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1192 } catch (UntrustedIdentityException e
) {
1193 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1194 results
.add(SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey()));
1198 // Send to all individually, so sync messages are sent correctly
1199 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1200 for (SignalServiceAddress address
: recipients
) {
1201 ContactInfo contact
= account
.getContactStore().getContact(address
);
1202 if (contact
!= null) {
1203 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1205 messageBuilder
.withExpiration(0);
1207 message
= messageBuilder
.build();
1209 SendMessageResult result
= messageSender
.sendMessage(address
, getAccessFor(address
), message
);
1210 results
.add(result
);
1211 } catch (UntrustedIdentityException e
) {
1212 account
.getSignalProtocolStore().saveIdentity(e
.getIdentifier(), e
.getIdentityKey(), TrustLevel
.UNTRUSTED
);
1213 results
.add(SendMessageResult
.identityFailure(address
, e
.getIdentityKey()));
1219 if (message
!= null && message
.isEndSession()) {
1220 for (SignalServiceAddress recipient
: recipients
) {
1221 handleEndSession(recipient
.getNumber().get());
1228 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, ProtocolUntrustedIdentityException
, SelfSendException
, UnsupportedDataMessageException
{
1229 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(), account
.getSignalProtocolStore(), Utils
.getCertificateValidator());
1231 return cipher
.decrypt(envelope
);
1232 } catch (ProtocolUntrustedIdentityException e
) {
1233 // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else
1234 // account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
1239 private void handleEndSession(String source
) {
1240 account
.getSignalProtocolStore().deleteAllSessions(source
);
1243 private void handleSignalServiceDataMessage(SignalServiceDataMessage message
, boolean isSync
, SignalServiceAddress source
, SignalServiceAddress destination
, boolean ignoreAttachments
) {
1244 if (message
.getGroupInfo().isPresent()) {
1245 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1246 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1247 switch (groupInfo
.getType()) {
1249 if (group
== null) {
1250 group
= new GroupInfo(groupInfo
.getGroupId());
1253 if (groupInfo
.getAvatar().isPresent()) {
1254 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1255 if (avatar
.isPointer()) {
1257 retrieveGroupAvatarAttachment(avatar
.asPointer(), group
.groupId
);
1258 } catch (IOException
| InvalidMessageException e
) {
1259 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer().getId() + "): " + e
.getMessage());
1264 if (groupInfo
.getName().isPresent()) {
1265 group
.name
= groupInfo
.getName().get();
1268 if (groupInfo
.getMembers().isPresent()) {
1269 group
.addMembers(groupInfo
.getMembers().get());
1272 account
.getGroupStore().updateGroup(group
);
1275 if (group
== null) {
1277 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1278 } catch (IOException
| EncapsulatedExceptions e
) {
1279 e
.printStackTrace();
1284 if (group
== null) {
1286 sendGroupInfoRequest(groupInfo
.getGroupId(), source
);
1287 } catch (IOException
| EncapsulatedExceptions e
) {
1288 e
.printStackTrace();
1291 group
.removeMember(source
);
1292 account
.getGroupStore().updateGroup(group
);
1296 if (group
!= null) {
1298 sendUpdateGroupMessage(groupInfo
.getGroupId(), source
);
1299 } catch (IOException
| EncapsulatedExceptions e
) {
1300 e
.printStackTrace();
1301 } catch (NotAGroupMemberException e
) {
1302 // We have left this group, so don't send a group update message
1308 if (message
.isEndSession()) {
1309 handleEndSession(isSync ? destination
.getNumber().get() : source
.getNumber().get());
1311 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1312 if (message
.getGroupInfo().isPresent()) {
1313 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1314 GroupInfo group
= account
.getGroupStore().getGroup(groupInfo
.getGroupId());
1315 if (group
== null) {
1316 group
= new GroupInfo(groupInfo
.getGroupId());
1318 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1319 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1320 account
.getGroupStore().updateGroup(group
);
1323 ContactInfo contact
= account
.getContactStore().getContact(isSync ? destination
: source
);
1324 if (contact
== null) {
1325 contact
= new ContactInfo(isSync ? destination
: source
);
1327 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1328 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1329 account
.getContactStore().updateContact(contact
);
1333 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1334 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1335 if (attachment
.isPointer()) {
1337 retrieveAttachment(attachment
.asPointer());
1338 } catch (IOException
| InvalidMessageException e
) {
1339 System
.err
.println("Failed to retrieve attachment (" + attachment
.asPointer().getId() + "): " + e
.getMessage());
1344 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1345 if (source
.matches(account
.getSelfAddress())) {
1347 this.account
.setProfileKey(new ProfileKey(message
.getProfileKey().get()));
1348 } catch (InvalidInputException ignored
) {
1350 ContactInfo contact
= account
.getContactStore().getContact(source
);
1351 if (contact
!= null) {
1352 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1353 account
.getContactStore().updateContact(contact
);
1356 ContactInfo contact
= account
.getContactStore().getContact(source
);
1357 if (contact
== null) {
1358 contact
= new ContactInfo(source
);
1360 contact
.profileKey
= Base64
.encodeBytes(message
.getProfileKey().get());
1361 account
.getContactStore().updateContact(contact
);
1364 if (message
.getPreviews().isPresent()) {
1365 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1366 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1367 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1368 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1370 retrieveAttachment(attachment
);
1371 } catch (IOException
| InvalidMessageException e
) {
1372 System
.err
.println("Failed to retrieve attachment (" + attachment
.getId() + "): " + e
.getMessage());
1379 private void retryFailedReceivedMessages(ReceiveMessageHandler handler
, boolean ignoreAttachments
) {
1380 final File cachePath
= new File(getMessageCachePath());
1381 if (!cachePath
.exists()) {
1384 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1385 if (!dir
.isDirectory()) {
1389 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1390 if (!fileEntry
.isFile()) {
1393 SignalServiceEnvelope envelope
;
1395 envelope
= Utils
.loadEnvelope(fileEntry
);
1396 if (envelope
== null) {
1399 } catch (IOException e
) {
1400 e
.printStackTrace();
1403 SignalServiceContent content
= null;
1404 if (!envelope
.isReceipt()) {
1406 content
= decryptMessage(envelope
);
1407 } catch (Exception e
) {
1410 handleMessage(envelope
, content
, ignoreAttachments
);
1413 handler
.handleMessage(envelope
, content
, null);
1415 Files
.delete(fileEntry
.toPath());
1416 } catch (IOException e
) {
1417 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1420 // Try to delete directory if empty
1425 public void receiveMessages(long timeout
, TimeUnit unit
, boolean returnOnTimeout
, boolean ignoreAttachments
, ReceiveMessageHandler handler
) throws IOException
{
1426 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1427 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1430 if (messagePipe
== null) {
1431 messagePipe
= messageReceiver
.createMessagePipe();
1435 SignalServiceEnvelope envelope
;
1436 SignalServiceContent content
= null;
1437 Exception exception
= null;
1438 final long now
= new Date().getTime();
1440 envelope
= messagePipe
.read(timeout
, unit
, envelope1
-> {
1441 // store message on disk, before acknowledging receipt to the server
1443 File cacheFile
= getMessageCacheFile(envelope1
.getSourceE164().get(), now
, envelope1
.getTimestamp());
1444 Utils
.storeEnvelope(envelope1
, cacheFile
);
1445 } catch (IOException e
) {
1446 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: " + e
.getMessage());
1449 } catch (TimeoutException e
) {
1450 if (returnOnTimeout
)
1453 } catch (InvalidVersionException e
) {
1454 System
.err
.println("Ignoring error: " + e
.getMessage());
1457 if (!envelope
.isReceipt()) {
1459 content
= decryptMessage(envelope
);
1460 } catch (Exception e
) {
1463 handleMessage(envelope
, content
, ignoreAttachments
);
1466 if (!isMessageBlocked(envelope
, content
)) {
1467 handler
.handleMessage(envelope
, content
, exception
);
1469 if (!(exception
instanceof ProtocolUntrustedIdentityException
)) {
1470 File cacheFile
= null;
1472 cacheFile
= getMessageCacheFile(envelope
.getSourceE164().get(), now
, envelope
.getTimestamp());
1473 Files
.delete(cacheFile
.toPath());
1474 // Try to delete directory if empty
1475 new File(getMessageCachePath()).delete();
1476 } catch (IOException e
) {
1477 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1482 if (messagePipe
!= null) {
1483 messagePipe
.shutdown();
1489 private boolean isMessageBlocked(SignalServiceEnvelope envelope
, SignalServiceContent content
) {
1490 SignalServiceAddress source
;
1491 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1492 source
= envelope
.getSourceAddress();
1493 } else if (content
!= null) {
1494 source
= content
.getSender();
1498 ContactInfo sourceContact
= getContact(source
.getNumber().get());
1499 if (sourceContact
!= null && sourceContact
.blocked
) {
1503 if (content
!= null && content
.getDataMessage().isPresent()) {
1504 SignalServiceDataMessage message
= content
.getDataMessage().get();
1505 if (message
.getGroupInfo().isPresent()) {
1506 SignalServiceGroup groupInfo
= message
.getGroupInfo().get();
1507 GroupInfo group
= getGroup(groupInfo
.getGroupId());
1508 if (groupInfo
.getType() == SignalServiceGroup
.Type
.DELIVER
&& group
!= null && group
.blocked
) {
1516 private void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
) {
1517 if (content
!= null) {
1518 SignalServiceAddress sender
;
1519 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1520 sender
= envelope
.getSourceAddress();
1522 sender
= content
.getSender();
1524 if (content
.getDataMessage().isPresent()) {
1525 SignalServiceDataMessage message
= content
.getDataMessage().get();
1527 if (content
.isNeedsReceipt()) {
1529 sendReceipt(sender
, message
.getTimestamp());
1530 } catch (IOException
| UntrustedIdentityException e
) {
1531 e
.printStackTrace();
1535 handleSignalServiceDataMessage(message
, false, sender
, account
.getSelfAddress(), ignoreAttachments
);
1537 if (content
.getSyncMessage().isPresent()) {
1538 account
.setMultiDevice(true);
1539 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
1540 if (syncMessage
.getSent().isPresent()) {
1541 SentTranscriptMessage message
= syncMessage
.getSent().get();
1542 handleSignalServiceDataMessage(message
.getMessage(), true, sender
, message
.getDestination().orNull(), ignoreAttachments
);
1544 if (syncMessage
.getRequest().isPresent()) {
1545 RequestMessage rm
= syncMessage
.getRequest().get();
1546 if (rm
.isContactsRequest()) {
1549 } catch (UntrustedIdentityException
| IOException e
) {
1550 e
.printStackTrace();
1553 if (rm
.isGroupsRequest()) {
1556 } catch (UntrustedIdentityException
| IOException e
) {
1557 e
.printStackTrace();
1560 if (rm
.isBlockedListRequest()) {
1563 } catch (UntrustedIdentityException
| IOException e
) {
1564 e
.printStackTrace();
1567 // TODO Handle rm.isConfigurationRequest();
1569 if (syncMessage
.getGroups().isPresent()) {
1570 File tmpFile
= null;
1572 tmpFile
= IOUtils
.createTempFile();
1573 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups().get().asPointer(), tmpFile
)) {
1574 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
1576 while ((g
= s
.read()) != null) {
1577 GroupInfo syncGroup
= account
.getGroupStore().getGroup(g
.getId());
1578 if (syncGroup
== null) {
1579 syncGroup
= new GroupInfo(g
.getId());
1581 if (g
.getName().isPresent()) {
1582 syncGroup
.name
= g
.getName().get();
1584 syncGroup
.addMembers(g
.getMembers());
1585 if (!g
.isActive()) {
1586 syncGroup
.removeMember(account
.getSelfAddress());
1588 // Add ourself to the member set as it's marked as active
1589 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
1591 syncGroup
.blocked
= g
.isBlocked();
1592 if (g
.getColor().isPresent()) {
1593 syncGroup
.color
= g
.getColor().get();
1596 if (g
.getAvatar().isPresent()) {
1597 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.groupId
);
1599 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
1600 syncGroup
.archived
= g
.isArchived();
1601 account
.getGroupStore().updateGroup(syncGroup
);
1604 } catch (Exception e
) {
1605 e
.printStackTrace();
1607 if (tmpFile
!= null) {
1609 Files
.delete(tmpFile
.toPath());
1610 } catch (IOException e
) {
1611 System
.err
.println("Failed to delete received groups temp file “" + tmpFile
+ "”: " + e
.getMessage());
1616 if (syncMessage
.getBlockedList().isPresent()) {
1617 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
1618 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
1619 if (address
.getNumber().isPresent()) {
1621 setContactBlocked(address
.getNumber().get(), true);
1622 } catch (InvalidNumberException e
) {
1623 e
.printStackTrace();
1627 for (byte[] groupId
: blockedListMessage
.getGroupIds()) {
1629 setGroupBlocked(groupId
, true);
1630 } catch (GroupNotFoundException e
) {
1631 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64
.encodeBytes(groupId
));
1635 if (syncMessage
.getContacts().isPresent()) {
1636 File tmpFile
= null;
1638 tmpFile
= IOUtils
.createTempFile();
1639 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
1640 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream().asPointer(), tmpFile
)) {
1641 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
1642 if (contactsMessage
.isComplete()) {
1643 account
.getContactStore().clear();
1646 while ((c
= s
.read()) != null) {
1647 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
1648 account
.setProfileKey(c
.getProfileKey().get());
1650 ContactInfo contact
= account
.getContactStore().getContact(c
.getAddress());
1651 if (contact
== null) {
1652 contact
= new ContactInfo(c
.getAddress());
1654 if (c
.getName().isPresent()) {
1655 contact
.name
= c
.getName().get();
1657 if (c
.getColor().isPresent()) {
1658 contact
.color
= c
.getColor().get();
1660 if (c
.getProfileKey().isPresent()) {
1661 contact
.profileKey
= Base64
.encodeBytes(c
.getProfileKey().get().serialize());
1663 if (c
.getVerified().isPresent()) {
1664 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
1665 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1667 if (c
.getExpirationTimer().isPresent()) {
1668 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
1670 contact
.blocked
= c
.isBlocked();
1671 contact
.inboxPosition
= c
.getInboxPosition().orNull();
1672 contact
.archived
= c
.isArchived();
1673 account
.getContactStore().updateContact(contact
);
1675 if (c
.getAvatar().isPresent()) {
1676 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
1680 } catch (Exception e
) {
1681 e
.printStackTrace();
1683 if (tmpFile
!= null) {
1685 Files
.delete(tmpFile
.toPath());
1686 } catch (IOException e
) {
1687 System
.err
.println("Failed to delete received contacts temp file “" + tmpFile
+ "”: " + e
.getMessage());
1692 if (syncMessage
.getVerified().isPresent()) {
1693 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
1694 account
.getSignalProtocolStore().saveIdentity(verifiedMessage
.getDestination().getNumber().get(), verifiedMessage
.getIdentityKey(), TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
1696 if (syncMessage
.getConfiguration().isPresent()) {
1703 private File
getContactAvatarFile(String number
) {
1704 return new File(avatarsPath
, "contact-" + number
);
1707 private File
retrieveContactAvatarAttachment(SignalServiceAttachment attachment
, String number
) throws IOException
, InvalidMessageException
{
1708 IOUtils
.createPrivateDirectories(avatarsPath
);
1709 if (attachment
.isPointer()) {
1710 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1711 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
1713 SignalServiceAttachmentStream stream
= attachment
.asStream();
1714 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
1718 private File
getGroupAvatarFile(byte[] groupId
) {
1719 return new File(avatarsPath
, "group-" + Base64
.encodeBytes(groupId
).replace("/", "_"));
1722 private File
retrieveGroupAvatarAttachment(SignalServiceAttachment attachment
, byte[] groupId
) throws IOException
, InvalidMessageException
{
1723 IOUtils
.createPrivateDirectories(avatarsPath
);
1724 if (attachment
.isPointer()) {
1725 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
1726 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
1728 SignalServiceAttachmentStream stream
= attachment
.asStream();
1729 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
1733 public File
getAttachmentFile(long attachmentId
) {
1734 return new File(attachmentsPath
, attachmentId
+ "");
1737 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
{
1738 IOUtils
.createPrivateDirectories(attachmentsPath
);
1739 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getId()), true);
1742 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
) throws IOException
, InvalidMessageException
{
1743 if (storePreview
&& pointer
.getPreview().isPresent()) {
1744 File previewFile
= new File(outputFile
+ ".preview");
1745 try (OutputStream output
= new FileOutputStream(previewFile
)) {
1746 byte[] preview
= pointer
.getPreview().get();
1747 output
.write(preview
, 0, preview
.length
);
1748 } catch (FileNotFoundException e
) {
1749 e
.printStackTrace();
1754 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1756 File tmpFile
= IOUtils
.createTempFile();
1757 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
)) {
1758 try (OutputStream output
= new FileOutputStream(outputFile
)) {
1759 byte[] buffer
= new byte[4096];
1762 while ((read
= input
.read(buffer
)) != -1) {
1763 output
.write(buffer
, 0, read
);
1765 } catch (FileNotFoundException e
) {
1766 e
.printStackTrace();
1771 Files
.delete(tmpFile
.toPath());
1772 } catch (IOException e
) {
1773 System
.err
.println("Failed to delete received attachment temp file “" + tmpFile
+ "”: " + e
.getMessage());
1779 private InputStream
retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer
, File tmpFile
) throws IOException
, InvalidMessageException
{
1780 final SignalServiceMessageReceiver messageReceiver
= getMessageReceiver();
1781 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, BaseConfig
.MAX_ATTACHMENT_SIZE
);
1785 public boolean isRemote() {
1789 private void sendGroups() throws IOException
, UntrustedIdentityException
{
1790 File groupsFile
= IOUtils
.createTempFile();
1793 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
1794 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
1795 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1796 out
.write(new DeviceGroup(record.groupId
, Optional
.fromNullable(record.name
),
1797 new ArrayList
<>(record.getMembers()), createGroupAvatarAttachment(record.groupId
),
1798 record.isMember(account
.getSelfAddress()), Optional
.of(record.messageExpirationTime
),
1799 Optional
.fromNullable(record.color
), record.blocked
, Optional
.fromNullable(record.inboxPosition
), record.archived
));
1803 if (groupsFile
.exists() && groupsFile
.length() > 0) {
1804 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
1805 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1806 .withStream(groupsFileStream
)
1807 .withContentType("application/octet-stream")
1808 .withLength(groupsFile
.length())
1811 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
1816 Files
.delete(groupsFile
.toPath());
1817 } catch (IOException e
) {
1818 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
1823 public void sendContacts() throws IOException
, UntrustedIdentityException
{
1824 File contactsFile
= IOUtils
.createTempFile();
1827 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
1828 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
1829 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1830 VerifiedMessage verifiedMessage
= null;
1831 if (getIdentities().containsKey(record.number
)) {
1832 JsonIdentityKeyStore
.Identity currentIdentity
= null;
1833 for (JsonIdentityKeyStore
.Identity id
: getIdentities().get(record.number
)) {
1834 if (currentIdentity
== null || id
.getDateAdded().after(currentIdentity
.getDateAdded())) {
1835 currentIdentity
= id
;
1838 if (currentIdentity
!= null) {
1839 verifiedMessage
= new VerifiedMessage(record.getAddress(), currentIdentity
.getIdentityKey(), currentIdentity
.getTrustLevel().toVerifiedState(), currentIdentity
.getDateAdded().getTime());
1843 ProfileKey profileKey
= null;
1845 profileKey
= record.profileKey
== null ?
null : new ProfileKey(Base64
.decode(record.profileKey
));
1846 } catch (InvalidInputException ignored
) {
1848 out
.write(new DeviceContact(record.getAddress(), Optional
.fromNullable(record.name
),
1849 createContactAvatarAttachment(record.number
), Optional
.fromNullable(record.color
),
1850 Optional
.fromNullable(verifiedMessage
), Optional
.fromNullable(profileKey
), record.blocked
,
1851 Optional
.of(record.messageExpirationTime
),
1852 Optional
.fromNullable(record.inboxPosition
), record.archived
));
1855 if (account
.getProfileKey() != null) {
1856 // Send our own profile key as well
1857 out
.write(new DeviceContact(account
.getSelfAddress(),
1858 Optional
.absent(), Optional
.absent(),
1859 Optional
.absent(), Optional
.absent(),
1860 Optional
.of(account
.getProfileKey()),
1861 false, Optional
.absent(), Optional
.absent(), false));
1865 if (contactsFile
.exists() && contactsFile
.length() > 0) {
1866 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
1867 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
1868 .withStream(contactsFileStream
)
1869 .withContentType("application/octet-stream")
1870 .withLength(contactsFile
.length())
1873 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
1878 Files
.delete(contactsFile
.toPath());
1879 } catch (IOException e
) {
1880 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
1885 private void sendBlockedList() throws IOException
, UntrustedIdentityException
{
1886 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
1887 for (ContactInfo
record : account
.getContactStore().getContacts()) {
1888 if (record.blocked
) {
1889 addresses
.add(record.getAddress());
1892 List
<byte[]> groupIds
= new ArrayList
<>();
1893 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
1894 if (record.blocked
) {
1895 groupIds
.add(record.groupId
);
1898 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
1901 private void sendVerifiedMessage(SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
) throws IOException
, UntrustedIdentityException
{
1902 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
, identityKey
, trustLevel
.toVerifiedState(), System
.currentTimeMillis());
1903 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
1906 public List
<ContactInfo
> getContacts() {
1907 return account
.getContactStore().getContacts();
1910 public ContactInfo
getContact(String number
) {
1911 return account
.getContactStore().getContact(new SignalServiceAddress(null, number
));
1914 public GroupInfo
getGroup(byte[] groupId
) {
1915 return account
.getGroupStore().getGroup(groupId
);
1918 public Map
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities() {
1919 return account
.getSignalProtocolStore().getIdentities();
1922 public Pair
<String
, List
<JsonIdentityKeyStore
.Identity
>> getIdentities(String number
) throws InvalidNumberException
{
1923 String canonicalizedNumber
= Utils
.canonicalizeNumber(number
, account
.getUsername());
1924 return new Pair
<>(canonicalizedNumber
, account
.getSignalProtocolStore().getIdentities(canonicalizedNumber
));
1928 * Trust this the identity with this fingerprint
1930 * @param name username of the identity
1931 * @param fingerprint Fingerprint
1933 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) {
1934 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1938 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1939 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
1943 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1945 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1946 } catch (IOException
| UntrustedIdentityException e
) {
1947 e
.printStackTrace();
1956 * Trust this the identity with this safety number
1958 * @param name username of the identity
1959 * @param safetyNumber Safety number
1961 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) {
1962 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1966 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1967 if (!safetyNumber
.equals(computeSafetyNumber(name
, id
.getIdentityKey()))) {
1971 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1973 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
1974 } catch (IOException
| UntrustedIdentityException e
) {
1975 e
.printStackTrace();
1984 * Trust all keys of this identity without verification
1986 * @param name username of the identity
1988 public boolean trustIdentityAllKeys(String name
) {
1989 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(name
);
1993 for (JsonIdentityKeyStore
.Identity id
: ids
) {
1994 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
1995 account
.getSignalProtocolStore().saveIdentity(name
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1997 sendVerifiedMessage(new SignalServiceAddress(null, name
), id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
1998 } catch (IOException
| UntrustedIdentityException e
) {
1999 e
.printStackTrace();
2007 public String
computeSafetyNumber(String theirUsername
, IdentityKey theirIdentityKey
) {
2008 return Utils
.computeSafetyNumber(account
.getUsername(), getIdentity(), theirUsername
, theirIdentityKey
);
2011 public interface ReceiveMessageHandler
{
2013 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);