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
.manager
.helper
.GroupHelper
;
22 import org
.asamk
.signal
.manager
.helper
.ProfileHelper
;
23 import org
.asamk
.signal
.manager
.helper
.UnidentifiedAccessHelper
;
24 import org
.asamk
.signal
.storage
.SignalAccount
;
25 import org
.asamk
.signal
.storage
.contacts
.ContactInfo
;
26 import org
.asamk
.signal
.storage
.groups
.GroupInfo
;
27 import org
.asamk
.signal
.storage
.groups
.GroupInfoV1
;
28 import org
.asamk
.signal
.storage
.groups
.GroupInfoV2
;
29 import org
.asamk
.signal
.storage
.profiles
.SignalProfile
;
30 import org
.asamk
.signal
.storage
.profiles
.SignalProfileEntry
;
31 import org
.asamk
.signal
.storage
.protocol
.JsonIdentityKeyStore
;
32 import org
.asamk
.signal
.storage
.stickers
.Sticker
;
33 import org
.asamk
.signal
.util
.IOUtils
;
34 import org
.asamk
.signal
.util
.Util
;
35 import org
.signal
.libsignal
.metadata
.InvalidMetadataMessageException
;
36 import org
.signal
.libsignal
.metadata
.InvalidMetadataVersionException
;
37 import org
.signal
.libsignal
.metadata
.ProtocolDuplicateMessageException
;
38 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyException
;
39 import org
.signal
.libsignal
.metadata
.ProtocolInvalidKeyIdException
;
40 import org
.signal
.libsignal
.metadata
.ProtocolInvalidMessageException
;
41 import org
.signal
.libsignal
.metadata
.ProtocolInvalidVersionException
;
42 import org
.signal
.libsignal
.metadata
.ProtocolLegacyMessageException
;
43 import org
.signal
.libsignal
.metadata
.ProtocolNoSessionException
;
44 import org
.signal
.libsignal
.metadata
.ProtocolUntrustedIdentityException
;
45 import org
.signal
.libsignal
.metadata
.SelfSendException
;
46 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
47 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
48 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
49 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedMember
;
50 import org
.signal
.zkgroup
.InvalidInputException
;
51 import org
.signal
.zkgroup
.VerificationFailedException
;
52 import org
.signal
.zkgroup
.auth
.AuthCredentialResponse
;
53 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
54 import org
.signal
.zkgroup
.groups
.GroupSecretParams
;
55 import org
.signal
.zkgroup
.profiles
.ClientZkProfileOperations
;
56 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
57 import org
.signal
.zkgroup
.profiles
.ProfileKeyCredential
;
58 import org
.whispersystems
.libsignal
.IdentityKey
;
59 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
60 import org
.whispersystems
.libsignal
.InvalidKeyException
;
61 import org
.whispersystems
.libsignal
.InvalidMessageException
;
62 import org
.whispersystems
.libsignal
.InvalidVersionException
;
63 import org
.whispersystems
.libsignal
.ecc
.Curve
;
64 import org
.whispersystems
.libsignal
.ecc
.ECKeyPair
;
65 import org
.whispersystems
.libsignal
.ecc
.ECPublicKey
;
66 import org
.whispersystems
.libsignal
.state
.PreKeyRecord
;
67 import org
.whispersystems
.libsignal
.state
.SignedPreKeyRecord
;
68 import org
.whispersystems
.libsignal
.util
.KeyHelper
;
69 import org
.whispersystems
.libsignal
.util
.Medium
;
70 import org
.whispersystems
.libsignal
.util
.Pair
;
71 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
72 import org
.whispersystems
.signalservice
.api
.SignalServiceAccountManager
;
73 import org
.whispersystems
.signalservice
.api
.SignalServiceMessagePipe
;
74 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageReceiver
;
75 import org
.whispersystems
.signalservice
.api
.SignalServiceMessageSender
;
76 import org
.whispersystems
.signalservice
.api
.crypto
.InvalidCiphertextException
;
77 import org
.whispersystems
.signalservice
.api
.crypto
.ProfileCipher
;
78 import org
.whispersystems
.signalservice
.api
.crypto
.SignalServiceCipher
;
79 import org
.whispersystems
.signalservice
.api
.crypto
.UnidentifiedAccessPair
;
80 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
81 import org
.whispersystems
.signalservice
.api
.groupsv2
.ClientZkOperations
;
82 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
83 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Api
;
84 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
85 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
86 import org
.whispersystems
.signalservice
.api
.messages
.SendMessageResult
;
87 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
88 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentPointer
;
89 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentRemoteId
;
90 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
91 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceContent
;
92 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
93 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceEnvelope
;
94 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
95 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroupV2
;
96 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceReceiptMessage
;
97 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
;
98 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceStickerManifestUpload
.StickerInfo
;
99 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.BlockedListMessage
;
100 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.ContactsMessage
;
101 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContact
;
102 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsInputStream
;
103 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceContactsOutputStream
;
104 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroup
;
105 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsInputStream
;
106 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceGroupsOutputStream
;
107 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.DeviceInfo
;
108 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.RequestMessage
;
109 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SentTranscriptMessage
;
110 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.SignalServiceSyncMessage
;
111 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.StickerPackOperationMessage
;
112 import org
.whispersystems
.signalservice
.api
.messages
.multidevice
.VerifiedMessage
;
113 import org
.whispersystems
.signalservice
.api
.profiles
.ProfileAndCredential
;
114 import org
.whispersystems
.signalservice
.api
.profiles
.SignalServiceProfile
;
115 import org
.whispersystems
.signalservice
.api
.push
.ContactTokenDetails
;
116 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
117 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.MissingConfigurationException
;
118 import org
.whispersystems
.signalservice
.api
.util
.InvalidNumberException
;
119 import org
.whispersystems
.signalservice
.api
.util
.SleepTimer
;
120 import org
.whispersystems
.signalservice
.api
.util
.StreamDetails
;
121 import org
.whispersystems
.signalservice
.api
.util
.UptimeSleepTimer
;
122 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
123 import org
.whispersystems
.signalservice
.internal
.configuration
.SignalServiceConfiguration
;
124 import org
.whispersystems
.signalservice
.internal
.contacts
.crypto
.Quote
;
125 import org
.whispersystems
.signalservice
.internal
.contacts
.crypto
.UnauthenticatedQuoteException
;
126 import org
.whispersystems
.signalservice
.internal
.contacts
.crypto
.UnauthenticatedResponseException
;
127 import org
.whispersystems
.signalservice
.internal
.push
.SignalServiceProtos
;
128 import org
.whispersystems
.signalservice
.internal
.push
.UnsupportedDataMessageException
;
129 import org
.whispersystems
.signalservice
.internal
.push
.VerifyAccountResponse
;
130 import org
.whispersystems
.signalservice
.internal
.util
.DynamicCredentialsProvider
;
131 import org
.whispersystems
.signalservice
.internal
.util
.Hex
;
132 import org
.whispersystems
.util
.Base64
;
134 import java
.io
.Closeable
;
136 import java
.io
.FileInputStream
;
137 import java
.io
.FileNotFoundException
;
138 import java
.io
.FileOutputStream
;
139 import java
.io
.IOException
;
140 import java
.io
.InputStream
;
141 import java
.io
.OutputStream
;
143 import java
.net
.URISyntaxException
;
144 import java
.net
.URLEncoder
;
145 import java
.nio
.charset
.StandardCharsets
;
146 import java
.nio
.file
.Files
;
147 import java
.nio
.file
.Paths
;
148 import java
.nio
.file
.StandardCopyOption
;
149 import java
.security
.SignatureException
;
150 import java
.util
.ArrayList
;
151 import java
.util
.Arrays
;
152 import java
.util
.Collection
;
153 import java
.util
.Collections
;
154 import java
.util
.Date
;
155 import java
.util
.HashMap
;
156 import java
.util
.HashSet
;
157 import java
.util
.List
;
158 import java
.util
.Locale
;
159 import java
.util
.Map
;
160 import java
.util
.Objects
;
161 import java
.util
.Set
;
162 import java
.util
.UUID
;
163 import java
.util
.concurrent
.ExecutorService
;
164 import java
.util
.concurrent
.TimeUnit
;
165 import java
.util
.concurrent
.TimeoutException
;
166 import java
.util
.stream
.Collectors
;
167 import java
.util
.zip
.ZipEntry
;
168 import java
.util
.zip
.ZipFile
;
170 import static org
.asamk
.signal
.manager
.ServiceConfig
.CDS_MRENCLAVE
;
171 import static org
.asamk
.signal
.manager
.ServiceConfig
.capabilities
;
172 import static org
.asamk
.signal
.manager
.ServiceConfig
.getIasKeyStore
;
174 public class Manager
implements Closeable
{
176 private final SleepTimer timer
= new UptimeSleepTimer();
178 private final SignalServiceConfiguration serviceConfiguration
;
179 private final String userAgent
;
180 private final boolean discoverableByPhoneNumber
= true;
181 private final boolean unrestrictedUnidentifiedAccess
= false;
183 private final SignalAccount account
;
184 private final PathConfig pathConfig
;
185 private SignalServiceAccountManager accountManager
;
186 private GroupsV2Api groupsV2Api
;
187 private final GroupsV2Operations groupsV2Operations
;
189 private SignalServiceMessageReceiver messageReceiver
= null;
190 private SignalServiceMessagePipe messagePipe
= null;
191 private SignalServiceMessagePipe unidentifiedMessagePipe
= null;
193 private final UnidentifiedAccessHelper unidentifiedAccessHelper
;
194 private final ProfileHelper profileHelper
;
195 private final GroupHelper groupHelper
;
198 SignalAccount account
,
199 PathConfig pathConfig
,
200 SignalServiceConfiguration serviceConfiguration
,
203 this.account
= account
;
204 this.pathConfig
= pathConfig
;
205 this.serviceConfiguration
= serviceConfiguration
;
206 this.userAgent
= userAgent
;
207 this.groupsV2Operations
= capabilities
.isGv2() ?
new GroupsV2Operations(ClientZkOperations
.create(
208 serviceConfiguration
)) : null;
209 this.accountManager
= createSignalServiceAccountManager();
210 this.groupsV2Api
= accountManager
.getGroupsV2Api();
212 this.account
.setResolver(this::resolveSignalServiceAddress
);
214 this.unidentifiedAccessHelper
= new UnidentifiedAccessHelper(account
::getProfileKey
,
215 account
.getProfileStore()::getProfileKey
,
216 this::getRecipientProfile
,
217 this::getSenderCertificate
);
218 this.profileHelper
= new ProfileHelper(account
.getProfileStore()::getProfileKey
,
219 unidentifiedAccessHelper
::getAccessFor
,
220 unidentified
-> unidentified ?
getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
221 this::getOrCreateMessageReceiver
);
222 this.groupHelper
= new GroupHelper(this::getRecipientProfileKeyCredential
,
223 this::getRecipientProfile
,
224 account
::getSelfAddress
,
227 this::getGroupAuthForToday
);
230 public String
getUsername() {
231 return account
.getUsername();
234 public SignalServiceAddress
getSelfAddress() {
235 return account
.getSelfAddress();
238 private SignalServiceAccountManager
createSignalServiceAccountManager() {
239 return new SignalServiceAccountManager(serviceConfiguration
,
240 new DynamicCredentialsProvider(account
.getUuid(),
241 account
.getUsername(),
242 account
.getPassword(),
244 account
.getDeviceId()),
250 private IdentityKeyPair
getIdentityKeyPair() {
251 return account
.getSignalProtocolStore().getIdentityKeyPair();
254 public int getDeviceId() {
255 return account
.getDeviceId();
258 private String
getMessageCachePath() {
259 return pathConfig
.getDataPath() + "/" + account
.getUsername() + ".d/msg-cache";
262 private String
getMessageCachePath(String sender
) {
263 if (sender
== null || sender
.isEmpty()) {
264 return getMessageCachePath();
267 return getMessageCachePath() + "/" + sender
.replace("/", "_");
270 private File
getMessageCacheFile(String sender
, long now
, long timestamp
) throws IOException
{
271 String cachePath
= getMessageCachePath(sender
);
272 IOUtils
.createPrivateDirectories(cachePath
);
273 return new File(cachePath
+ "/" + now
+ "_" + timestamp
);
276 public static Manager
init(
277 String username
, String settingsPath
, SignalServiceConfiguration serviceConfiguration
, String userAgent
278 ) throws IOException
{
279 PathConfig pathConfig
= PathConfig
.createDefault(settingsPath
);
281 if (!SignalAccount
.userExists(pathConfig
.getDataPath(), username
)) {
282 IdentityKeyPair identityKey
= KeyHelper
.generateIdentityKeyPair();
283 int registrationId
= KeyHelper
.generateRegistrationId(false);
285 ProfileKey profileKey
= KeyUtils
.createProfileKey();
286 SignalAccount account
= SignalAccount
.create(pathConfig
.getDataPath(),
293 return new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
296 SignalAccount account
= SignalAccount
.load(pathConfig
.getDataPath(), username
);
298 Manager m
= new Manager(account
, pathConfig
, serviceConfiguration
, userAgent
);
300 m
.migrateLegacyConfigs();
305 private void migrateLegacyConfigs() {
306 if (account
.getProfileKey() == null && isRegistered()) {
307 // Old config file, creating new profile key
308 account
.setProfileKey(KeyUtils
.createProfileKey());
311 // Store profile keys only in profile store
312 for (ContactInfo contact
: account
.getContactStore().getContacts()) {
313 String profileKeyString
= contact
.profileKey
;
314 if (profileKeyString
== null) {
317 final ProfileKey profileKey
;
319 profileKey
= new ProfileKey(Base64
.decode(profileKeyString
));
320 } catch (InvalidInputException
| IOException e
) {
323 contact
.profileKey
= null;
324 account
.getProfileStore().storeProfileKey(contact
.getAddress(), profileKey
);
326 // Ensure our profile key is stored in profile store
327 account
.getProfileStore().storeProfileKey(getSelfAddress(), account
.getProfileKey());
330 public void checkAccountState() throws IOException
{
331 if (account
.isRegistered()) {
332 if (accountManager
.getPreKeysCount() < ServiceConfig
.PREKEY_MINIMUM_COUNT
) {
336 if (account
.getUuid() == null) {
337 account
.setUuid(accountManager
.getOwnUuid());
340 updateAccountAttributes();
344 public boolean isRegistered() {
345 return account
.isRegistered();
348 public void register(boolean voiceVerification
, String captcha
) throws IOException
{
349 account
.setPassword(KeyUtils
.createPassword());
351 // Resetting UUID, because registering doesn't work otherwise
352 account
.setUuid(null);
353 accountManager
= createSignalServiceAccountManager();
354 this.groupsV2Api
= accountManager
.getGroupsV2Api();
356 if (voiceVerification
) {
357 accountManager
.requestVoiceVerificationCode(Locale
.getDefault(),
358 Optional
.fromNullable(captcha
),
361 accountManager
.requestSmsVerificationCode(false, Optional
.fromNullable(captcha
), Optional
.absent());
364 account
.setRegistered(false);
368 public void updateAccountAttributes() throws IOException
{
369 accountManager
.setAccountAttributes(account
.getSignalingKey(),
370 account
.getSignalProtocolStore().getLocalRegistrationId(),
372 account
.getRegistrationLockPin(),
373 account
.getRegistrationLock(),
374 unidentifiedAccessHelper
.getSelfUnidentifiedAccessKey(),
375 unrestrictedUnidentifiedAccess
,
377 discoverableByPhoneNumber
);
380 public void setProfile(String name
, File avatar
) throws IOException
{
381 try (final StreamDetails streamDetails
= avatar
== null ?
null : Utils
.createStreamDetailsFromFile(avatar
)) {
382 accountManager
.setVersionedProfile(account
.getUuid(), account
.getProfileKey(), name
, streamDetails
);
386 public void unregister() throws IOException
{
387 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
388 // If this is the master device, other users can't send messages to this number anymore.
389 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
390 accountManager
.setGcmId(Optional
.absent());
392 account
.setRegistered(false);
396 public List
<DeviceInfo
> getLinkedDevices() throws IOException
{
397 List
<DeviceInfo
> devices
= accountManager
.getDevices();
398 account
.setMultiDevice(devices
.size() > 1);
403 public void removeLinkedDevices(int deviceId
) throws IOException
{
404 accountManager
.removeDevice(deviceId
);
405 List
<DeviceInfo
> devices
= accountManager
.getDevices();
406 account
.setMultiDevice(devices
.size() > 1);
410 public void addDeviceLink(URI linkUri
) throws IOException
, InvalidKeyException
{
411 Utils
.DeviceLinkInfo info
= Utils
.parseDeviceLinkUri(linkUri
);
413 addDevice(info
.deviceIdentifier
, info
.deviceKey
);
416 private void addDevice(String deviceIdentifier
, ECPublicKey deviceKey
) throws IOException
, InvalidKeyException
{
417 IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
418 String verificationCode
= accountManager
.getNewDeviceVerificationCode();
420 accountManager
.addDevice(deviceIdentifier
,
423 Optional
.of(account
.getProfileKey().serialize()),
425 account
.setMultiDevice(true);
429 private List
<PreKeyRecord
> generatePreKeys() {
430 List
<PreKeyRecord
> records
= new ArrayList
<>(ServiceConfig
.PREKEY_BATCH_SIZE
);
432 final int offset
= account
.getPreKeyIdOffset();
433 for (int i
= 0; i
< ServiceConfig
.PREKEY_BATCH_SIZE
; i
++) {
434 int preKeyId
= (offset
+ i
) % Medium
.MAX_VALUE
;
435 ECKeyPair keyPair
= Curve
.generateKeyPair();
436 PreKeyRecord
record = new PreKeyRecord(preKeyId
, keyPair
);
441 account
.addPreKeys(records
);
447 private SignedPreKeyRecord
generateSignedPreKey(IdentityKeyPair identityKeyPair
) {
449 ECKeyPair keyPair
= Curve
.generateKeyPair();
450 byte[] signature
= Curve
.calculateSignature(identityKeyPair
.getPrivateKey(),
451 keyPair
.getPublicKey().serialize());
452 SignedPreKeyRecord
record = new SignedPreKeyRecord(account
.getNextSignedPreKeyId(),
453 System
.currentTimeMillis(),
457 account
.addSignedPreKey(record);
461 } catch (InvalidKeyException e
) {
462 throw new AssertionError(e
);
466 public void verifyAccount(String verificationCode
, String pin
) throws IOException
{
467 verificationCode
= verificationCode
.replace("-", "");
468 account
.setSignalingKey(KeyUtils
.createSignalingKey());
469 // TODO make unrestricted unidentified access configurable
470 VerifyAccountResponse response
= accountManager
.verifyAccountWithCode(verificationCode
,
471 account
.getSignalingKey(),
472 account
.getSignalProtocolStore().getLocalRegistrationId(),
476 unidentifiedAccessHelper
.getSelfUnidentifiedAccessKey(),
477 unrestrictedUnidentifiedAccess
,
479 discoverableByPhoneNumber
);
481 UUID uuid
= UuidUtil
.parseOrNull(response
.getUuid());
482 // TODO response.isStorageCapable()
483 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
484 account
.setRegistered(true);
485 account
.setUuid(uuid
);
486 account
.setRegistrationLockPin(pin
);
487 account
.getSignalProtocolStore()
488 .saveIdentity(account
.getSelfAddress(),
489 getIdentityKeyPair().getPublicKey(),
490 TrustLevel
.TRUSTED_VERIFIED
);
496 public void setRegistrationLockPin(Optional
<String
> pin
) throws IOException
{
497 if (pin
.isPresent()) {
498 account
.setRegistrationLockPin(pin
.get());
499 throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
501 account
.setRegistrationLockPin(null);
502 accountManager
.removeRegistrationLockV1();
507 void refreshPreKeys() throws IOException
{
508 List
<PreKeyRecord
> oneTimePreKeys
= generatePreKeys();
509 final IdentityKeyPair identityKeyPair
= getIdentityKeyPair();
510 SignedPreKeyRecord signedPreKeyRecord
= generateSignedPreKey(identityKeyPair
);
512 accountManager
.setPreKeys(identityKeyPair
.getPublicKey(), signedPreKeyRecord
, oneTimePreKeys
);
515 private SignalServiceMessageReceiver
createMessageReceiver() {
516 final ClientZkProfileOperations clientZkProfileOperations
= capabilities
.isGv2() ? ClientZkOperations
.create(
517 serviceConfiguration
).getProfileOperations() : null;
518 return new SignalServiceMessageReceiver(serviceConfiguration
,
520 account
.getUsername(),
521 account
.getPassword(),
522 account
.getDeviceId(),
523 account
.getSignalingKey(),
527 clientZkProfileOperations
);
530 private SignalServiceMessageReceiver
getOrCreateMessageReceiver() {
531 if (messageReceiver
== null) {
532 messageReceiver
= createMessageReceiver();
534 return messageReceiver
;
537 private SignalServiceMessagePipe
getOrCreateMessagePipe() {
538 if (messagePipe
== null) {
539 messagePipe
= getOrCreateMessageReceiver().createMessagePipe();
544 private SignalServiceMessagePipe
getOrCreateUnidentifiedMessagePipe() {
545 if (unidentifiedMessagePipe
== null) {
546 unidentifiedMessagePipe
= getOrCreateMessageReceiver().createUnidentifiedMessagePipe();
548 return unidentifiedMessagePipe
;
551 private SignalServiceMessageSender
createMessageSender() {
552 final ClientZkProfileOperations clientZkProfileOperations
= capabilities
.isGv2() ? ClientZkOperations
.create(
553 serviceConfiguration
).getProfileOperations() : null;
554 final ExecutorService executor
= null;
555 return new SignalServiceMessageSender(serviceConfiguration
,
557 account
.getUsername(),
558 account
.getPassword(),
559 account
.getDeviceId(),
560 account
.getSignalProtocolStore(),
562 account
.isMultiDevice(),
563 Optional
.fromNullable(messagePipe
),
564 Optional
.fromNullable(unidentifiedMessagePipe
),
566 clientZkProfileOperations
,
568 ServiceConfig
.MAX_ENVELOPE_SIZE
);
571 private SignalServiceProfile
getEncryptedRecipientProfile(SignalServiceAddress address
) throws IOException
{
572 return profileHelper
.retrieveProfileSync(address
, SignalServiceProfile
.RequestType
.PROFILE
).getProfile();
575 private SignalProfile
getRecipientProfile(
576 SignalServiceAddress address
578 SignalProfileEntry profileEntry
= account
.getProfileStore().getProfileEntry(address
);
579 if (profileEntry
== null) {
582 long now
= new Date().getTime();
583 // Profiles are cache for 24h before retrieving them again
584 if (!profileEntry
.isRequestPending() && (
585 profileEntry
.getProfile() == null || now
- profileEntry
.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000
587 ProfileKey profileKey
= profileEntry
.getProfileKey();
588 profileEntry
.setRequestPending(true);
589 SignalProfile profile
;
591 profile
= retrieveRecipientProfile(address
, profileKey
);
592 } catch (IOException e
) {
593 System
.err
.println("Failed to retrieve profile, ignoring: " + e
.getMessage());
594 profileEntry
.setRequestPending(false);
597 profileEntry
.setRequestPending(false);
598 account
.getProfileStore()
599 .updateProfile(address
, profileKey
, now
, profile
, profileEntry
.getProfileKeyCredential());
602 return profileEntry
.getProfile();
605 private ProfileKeyCredential
getRecipientProfileKeyCredential(SignalServiceAddress address
) {
606 SignalProfileEntry profileEntry
= account
.getProfileStore().getProfileEntry(address
);
607 if (profileEntry
== null) {
610 if (profileEntry
.getProfileKeyCredential() == null) {
611 ProfileAndCredential profileAndCredential
;
613 profileAndCredential
= profileHelper
.retrieveProfileSync(address
,
614 SignalServiceProfile
.RequestType
.PROFILE_AND_CREDENTIAL
);
615 } catch (IOException e
) {
616 System
.err
.println("Failed to retrieve profile key credential, ignoring: " + e
.getMessage());
620 long now
= new Date().getTime();
621 final ProfileKeyCredential profileKeyCredential
= profileAndCredential
.getProfileKeyCredential().orNull();
622 final SignalProfile profile
= decryptProfile(address
,
623 profileEntry
.getProfileKey(),
624 profileAndCredential
.getProfile());
625 account
.getProfileStore()
626 .updateProfile(address
, profileEntry
.getProfileKey(), now
, profile
, profileKeyCredential
);
627 return profileKeyCredential
;
629 return profileEntry
.getProfileKeyCredential();
632 private SignalProfile
retrieveRecipientProfile(
633 SignalServiceAddress address
, ProfileKey profileKey
634 ) throws IOException
{
635 final SignalServiceProfile encryptedProfile
= getEncryptedRecipientProfile(address
);
637 return decryptProfile(address
, profileKey
, encryptedProfile
);
640 private SignalProfile
decryptProfile(
641 final SignalServiceAddress address
, final ProfileKey profileKey
, final SignalServiceProfile encryptedProfile
643 File avatarFile
= null;
645 avatarFile
= encryptedProfile
.getAvatar() == null
647 : retrieveProfileAvatar(address
, encryptedProfile
.getAvatar(), profileKey
);
648 } catch (Throwable e
) {
649 System
.err
.println("Failed to retrieve profile avatar, ignoring: " + e
.getMessage());
652 ProfileCipher profileCipher
= new ProfileCipher(profileKey
);
656 name
= encryptedProfile
.getName() == null
658 : new String(profileCipher
.decryptName(Base64
.decode(encryptedProfile
.getName())));
659 } catch (IOException e
) {
662 String unidentifiedAccess
;
664 unidentifiedAccess
= encryptedProfile
.getUnidentifiedAccess() == null
665 || !profileCipher
.verifyUnidentifiedAccess(Base64
.decode(encryptedProfile
.getUnidentifiedAccess()))
667 : encryptedProfile
.getUnidentifiedAccess();
668 } catch (IOException e
) {
669 unidentifiedAccess
= null;
671 return new SignalProfile(encryptedProfile
.getIdentityKey(),
675 encryptedProfile
.isUnrestrictedUnidentifiedAccess(),
676 encryptedProfile
.getCapabilities());
677 } catch (InvalidCiphertextException e
) {
682 private Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(GroupId groupId
) throws IOException
{
683 File file
= getGroupAvatarFile(groupId
);
684 if (!file
.exists()) {
685 return Optional
.absent();
688 return Optional
.of(Utils
.createAttachment(file
));
691 private Optional
<SignalServiceAttachmentStream
> createContactAvatarAttachment(String number
) throws IOException
{
692 File file
= getContactAvatarFile(number
);
693 if (!file
.exists()) {
694 return Optional
.absent();
697 return Optional
.of(Utils
.createAttachment(file
));
700 private GroupInfo
getGroupForSending(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
701 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
703 throw new GroupNotFoundException(groupId
);
705 if (!g
.isMember(account
.getSelfAddress())) {
706 throw new NotAGroupMemberException(groupId
, g
.getTitle());
711 private GroupInfo
getGroupForUpdating(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
712 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
714 throw new GroupNotFoundException(groupId
);
716 if (!g
.isMember(account
.getSelfAddress()) && !g
.isPendingMember(account
.getSelfAddress())) {
717 throw new NotAGroupMemberException(groupId
, g
.getTitle());
722 public List
<GroupInfo
> getGroups() {
723 return account
.getGroupStore().getGroups();
726 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessage(
727 SignalServiceDataMessage
.Builder messageBuilder
, GroupId groupId
728 ) throws IOException
, GroupNotFoundException
, NotAGroupMemberException
{
729 final GroupInfo g
= getGroupForSending(groupId
);
731 GroupUtils
.setGroupContext(messageBuilder
, g
);
732 messageBuilder
.withExpiration(g
.getMessageExpirationTime());
734 return sendMessage(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
737 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessage(
738 String messageText
, List
<String
> attachments
, GroupId groupId
739 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
740 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
741 .withBody(messageText
);
742 if (attachments
!= null) {
743 messageBuilder
.withAttachments(Utils
.getSignalServiceAttachments(attachments
));
746 return sendGroupMessage(messageBuilder
, groupId
);
749 public Pair
<Long
, List
<SendMessageResult
>> sendGroupMessageReaction(
750 String emoji
, boolean remove
, String targetAuthor
, long targetSentTimestamp
, GroupId groupId
751 ) throws IOException
, InvalidNumberException
, NotAGroupMemberException
, GroupNotFoundException
{
752 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
,
754 canonicalizeAndResolveSignalServiceAddress(targetAuthor
),
755 targetSentTimestamp
);
756 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
757 .withReaction(reaction
);
759 return sendGroupMessage(messageBuilder
, groupId
);
762 public Pair
<Long
, List
<SendMessageResult
>> sendQuitGroupMessage(GroupId groupId
) throws GroupNotFoundException
, IOException
, NotAGroupMemberException
{
764 SignalServiceDataMessage
.Builder messageBuilder
;
766 final GroupInfo g
= getGroupForUpdating(groupId
);
767 if (g
instanceof GroupInfoV1
) {
768 GroupInfoV1 groupInfoV1
= (GroupInfoV1
) g
;
769 SignalServiceGroup group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
770 .withId(groupId
.serialize())
772 messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
);
773 groupInfoV1
.removeMember(account
.getSelfAddress());
774 account
.getGroupStore().updateGroup(groupInfoV1
);
776 final GroupInfoV2 groupInfoV2
= (GroupInfoV2
) g
;
777 final Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.leaveGroup(groupInfoV2
);
778 groupInfoV2
.setGroup(groupGroupChangePair
.first());
779 messageBuilder
= getGroupUpdateMessageBuilder(groupInfoV2
, groupGroupChangePair
.second().toByteArray());
780 account
.getGroupStore().updateGroup(groupInfoV2
);
783 return sendMessage(messageBuilder
, g
.getMembersWithout(account
.getSelfAddress()));
786 private Pair
<GroupId
, List
<SendMessageResult
>> sendUpdateGroupMessage(
787 GroupId groupId
, String name
, Collection
<SignalServiceAddress
> members
, String avatarFile
788 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
790 SignalServiceDataMessage
.Builder messageBuilder
;
791 if (groupId
== null) {
793 GroupInfoV2 gv2
= groupHelper
.createGroupV2(name
, members
, avatarFile
);
795 GroupInfoV1 gv1
= new GroupInfoV1(GroupIdV1
.createRandom());
796 gv1
.addMembers(Collections
.singleton(account
.getSelfAddress()));
797 updateGroupV1(gv1
, name
, members
, avatarFile
);
798 messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
801 messageBuilder
= getGroupUpdateMessageBuilder(gv2
, null);
805 GroupInfo group
= getGroupForUpdating(groupId
);
806 if (group
instanceof GroupInfoV2
) {
807 final GroupInfoV2 groupInfoV2
= (GroupInfoV2
) group
;
809 Pair
<Long
, List
<SendMessageResult
>> result
= null;
810 if (groupInfoV2
.isPendingMember(getSelfAddress())) {
811 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.acceptInvite(groupInfoV2
);
812 result
= sendUpdateGroupMessage(groupInfoV2
,
813 groupGroupChangePair
.first(),
814 groupGroupChangePair
.second());
817 if (members
!= null) {
818 final Set
<SignalServiceAddress
> newMembers
= new HashSet
<>(members
);
819 newMembers
.removeAll(group
.getMembers());
820 if (newMembers
.size() > 0) {
821 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.updateGroupV2(groupInfoV2
,
823 result
= sendUpdateGroupMessage(groupInfoV2
,
824 groupGroupChangePair
.first(),
825 groupGroupChangePair
.second());
828 if (result
== null || name
!= null || avatarFile
!= null) {
829 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.updateGroupV2(groupInfoV2
,
832 result
= sendUpdateGroupMessage(groupInfoV2
,
833 groupGroupChangePair
.first(),
834 groupGroupChangePair
.second());
837 return new Pair
<>(group
.getGroupId(), result
.second());
839 GroupInfoV1 gv1
= (GroupInfoV1
) group
;
840 updateGroupV1(gv1
, name
, members
, avatarFile
);
841 messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
846 account
.getGroupStore().updateGroup(g
);
848 final Pair
<Long
, List
<SendMessageResult
>> result
= sendMessage(messageBuilder
,
849 g
.getMembersIncludingPendingWithout(account
.getSelfAddress()));
850 return new Pair
<>(g
.getGroupId(), result
.second());
853 public Pair
<GroupId
, List
<SendMessageResult
>> joinGroup(
854 GroupInviteLinkUrl inviteLinkUrl
855 ) throws IOException
, GroupLinkNotActiveException
{
856 return sendJoinGroupMessage(inviteLinkUrl
);
859 private Pair
<GroupId
, List
<SendMessageResult
>> sendJoinGroupMessage(
860 GroupInviteLinkUrl inviteLinkUrl
861 ) throws IOException
, GroupLinkNotActiveException
{
862 final DecryptedGroupJoinInfo groupJoinInfo
= groupHelper
.getDecryptedGroupJoinInfo(inviteLinkUrl
.getGroupMasterKey(),
863 inviteLinkUrl
.getPassword());
864 final GroupChange groupChange
= groupHelper
.joinGroup(inviteLinkUrl
.getGroupMasterKey(),
865 inviteLinkUrl
.getPassword(),
867 final GroupInfoV2 group
= getOrMigrateGroup(inviteLinkUrl
.getGroupMasterKey(),
868 groupJoinInfo
.getRevision() + 1,
869 groupChange
.toByteArray());
871 if (group
.getGroup() == null) {
872 // Only requested member, can't send update to group members
873 return new Pair
<>(group
.getGroupId(), List
.of());
876 final Pair
<Long
, List
<SendMessageResult
>> result
= sendUpdateGroupMessage(group
, group
.getGroup(), groupChange
);
878 return new Pair
<>(group
.getGroupId(), result
.second());
881 private Pair
<Long
, List
<SendMessageResult
>> sendUpdateGroupMessage(
882 GroupInfoV2 group
, DecryptedGroup newDecryptedGroup
, GroupChange groupChange
883 ) throws IOException
{
884 group
.setGroup(newDecryptedGroup
);
885 final SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(group
,
886 groupChange
.toByteArray());
887 account
.getGroupStore().updateGroup(group
);
888 return sendMessage(messageBuilder
, group
.getMembersIncludingPendingWithout(account
.getSelfAddress()));
891 private void updateGroupV1(
894 final Collection
<SignalServiceAddress
> members
,
895 final String avatarFile
896 ) throws IOException
{
901 if (members
!= null) {
902 final Set
<String
> newE164Members
= new HashSet
<>();
903 for (SignalServiceAddress member
: members
) {
904 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
907 newE164Members
.add(member
.getNumber().get());
910 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
911 if (contacts
.size() != newE164Members
.size()) {
912 // Some of the new members are not registered on Signal
913 for (ContactTokenDetails contact
: contacts
) {
914 newE164Members
.remove(contact
.getNumber());
916 throw new IOException("Failed to add members "
917 + Util
.join(", ", newE164Members
)
918 + " to group: Not registered on Signal");
921 g
.addMembers(members
);
924 if (avatarFile
!= null) {
925 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
926 File aFile
= getGroupAvatarFile(g
.getGroupId());
927 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
931 Pair
<Long
, List
<SendMessageResult
>> sendUpdateGroupMessage(
932 GroupIdV1 groupId
, SignalServiceAddress recipient
933 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
935 GroupInfo group
= getGroupForSending(groupId
);
936 if (!(group
instanceof GroupInfoV1
)) {
937 throw new RuntimeException("Received an invalid group request for a v2 group!");
939 g
= (GroupInfoV1
) group
;
941 if (!g
.isMember(recipient
)) {
942 throw new NotAGroupMemberException(groupId
, g
.name
);
945 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
947 // Send group message only to the recipient who requested it
948 return sendMessage(messageBuilder
, Collections
.singleton(recipient
));
951 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV1 g
) throws AttachmentInvalidException
{
952 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
953 .withId(g
.getGroupId().serialize())
955 .withMembers(new ArrayList
<>(g
.getMembers()));
957 File aFile
= getGroupAvatarFile(g
.getGroupId());
958 if (aFile
.exists()) {
960 group
.withAvatar(Utils
.createAttachment(aFile
));
961 } catch (IOException e
) {
962 throw new AttachmentInvalidException(aFile
.toString(), e
);
966 return SignalServiceDataMessage
.newBuilder()
967 .asGroupMessage(group
.build())
968 .withExpiration(g
.getMessageExpirationTime());
971 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV2 g
, byte[] signedGroupChange
) {
972 SignalServiceGroupV2
.Builder group
= SignalServiceGroupV2
.newBuilder(g
.getMasterKey())
973 .withRevision(g
.getGroup().getRevision())
974 .withSignedGroupChange(signedGroupChange
);
975 return SignalServiceDataMessage
.newBuilder()
976 .asGroupMessage(group
.build())
977 .withExpiration(g
.getMessageExpirationTime());
980 Pair
<Long
, List
<SendMessageResult
>> sendGroupInfoRequest(
981 GroupIdV1 groupId
, SignalServiceAddress recipient
982 ) throws IOException
{
983 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
984 .withId(groupId
.serialize());
986 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
987 .asGroupMessage(group
.build());
989 // Send group info request message to the recipient who sent us a message with this groupId
990 return sendMessage(messageBuilder
, Collections
.singleton(recipient
));
994 SignalServiceAddress remoteAddress
, long messageId
995 ) throws IOException
, UntrustedIdentityException
{
996 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
997 Collections
.singletonList(messageId
),
998 System
.currentTimeMillis());
1000 createMessageSender().sendReceipt(remoteAddress
,
1001 unidentifiedAccessHelper
.getAccessFor(remoteAddress
),
1005 public Pair
<Long
, List
<SendMessageResult
>> sendMessage(
1006 String messageText
, List
<String
> attachments
, List
<String
> recipients
1007 ) throws IOException
, AttachmentInvalidException
, InvalidNumberException
{
1008 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1009 .withBody(messageText
);
1010 if (attachments
!= null) {
1011 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
1013 // Upload attachments here, so we only upload once even for multiple recipients
1014 SignalServiceMessageSender messageSender
= createMessageSender();
1015 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
1016 for (SignalServiceAttachment attachment
: attachmentStreams
) {
1017 if (attachment
.isStream()) {
1018 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
1019 } else if (attachment
.isPointer()) {
1020 attachmentPointers
.add(attachment
.asPointer());
1024 messageBuilder
.withAttachments(attachmentPointers
);
1026 return sendMessage(messageBuilder
, getSignalServiceAddresses(recipients
));
1029 public Pair
<Long
, List
<SendMessageResult
>> sendMessageReaction(
1030 String emoji
, boolean remove
, String targetAuthor
, long targetSentTimestamp
, List
<String
> recipients
1031 ) throws IOException
, InvalidNumberException
{
1032 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
,
1034 canonicalizeAndResolveSignalServiceAddress(targetAuthor
),
1035 targetSentTimestamp
);
1036 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1037 .withReaction(reaction
);
1038 return sendMessage(messageBuilder
, getSignalServiceAddresses(recipients
));
1041 public Pair
<Long
, List
<SendMessageResult
>> sendEndSessionMessage(List
<String
> recipients
) throws IOException
, InvalidNumberException
{
1042 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().asEndSessionMessage();
1044 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
1046 return sendMessage(messageBuilder
, signalServiceAddresses
);
1047 } catch (Exception e
) {
1048 for (SignalServiceAddress address
: signalServiceAddresses
) {
1049 handleEndSession(address
);
1056 public String
getContactName(String number
) throws InvalidNumberException
{
1057 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
1058 if (contact
== null) {
1061 return contact
.name
;
1065 public void setContactName(String number
, String name
) throws InvalidNumberException
{
1066 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
1067 ContactInfo contact
= account
.getContactStore().getContact(address
);
1068 if (contact
== null) {
1069 contact
= new ContactInfo(address
);
1071 contact
.name
= name
;
1072 account
.getContactStore().updateContact(contact
);
1076 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
1077 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
1080 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
1081 ContactInfo contact
= account
.getContactStore().getContact(address
);
1082 if (contact
== null) {
1083 contact
= new ContactInfo(address
);
1085 contact
.blocked
= blocked
;
1086 account
.getContactStore().updateContact(contact
);
1090 public void setGroupBlocked(final GroupId groupId
, final boolean blocked
) throws GroupNotFoundException
{
1091 GroupInfo group
= getGroup(groupId
);
1092 if (group
== null) {
1093 throw new GroupNotFoundException(groupId
);
1096 group
.setBlocked(blocked
);
1097 account
.getGroupStore().updateGroup(group
);
1101 public Pair
<GroupId
, List
<SendMessageResult
>> updateGroup(
1102 GroupId groupId
, String name
, List
<String
> members
, String avatar
1103 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
, NotAGroupMemberException
{
1104 return sendUpdateGroupMessage(groupId
,
1106 members
== null ?
null : getSignalServiceAddresses(members
),
1111 * Change the expiration timer for a contact
1113 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) throws IOException
{
1114 ContactInfo contact
= account
.getContactStore().getContact(address
);
1115 contact
.messageExpirationTime
= messageExpirationTimer
;
1116 account
.getContactStore().updateContact(contact
);
1117 sendExpirationTimerUpdate(address
);
1121 private void sendExpirationTimerUpdate(SignalServiceAddress address
) throws IOException
{
1122 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1123 .asExpirationUpdate();
1124 sendMessage(messageBuilder
, Collections
.singleton(address
));
1128 * Change the expiration timer for a contact
1130 public void setExpirationTimer(
1131 String number
, int messageExpirationTimer
1132 ) throws IOException
, InvalidNumberException
{
1133 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
1134 setExpirationTimer(address
, messageExpirationTimer
);
1138 * Change the expiration timer for a group
1140 public void setExpirationTimer(GroupId groupId
, int messageExpirationTimer
) {
1141 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
1142 if (g
instanceof GroupInfoV1
) {
1143 GroupInfoV1 groupInfoV1
= (GroupInfoV1
) g
;
1144 groupInfoV1
.messageExpirationTime
= messageExpirationTimer
;
1145 account
.getGroupStore().updateGroup(groupInfoV1
);
1147 throw new RuntimeException("TODO Not implemented!");
1152 * Upload the sticker pack from path.
1154 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
1155 * @return if successful, returns the URL to install the sticker pack in the signal app
1157 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
1158 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
1160 SignalServiceMessageSender messageSender
= createMessageSender();
1162 byte[] packKey
= KeyUtils
.createStickerUploadKey();
1163 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
1165 Sticker sticker
= new Sticker(Hex
.fromStringCondensed(packId
), packKey
);
1166 account
.getStickerStore().updateSticker(sticker
);
1170 return new URI("https",
1173 "pack_id=" + URLEncoder
.encode(packId
, StandardCharsets
.UTF_8
) + "&pack_key=" + URLEncoder
.encode(
1174 Hex
.toStringCondensed(packKey
),
1175 StandardCharsets
.UTF_8
)).toString();
1176 } catch (URISyntaxException e
) {
1177 throw new AssertionError(e
);
1181 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(
1183 ) throws IOException
, StickerPackInvalidException
{
1185 String rootPath
= null;
1187 final File file
= new File(path
);
1188 if (file
.getName().endsWith(".zip")) {
1189 zip
= new ZipFile(file
);
1190 } else if (file
.getName().equals("manifest.json")) {
1191 rootPath
= file
.getParent();
1193 throw new StickerPackInvalidException("Could not find manifest.json");
1196 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
1198 if (pack
.stickers
== null) {
1199 throw new StickerPackInvalidException("Must set a 'stickers' field.");
1202 if (pack
.stickers
.isEmpty()) {
1203 throw new StickerPackInvalidException("Must include stickers.");
1206 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
1207 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
1208 if (sticker
.file
== null) {
1209 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
1212 Pair
<InputStream
, Long
> data
;
1214 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
1215 } catch (IOException ignored
) {
1216 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
1219 String contentType
= Utils
.getFileMimeType(new File(sticker
.file
), null);
1220 StickerInfo stickerInfo
= new StickerInfo(data
.first(),
1222 Optional
.fromNullable(sticker
.emoji
).or(""),
1224 stickers
.add(stickerInfo
);
1227 StickerInfo cover
= null;
1228 if (pack
.cover
!= null) {
1229 if (pack
.cover
.file
== null) {
1230 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
1233 Pair
<InputStream
, Long
> data
;
1235 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
1236 } catch (IOException ignored
) {
1237 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
1240 String contentType
= Utils
.getFileMimeType(new File(pack
.cover
.file
), null);
1241 cover
= new StickerInfo(data
.first(),
1243 Optional
.fromNullable(pack
.cover
.emoji
).or(""),
1247 return new SignalServiceStickerManifestUpload(pack
.title
, pack
.author
, cover
, stickers
);
1250 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
1251 InputStream inputStream
;
1253 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
1255 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
1257 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
1260 private static Pair
<InputStream
, Long
> getInputStreamAndLength(
1261 final String rootPath
, final ZipFile zip
, final String subfile
1262 ) throws IOException
{
1264 final ZipEntry entry
= zip
.getEntry(subfile
);
1265 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
1267 final File file
= new File(rootPath
, subfile
);
1268 return new Pair
<>(new FileInputStream(file
), file
.length());
1272 void requestSyncGroups() throws IOException
{
1273 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1274 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
)
1276 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1278 sendSyncMessage(message
);
1279 } catch (UntrustedIdentityException e
) {
1280 e
.printStackTrace();
1284 void requestSyncContacts() throws IOException
{
1285 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1286 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
)
1288 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1290 sendSyncMessage(message
);
1291 } catch (UntrustedIdentityException e
) {
1292 e
.printStackTrace();
1296 void requestSyncBlocked() throws IOException
{
1297 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1298 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
)
1300 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1302 sendSyncMessage(message
);
1303 } catch (UntrustedIdentityException e
) {
1304 e
.printStackTrace();
1308 void requestSyncConfiguration() throws IOException
{
1309 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1310 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
)
1312 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1314 sendSyncMessage(message
);
1315 } catch (UntrustedIdentityException e
) {
1316 e
.printStackTrace();
1320 private byte[] getSenderCertificate() {
1321 // TODO support UUID capable sender certificates
1322 // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
1325 certificate
= accountManager
.getSenderCertificate();
1326 } catch (IOException e
) {
1327 System
.err
.println("Failed to get sender certificate: " + e
);
1330 // TODO cache for a day
1334 private void sendSyncMessage(SignalServiceSyncMessage message
) throws IOException
, UntrustedIdentityException
{
1335 SignalServiceMessageSender messageSender
= createMessageSender();
1337 messageSender
.sendMessage(message
, unidentifiedAccessHelper
.getAccessForSync());
1338 } catch (UntrustedIdentityException e
) {
1339 account
.getSignalProtocolStore()
1340 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1342 TrustLevel
.UNTRUSTED
);
1347 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1348 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1349 final Set
<SignalServiceAddress
> missingUuids
= new HashSet
<>();
1351 for (String number
: numbers
) {
1352 final SignalServiceAddress resolvedAddress
= canonicalizeAndResolveSignalServiceAddress(number
);
1353 if (resolvedAddress
.getUuid().isPresent()) {
1354 signalServiceAddresses
.add(resolvedAddress
);
1356 missingUuids
.add(resolvedAddress
);
1360 Map
<String
, UUID
> registeredUsers
;
1362 registeredUsers
= accountManager
.getRegisteredUsers(getIasKeyStore(),
1363 missingUuids
.stream().map(a
-> a
.getNumber().get()).collect(Collectors
.toSet()),
1365 } catch (IOException
| Quote
.InvalidQuoteFormatException
| UnauthenticatedQuoteException
| SignatureException
| UnauthenticatedResponseException e
) {
1366 System
.err
.println("Failed to resolve uuids from server: " + e
.getMessage());
1367 registeredUsers
= new HashMap
<>();
1370 for (SignalServiceAddress address
: missingUuids
) {
1371 final String number
= address
.getNumber().get();
1372 if (registeredUsers
.containsKey(number
)) {
1373 final SignalServiceAddress newAddress
= resolveSignalServiceAddress(new SignalServiceAddress(
1374 registeredUsers
.get(number
),
1376 signalServiceAddresses
.add(newAddress
);
1378 signalServiceAddresses
.add(address
);
1382 return signalServiceAddresses
;
1385 private Pair
<Long
, List
<SendMessageResult
>> sendMessage(
1386 SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
1387 ) throws IOException
{
1388 recipients
= recipients
.stream().map(this::resolveSignalServiceAddress
).collect(Collectors
.toSet());
1389 final long timestamp
= System
.currentTimeMillis();
1390 messageBuilder
.withTimestamp(timestamp
);
1391 getOrCreateMessagePipe();
1392 getOrCreateUnidentifiedMessagePipe();
1393 SignalServiceDataMessage message
= null;
1395 message
= messageBuilder
.build();
1396 if (message
.getGroupContext().isPresent()) {
1398 SignalServiceMessageSender messageSender
= createMessageSender();
1399 final boolean isRecipientUpdate
= false;
1400 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
),
1401 unidentifiedAccessHelper
.getAccessFor(recipients
),
1404 for (SendMessageResult r
: result
) {
1405 if (r
.getIdentityFailure() != null) {
1406 account
.getSignalProtocolStore()
1407 .saveIdentity(r
.getAddress(),
1408 r
.getIdentityFailure().getIdentityKey(),
1409 TrustLevel
.UNTRUSTED
);
1412 return new Pair
<>(timestamp
, result
);
1413 } catch (UntrustedIdentityException e
) {
1414 account
.getSignalProtocolStore()
1415 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1417 TrustLevel
.UNTRUSTED
);
1418 return new Pair
<>(timestamp
, Collections
.emptyList());
1421 // Send to all individually, so sync messages are sent correctly
1422 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1423 for (SignalServiceAddress address
: recipients
) {
1424 ContactInfo contact
= account
.getContactStore().getContact(address
);
1425 if (contact
!= null) {
1426 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1427 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1429 messageBuilder
.withExpiration(0);
1430 messageBuilder
.withProfileKey(null);
1432 message
= messageBuilder
.build();
1433 if (address
.matches(account
.getSelfAddress())) {
1434 results
.add(sendSelfMessage(message
));
1436 results
.add(sendMessage(address
, message
));
1439 return new Pair
<>(timestamp
, results
);
1442 if (message
!= null && message
.isEndSession()) {
1443 for (SignalServiceAddress recipient
: recipients
) {
1444 handleEndSession(recipient
);
1451 private SendMessageResult
sendSelfMessage(SignalServiceDataMessage message
) throws IOException
{
1452 SignalServiceMessageSender messageSender
= createMessageSender();
1454 SignalServiceAddress recipient
= account
.getSelfAddress();
1456 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= unidentifiedAccessHelper
.getAccessFor(recipient
);
1457 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1458 message
.getTimestamp(),
1460 message
.getExpiresInSeconds(),
1461 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1463 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1466 long startTime
= System
.currentTimeMillis();
1467 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1468 return SendMessageResult
.success(recipient
,
1469 unidentifiedAccess
.isPresent(),
1471 System
.currentTimeMillis() - startTime
);
1472 } catch (UntrustedIdentityException e
) {
1473 account
.getSignalProtocolStore()
1474 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1476 TrustLevel
.UNTRUSTED
);
1477 return SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey());
1481 private SendMessageResult
sendMessage(
1482 SignalServiceAddress address
, SignalServiceDataMessage message
1483 ) throws IOException
{
1484 SignalServiceMessageSender messageSender
= createMessageSender();
1487 return messageSender
.sendMessage(address
, unidentifiedAccessHelper
.getAccessFor(address
), message
);
1488 } catch (UntrustedIdentityException e
) {
1489 account
.getSignalProtocolStore()
1490 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1492 TrustLevel
.UNTRUSTED
);
1493 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
1497 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1498 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(),
1499 account
.getSignalProtocolStore(),
1500 Utils
.getCertificateValidator());
1502 return cipher
.decrypt(envelope
);
1503 } catch (ProtocolUntrustedIdentityException e
) {
1504 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1505 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
1507 account
.getSignalProtocolStore()
1508 .saveIdentity(resolveSignalServiceAddress(identityException
.getName()),
1509 identityException
.getUntrustedIdentity(),
1510 TrustLevel
.UNTRUSTED
);
1511 throw identityException
;
1513 throw new AssertionError(e
);
1517 private void handleEndSession(SignalServiceAddress source
) {
1518 account
.getSignalProtocolStore().deleteAllSessions(source
);
1521 private static int currentTimeDays() {
1522 return (int) TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis());
1525 private GroupsV2AuthorizationString
getGroupAuthForToday(
1526 final GroupSecretParams groupSecretParams
1527 ) throws IOException
{
1528 final int today
= currentTimeDays();
1529 // Returns credentials for the next 7 days
1530 final HashMap
<Integer
, AuthCredentialResponse
> credentials
= groupsV2Api
.getCredentials(today
);
1531 // TODO cache credentials until they expire
1532 AuthCredentialResponse authCredentialResponse
= credentials
.get(today
);
1534 return groupsV2Api
.getGroupsV2AuthorizationString(account
.getUuid(),
1537 authCredentialResponse
);
1538 } catch (VerificationFailedException e
) {
1539 throw new IOException(e
);
1543 private List
<HandleAction
> handleSignalServiceDataMessage(
1544 SignalServiceDataMessage message
,
1546 SignalServiceAddress source
,
1547 SignalServiceAddress destination
,
1548 boolean ignoreAttachments
1550 List
<HandleAction
> actions
= new ArrayList
<>();
1551 if (message
.getGroupContext().isPresent()) {
1552 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1553 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1554 GroupIdV1 groupId
= GroupId
.v1(groupInfo
.getGroupId());
1555 GroupInfo group
= account
.getGroupStore().getGroup(groupId
);
1556 if (group
== null || group
instanceof GroupInfoV1
) {
1557 GroupInfoV1 groupV1
= (GroupInfoV1
) group
;
1558 switch (groupInfo
.getType()) {
1560 if (groupV1
== null) {
1561 groupV1
= new GroupInfoV1(groupId
);
1564 if (groupInfo
.getAvatar().isPresent()) {
1565 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1566 if (avatar
.isPointer()) {
1568 retrieveGroupAvatarAttachment(avatar
.asPointer(), groupV1
.getGroupId());
1569 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1570 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer()
1571 .getRemoteId() + "): " + e
.getMessage());
1576 if (groupInfo
.getName().isPresent()) {
1577 groupV1
.name
= groupInfo
.getName().get();
1580 if (groupInfo
.getMembers().isPresent()) {
1581 groupV1
.addMembers(groupInfo
.getMembers()
1584 .map(this::resolveSignalServiceAddress
)
1585 .collect(Collectors
.toSet()));
1588 account
.getGroupStore().updateGroup(groupV1
);
1592 if (groupV1
== null && !isSync
) {
1593 actions
.add(new SendGroupInfoRequestAction(source
, groupV1
.getGroupId()));
1597 if (groupV1
!= null) {
1598 groupV1
.removeMember(source
);
1599 account
.getGroupStore().updateGroup(groupV1
);
1604 if (groupV1
!= null && !isSync
) {
1605 actions
.add(new SendGroupUpdateAction(source
, groupV1
.getGroupId()));
1610 // Received a group v1 message for a v2 group
1613 if (message
.getGroupContext().get().getGroupV2().isPresent()) {
1614 final SignalServiceGroupV2 groupContext
= message
.getGroupContext().get().getGroupV2().get();
1615 final GroupMasterKey groupMasterKey
= groupContext
.getMasterKey();
1617 getOrMigrateGroup(groupMasterKey
,
1618 groupContext
.getRevision(),
1619 groupContext
.hasSignedGroupChange() ? groupContext
.getSignedGroupChange() : null);
1623 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1624 if (conversationPartnerAddress
!= null && message
.isEndSession()) {
1625 handleEndSession(conversationPartnerAddress
);
1627 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1628 if (message
.getGroupContext().isPresent()) {
1629 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1630 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1631 GroupInfoV1 group
= account
.getGroupStore().getOrCreateGroupV1(GroupId
.v1(groupInfo
.getGroupId()));
1632 if (group
!= null) {
1633 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1634 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1635 account
.getGroupStore().updateGroup(group
);
1638 } else if (message
.getGroupContext().get().getGroupV2().isPresent()) {
1639 // disappearing message timer already stored in the DecryptedGroup
1641 } else if (conversationPartnerAddress
!= null) {
1642 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1643 if (contact
== null) {
1644 contact
= new ContactInfo(conversationPartnerAddress
);
1646 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1647 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1648 account
.getContactStore().updateContact(contact
);
1652 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1653 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1654 if (attachment
.isPointer()) {
1656 retrieveAttachment(attachment
.asPointer());
1657 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1658 System
.err
.println("Failed to retrieve attachment ("
1659 + attachment
.asPointer().getRemoteId()
1666 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1667 final ProfileKey profileKey
;
1669 profileKey
= new ProfileKey(message
.getProfileKey().get());
1670 } catch (InvalidInputException e
) {
1671 throw new AssertionError(e
);
1673 if (source
.matches(account
.getSelfAddress())) {
1674 this.account
.setProfileKey(profileKey
);
1676 this.account
.getProfileStore().storeProfileKey(source
, profileKey
);
1678 if (message
.getPreviews().isPresent()) {
1679 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1680 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1681 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1682 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1684 retrieveAttachment(attachment
);
1685 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1686 System
.err
.println("Failed to retrieve attachment ("
1687 + attachment
.getRemoteId()
1694 if (message
.getQuote().isPresent()) {
1695 final SignalServiceDataMessage
.Quote quote
= message
.getQuote().get();
1697 for (SignalServiceDataMessage
.Quote
.QuotedAttachment quotedAttachment
: quote
.getAttachments()) {
1698 final SignalServiceAttachment attachment
= quotedAttachment
.getThumbnail();
1699 if (attachment
!= null && attachment
.isPointer()) {
1701 retrieveAttachment(attachment
.asPointer());
1702 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1703 System
.err
.println("Failed to retrieve attachment ("
1704 + attachment
.asPointer().getRemoteId()
1711 if (message
.getSticker().isPresent()) {
1712 final SignalServiceDataMessage
.Sticker messageSticker
= message
.getSticker().get();
1713 Sticker sticker
= account
.getStickerStore().getSticker(messageSticker
.getPackId());
1714 if (sticker
== null) {
1715 sticker
= new Sticker(messageSticker
.getPackId(), messageSticker
.getPackKey());
1716 account
.getStickerStore().updateSticker(sticker
);
1722 private GroupInfoV2
getOrMigrateGroup(
1723 final GroupMasterKey groupMasterKey
, final int revision
, final byte[] signedGroupChange
1725 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
1727 GroupIdV2 groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
1728 GroupInfo groupInfo
= account
.getGroupStore().getGroup(groupId
);
1729 final GroupInfoV2 groupInfoV2
;
1730 if (groupInfo
instanceof GroupInfoV1
) {
1731 // Received a v2 group message for a v1 group, we need to locally migrate the group
1732 account
.getGroupStore().deleteGroup(groupInfo
.getGroupId());
1733 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
1734 System
.err
.println("Locally migrated group "
1735 + groupInfo
.getGroupId().toBase64()
1736 + " to group v2, id: "
1737 + groupInfoV2
.getGroupId().toBase64()
1739 } else if (groupInfo
instanceof GroupInfoV2
) {
1740 groupInfoV2
= (GroupInfoV2
) groupInfo
;
1742 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
1745 if (groupInfoV2
.getGroup() == null || groupInfoV2
.getGroup().getRevision() < revision
) {
1746 DecryptedGroup group
= null;
1747 if (signedGroupChange
!= null
1748 && groupInfoV2
.getGroup() != null
1749 && groupInfoV2
.getGroup().getRevision() + 1 == revision
) {
1750 group
= groupHelper
.getUpdatedDecryptedGroup(groupInfoV2
.getGroup(), signedGroupChange
, groupMasterKey
);
1752 if (group
== null) {
1753 group
= groupHelper
.getDecryptedGroup(groupSecretParams
);
1755 if (group
!= null) {
1756 storeProfileKeysFromMembers(group
);
1758 retrieveGroupAvatar(groupId
, groupSecretParams
, group
.getAvatar());
1759 } catch (IOException e
) {
1760 System
.err
.println("Failed to download group avatar, ignoring ...");
1763 groupInfoV2
.setGroup(group
);
1764 account
.getGroupStore().updateGroup(groupInfoV2
);
1770 private void storeProfileKeysFromMembers(final DecryptedGroup group
) {
1771 for (DecryptedMember member
: group
.getMembersList()) {
1772 final SignalServiceAddress address
= resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil
.parseOrThrow(
1773 member
.getUuid().toByteArray()), null));
1775 account
.getProfileStore()
1776 .storeProfileKey(address
, new ProfileKey(member
.getProfileKey().toByteArray()));
1777 } catch (InvalidInputException ignored
) {
1782 private void retryFailedReceivedMessages(
1783 ReceiveMessageHandler handler
, boolean ignoreAttachments
1785 final File cachePath
= new File(getMessageCachePath());
1786 if (!cachePath
.exists()) {
1789 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1790 if (!dir
.isDirectory()) {
1791 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1795 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1796 if (!fileEntry
.isFile()) {
1799 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1801 // Try to delete directory if empty
1806 private void retryFailedReceivedMessage(
1807 final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
1809 SignalServiceEnvelope envelope
;
1811 envelope
= Utils
.loadEnvelope(fileEntry
);
1812 if (envelope
== null) {
1815 } catch (IOException e
) {
1816 e
.printStackTrace();
1819 SignalServiceContent content
= null;
1820 if (!envelope
.isReceipt()) {
1822 content
= decryptMessage(envelope
);
1823 } catch (org
.whispersystems
.libsignal
.UntrustedIdentityException e
) {
1825 } catch (Exception er
) {
1826 // All other errors are not recoverable, so delete the cached message
1828 Files
.delete(fileEntry
.toPath());
1829 } catch (IOException e
) {
1830 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1834 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1835 for (HandleAction action
: actions
) {
1837 action
.execute(this);
1838 } catch (Throwable e
) {
1839 e
.printStackTrace();
1844 handler
.handleMessage(envelope
, content
, null);
1846 Files
.delete(fileEntry
.toPath());
1847 } catch (IOException e
) {
1848 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1852 public void receiveMessages(
1855 boolean returnOnTimeout
,
1856 boolean ignoreAttachments
,
1857 ReceiveMessageHandler handler
1858 ) throws IOException
{
1859 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1861 Set
<HandleAction
> queuedActions
= null;
1863 getOrCreateMessagePipe();
1865 boolean hasCaughtUpWithOldMessages
= false;
1868 SignalServiceEnvelope envelope
;
1869 SignalServiceContent content
= null;
1870 Exception exception
= null;
1871 final long now
= new Date().getTime();
1873 Optional
<SignalServiceEnvelope
> result
= messagePipe
.readOrEmpty(timeout
, unit
, envelope1
-> {
1874 // store message on disk, before acknowledging receipt to the server
1876 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1877 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1878 Utils
.storeEnvelope(envelope1
, cacheFile
);
1879 } catch (IOException e
) {
1880 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: "
1884 if (result
.isPresent()) {
1885 envelope
= result
.get();
1887 // Received indicator that server queue is empty
1888 hasCaughtUpWithOldMessages
= true;
1890 if (queuedActions
!= null) {
1891 for (HandleAction action
: queuedActions
) {
1893 action
.execute(this);
1894 } catch (Throwable e
) {
1895 e
.printStackTrace();
1899 queuedActions
.clear();
1900 queuedActions
= null;
1903 // Continue to wait another timeout for new messages
1906 } catch (TimeoutException e
) {
1907 if (returnOnTimeout
) return;
1909 } catch (InvalidVersionException e
) {
1910 System
.err
.println("Ignoring error: " + e
.getMessage());
1914 if (envelope
.hasSource()) {
1915 // Store uuid if we don't have it already
1916 SignalServiceAddress source
= envelope
.getSourceAddress();
1917 resolveSignalServiceAddress(source
);
1919 if (!envelope
.isReceipt()) {
1921 content
= decryptMessage(envelope
);
1922 } catch (Exception e
) {
1925 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1926 if (hasCaughtUpWithOldMessages
) {
1927 for (HandleAction action
: actions
) {
1929 action
.execute(this);
1930 } catch (Throwable e
) {
1931 e
.printStackTrace();
1935 if (queuedActions
== null) {
1936 queuedActions
= new HashSet
<>();
1938 queuedActions
.addAll(actions
);
1942 if (!isMessageBlocked(envelope
, content
)) {
1943 handler
.handleMessage(envelope
, content
, exception
);
1945 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
1946 File cacheFile
= null;
1948 String source
= envelope
.getSourceE164().isPresent() ? envelope
.getSourceE164().get() : "";
1949 cacheFile
= getMessageCacheFile(source
, now
, envelope
.getTimestamp());
1950 Files
.delete(cacheFile
.toPath());
1951 // Try to delete directory if empty
1952 new File(getMessageCachePath()).delete();
1953 } catch (IOException e
) {
1954 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1960 private boolean isMessageBlocked(
1961 SignalServiceEnvelope envelope
, SignalServiceContent content
1963 SignalServiceAddress source
;
1964 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1965 source
= envelope
.getSourceAddress();
1966 } else if (content
!= null) {
1967 source
= content
.getSender();
1971 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
1972 if (sourceContact
!= null && sourceContact
.blocked
) {
1976 if (content
!= null && content
.getDataMessage().isPresent()) {
1977 SignalServiceDataMessage message
= content
.getDataMessage().get();
1978 if (message
.getGroupContext().isPresent()) {
1979 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1980 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1981 if (groupInfo
.getType() != SignalServiceGroup
.Type
.DELIVER
) {
1985 GroupId groupId
= GroupUtils
.getGroupId(message
.getGroupContext().get());
1986 GroupInfo group
= account
.getGroupStore().getGroup(groupId
);
1987 if (group
!= null && group
.isBlocked()) {
1995 private List
<HandleAction
> handleMessage(
1996 SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
1998 List
<HandleAction
> actions
= new ArrayList
<>();
1999 if (content
!= null) {
2000 final SignalServiceAddress sender
;
2001 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
2002 sender
= envelope
.getSourceAddress();
2004 sender
= content
.getSender();
2006 // Store uuid if we don't have it already
2007 resolveSignalServiceAddress(sender
);
2009 if (content
.getDataMessage().isPresent()) {
2010 SignalServiceDataMessage message
= content
.getDataMessage().get();
2012 if (content
.isNeedsReceipt()) {
2013 actions
.add(new SendReceiptAction(sender
, message
.getTimestamp()));
2016 actions
.addAll(handleSignalServiceDataMessage(message
,
2019 account
.getSelfAddress(),
2020 ignoreAttachments
));
2022 if (content
.getSyncMessage().isPresent()) {
2023 account
.setMultiDevice(true);
2024 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
2025 if (syncMessage
.getSent().isPresent()) {
2026 SentTranscriptMessage message
= syncMessage
.getSent().get();
2027 final SignalServiceAddress destination
= message
.getDestination().orNull();
2028 actions
.addAll(handleSignalServiceDataMessage(message
.getMessage(),
2032 ignoreAttachments
));
2034 if (syncMessage
.getRequest().isPresent()) {
2035 RequestMessage rm
= syncMessage
.getRequest().get();
2036 if (rm
.isContactsRequest()) {
2037 actions
.add(SendSyncContactsAction
.create());
2039 if (rm
.isGroupsRequest()) {
2040 actions
.add(SendSyncGroupsAction
.create());
2042 if (rm
.isBlockedListRequest()) {
2043 actions
.add(SendSyncBlockedListAction
.create());
2045 // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest();
2047 if (syncMessage
.getGroups().isPresent()) {
2048 File tmpFile
= null;
2050 tmpFile
= IOUtils
.createTempFile();
2051 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups()
2053 .asPointer(), tmpFile
)) {
2054 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
2056 while ((g
= s
.read()) != null) {
2057 GroupInfoV1 syncGroup
= account
.getGroupStore()
2058 .getOrCreateGroupV1(GroupId
.v1(g
.getId()));
2059 if (syncGroup
!= null) {
2060 if (g
.getName().isPresent()) {
2061 syncGroup
.name
= g
.getName().get();
2063 syncGroup
.addMembers(g
.getMembers()
2065 .map(this::resolveSignalServiceAddress
)
2066 .collect(Collectors
.toSet()));
2067 if (!g
.isActive()) {
2068 syncGroup
.removeMember(account
.getSelfAddress());
2070 // Add ourself to the member set as it's marked as active
2071 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
2073 syncGroup
.blocked
= g
.isBlocked();
2074 if (g
.getColor().isPresent()) {
2075 syncGroup
.color
= g
.getColor().get();
2078 if (g
.getAvatar().isPresent()) {
2079 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.getGroupId());
2081 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
2082 syncGroup
.archived
= g
.isArchived();
2083 account
.getGroupStore().updateGroup(syncGroup
);
2087 } catch (Exception e
) {
2088 e
.printStackTrace();
2090 if (tmpFile
!= null) {
2092 Files
.delete(tmpFile
.toPath());
2093 } catch (IOException e
) {
2094 System
.err
.println("Failed to delete received groups temp file “"
2102 if (syncMessage
.getBlockedList().isPresent()) {
2103 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
2104 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
2105 setContactBlocked(resolveSignalServiceAddress(address
), true);
2107 for (GroupId groupId
: blockedListMessage
.getGroupIds()
2109 .map(GroupId
::unknownVersion
)
2110 .collect(Collectors
.toSet())) {
2112 setGroupBlocked(groupId
, true);
2113 } catch (GroupNotFoundException e
) {
2114 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: "
2115 + groupId
.toBase64());
2119 if (syncMessage
.getContacts().isPresent()) {
2120 File tmpFile
= null;
2122 tmpFile
= IOUtils
.createTempFile();
2123 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
2124 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream()
2125 .asPointer(), tmpFile
)) {
2126 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
2127 if (contactsMessage
.isComplete()) {
2128 account
.getContactStore().clear();
2131 while ((c
= s
.read()) != null) {
2132 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
2133 account
.setProfileKey(c
.getProfileKey().get());
2135 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
2136 ContactInfo contact
= account
.getContactStore().getContact(address
);
2137 if (contact
== null) {
2138 contact
= new ContactInfo(address
);
2140 if (c
.getName().isPresent()) {
2141 contact
.name
= c
.getName().get();
2143 if (c
.getColor().isPresent()) {
2144 contact
.color
= c
.getColor().get();
2146 if (c
.getProfileKey().isPresent()) {
2147 account
.getProfileStore().storeProfileKey(address
, c
.getProfileKey().get());
2149 if (c
.getVerified().isPresent()) {
2150 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
2151 account
.getSignalProtocolStore()
2152 .setIdentityTrustLevel(verifiedMessage
.getDestination(),
2153 verifiedMessage
.getIdentityKey(),
2154 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
2156 if (c
.getExpirationTimer().isPresent()) {
2157 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
2159 contact
.blocked
= c
.isBlocked();
2160 contact
.inboxPosition
= c
.getInboxPosition().orNull();
2161 contact
.archived
= c
.isArchived();
2162 account
.getContactStore().updateContact(contact
);
2164 if (c
.getAvatar().isPresent()) {
2165 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
2169 } catch (Exception e
) {
2170 e
.printStackTrace();
2172 if (tmpFile
!= null) {
2174 Files
.delete(tmpFile
.toPath());
2175 } catch (IOException e
) {
2176 System
.err
.println("Failed to delete received contacts temp file “"
2184 if (syncMessage
.getVerified().isPresent()) {
2185 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
2186 account
.getSignalProtocolStore()
2187 .setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()),
2188 verifiedMessage
.getIdentityKey(),
2189 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
2191 if (syncMessage
.getStickerPackOperations().isPresent()) {
2192 final List
<StickerPackOperationMessage
> stickerPackOperationMessages
= syncMessage
.getStickerPackOperations()
2194 for (StickerPackOperationMessage m
: stickerPackOperationMessages
) {
2195 if (!m
.getPackId().isPresent()) {
2198 Sticker sticker
= account
.getStickerStore().getSticker(m
.getPackId().get());
2199 if (sticker
== null) {
2200 if (!m
.getPackKey().isPresent()) {
2203 sticker
= new Sticker(m
.getPackId().get(), m
.getPackKey().get());
2205 sticker
.setInstalled(!m
.getType().isPresent()
2206 || m
.getType().get() == StickerPackOperationMessage
.Type
.INSTALL
);
2207 account
.getStickerStore().updateSticker(sticker
);
2210 if (syncMessage
.getConfiguration().isPresent()) {
2218 private File
getContactAvatarFile(String number
) {
2219 return new File(pathConfig
.getAvatarsPath(), "contact-" + number
);
2222 private File
retrieveContactAvatarAttachment(
2223 SignalServiceAttachment attachment
, String number
2224 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2225 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2226 if (attachment
.isPointer()) {
2227 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
2228 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
2230 SignalServiceAttachmentStream stream
= attachment
.asStream();
2231 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
2235 private File
getGroupAvatarFile(GroupId groupId
) {
2236 return new File(pathConfig
.getAvatarsPath(), "group-" + groupId
.toBase64().replace("/", "_"));
2239 private File
retrieveGroupAvatarAttachment(
2240 SignalServiceAttachment attachment
, GroupId groupId
2241 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2242 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2243 if (attachment
.isPointer()) {
2244 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
2245 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
2247 SignalServiceAttachmentStream stream
= attachment
.asStream();
2248 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
2252 private File
retrieveGroupAvatar(
2253 GroupId groupId
, GroupSecretParams groupSecretParams
, String cdnKey
2254 ) throws IOException
{
2255 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2256 SignalServiceMessageReceiver receiver
= getOrCreateMessageReceiver();
2257 File outputFile
= getGroupAvatarFile(groupId
);
2258 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
2260 File tmpFile
= IOUtils
.createTempFile();
2261 tmpFile
.deleteOnExit();
2262 try (InputStream input
= receiver
.retrieveGroupsV2ProfileAvatar(cdnKey
,
2264 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
2265 byte[] encryptedData
= IOUtils
.readFully(input
);
2267 byte[] decryptedData
= groupOperations
.decryptAvatar(encryptedData
);
2268 try (OutputStream output
= new FileOutputStream(outputFile
)) {
2269 output
.write(decryptedData
);
2273 Files
.delete(tmpFile
.toPath());
2274 } catch (IOException e
) {
2275 System
.err
.println("Failed to delete received avatar temp file “" + tmpFile
+ "”: " + e
.getMessage());
2281 private File
getProfileAvatarFile(SignalServiceAddress address
) {
2282 return new File(pathConfig
.getAvatarsPath(), "profile-" + address
.getLegacyIdentifier());
2285 private File
retrieveProfileAvatar(
2286 SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
2287 ) throws IOException
{
2288 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2289 SignalServiceMessageReceiver receiver
= getOrCreateMessageReceiver();
2290 File outputFile
= getProfileAvatarFile(address
);
2292 File tmpFile
= IOUtils
.createTempFile();
2293 try (InputStream input
= receiver
.retrieveProfileAvatar(avatarPath
,
2296 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
2297 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
2298 IOUtils
.copyStreamToFile(input
, outputFile
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
2301 Files
.delete(tmpFile
.toPath());
2302 } catch (IOException e
) {
2303 System
.err
.println("Failed to delete received avatar temp file “" + tmpFile
+ "”: " + e
.getMessage());
2309 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
2310 return new File(pathConfig
.getAttachmentsPath(), attachmentId
.toString());
2313 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2314 IOUtils
.createPrivateDirectories(pathConfig
.getAttachmentsPath());
2315 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
2318 private File
retrieveAttachment(
2319 SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
2320 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2321 if (storePreview
&& pointer
.getPreview().isPresent()) {
2322 File previewFile
= new File(outputFile
+ ".preview");
2323 try (OutputStream output
= new FileOutputStream(previewFile
)) {
2324 byte[] preview
= pointer
.getPreview().get();
2325 output
.write(preview
, 0, preview
.length
);
2326 } catch (FileNotFoundException e
) {
2327 e
.printStackTrace();
2332 final SignalServiceMessageReceiver messageReceiver
= getOrCreateMessageReceiver();
2334 File tmpFile
= IOUtils
.createTempFile();
2335 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
,
2337 ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
2338 IOUtils
.copyStreamToFile(input
, outputFile
);
2341 Files
.delete(tmpFile
.toPath());
2342 } catch (IOException e
) {
2343 System
.err
.println("Failed to delete received attachment temp file “"
2352 private InputStream
retrieveAttachmentAsStream(
2353 SignalServiceAttachmentPointer pointer
, File tmpFile
2354 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2355 final SignalServiceMessageReceiver messageReceiver
= getOrCreateMessageReceiver();
2356 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
2359 void sendGroups() throws IOException
, UntrustedIdentityException
{
2360 File groupsFile
= IOUtils
.createTempFile();
2363 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
2364 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
2365 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
2366 if (record instanceof GroupInfoV1
) {
2367 GroupInfoV1 groupInfo
= (GroupInfoV1
) record;
2368 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
2369 Optional
.fromNullable(groupInfo
.name
),
2370 new ArrayList
<>(groupInfo
.getMembers()),
2371 createGroupAvatarAttachment(groupInfo
.getGroupId()),
2372 groupInfo
.isMember(account
.getSelfAddress()),
2373 Optional
.of(groupInfo
.messageExpirationTime
),
2374 Optional
.fromNullable(groupInfo
.color
),
2376 Optional
.fromNullable(groupInfo
.inboxPosition
),
2377 groupInfo
.archived
));
2382 if (groupsFile
.exists() && groupsFile
.length() > 0) {
2383 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
2384 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
2385 .withStream(groupsFileStream
)
2386 .withContentType("application/octet-stream")
2387 .withLength(groupsFile
.length())
2390 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
2395 Files
.delete(groupsFile
.toPath());
2396 } catch (IOException e
) {
2397 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
2402 public void sendContacts() throws IOException
, UntrustedIdentityException
{
2403 File contactsFile
= IOUtils
.createTempFile();
2406 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
2407 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
2408 for (ContactInfo
record : account
.getContactStore().getContacts()) {
2409 VerifiedMessage verifiedMessage
= null;
2410 JsonIdentityKeyStore
.Identity currentIdentity
= account
.getSignalProtocolStore()
2411 .getIdentity(record.getAddress());
2412 if (currentIdentity
!= null) {
2413 verifiedMessage
= new VerifiedMessage(record.getAddress(),
2414 currentIdentity
.getIdentityKey(),
2415 currentIdentity
.getTrustLevel().toVerifiedState(),
2416 currentIdentity
.getDateAdded().getTime());
2419 ProfileKey profileKey
= account
.getProfileStore().getProfileKey(record.getAddress());
2420 out
.write(new DeviceContact(record.getAddress(),
2421 Optional
.fromNullable(record.name
),
2422 createContactAvatarAttachment(record.number
),
2423 Optional
.fromNullable(record.color
),
2424 Optional
.fromNullable(verifiedMessage
),
2425 Optional
.fromNullable(profileKey
),
2427 Optional
.of(record.messageExpirationTime
),
2428 Optional
.fromNullable(record.inboxPosition
),
2432 if (account
.getProfileKey() != null) {
2433 // Send our own profile key as well
2434 out
.write(new DeviceContact(account
.getSelfAddress(),
2439 Optional
.of(account
.getProfileKey()),
2447 if (contactsFile
.exists() && contactsFile
.length() > 0) {
2448 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
2449 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
2450 .withStream(contactsFileStream
)
2451 .withContentType("application/octet-stream")
2452 .withLength(contactsFile
.length())
2455 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
2460 Files
.delete(contactsFile
.toPath());
2461 } catch (IOException e
) {
2462 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
2467 void sendBlockedList() throws IOException
, UntrustedIdentityException
{
2468 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
2469 for (ContactInfo
record : account
.getContactStore().getContacts()) {
2470 if (record.blocked
) {
2471 addresses
.add(record.getAddress());
2474 List
<byte[]> groupIds
= new ArrayList
<>();
2475 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
2476 if (record.isBlocked()) {
2477 groupIds
.add(record.getGroupId().serialize());
2480 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
2483 private void sendVerifiedMessage(
2484 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
2485 ) throws IOException
, UntrustedIdentityException
{
2486 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
,
2488 trustLevel
.toVerifiedState(),
2489 System
.currentTimeMillis());
2490 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
2493 public List
<ContactInfo
> getContacts() {
2494 return account
.getContactStore().getContacts();
2497 public ContactInfo
getContact(String number
) {
2498 return account
.getContactStore().getContact(Util
.getSignalServiceAddressFromIdentifier(number
));
2501 public GroupInfo
getGroup(GroupId groupId
) {
2502 return account
.getGroupStore().getGroup(groupId
);
2505 public List
<JsonIdentityKeyStore
.Identity
> getIdentities() {
2506 return account
.getSignalProtocolStore().getIdentities();
2509 public List
<JsonIdentityKeyStore
.Identity
> getIdentities(String number
) throws InvalidNumberException
{
2510 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
2514 * Trust this the identity with this fingerprint
2516 * @param name username of the identity
2517 * @param fingerprint Fingerprint
2519 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
2520 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2521 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2525 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2526 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
2530 account
.getSignalProtocolStore()
2531 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2533 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2534 } catch (IOException
| UntrustedIdentityException e
) {
2535 e
.printStackTrace();
2544 * Trust this the identity with this safety number
2546 * @param name username of the identity
2547 * @param safetyNumber Safety number
2549 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
2550 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2551 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2555 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2556 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
2560 account
.getSignalProtocolStore()
2561 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2563 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2564 } catch (IOException
| UntrustedIdentityException e
) {
2565 e
.printStackTrace();
2574 * Trust all keys of this identity without verification
2576 * @param name username of the identity
2578 public boolean trustIdentityAllKeys(String name
) {
2579 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
2580 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2584 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2585 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2586 account
.getSignalProtocolStore()
2587 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2589 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2590 } catch (IOException
| UntrustedIdentityException e
) {
2591 e
.printStackTrace();
2599 public String
computeSafetyNumber(
2600 SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
2602 return Utils
.computeSafetyNumber(account
.getSelfAddress(),
2603 getIdentityKeyPair().getPublicKey(),
2608 void saveAccount() {
2612 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2613 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
)
2615 : Util
.canonicalizeNumber(identifier
, account
.getUsername());
2616 return resolveSignalServiceAddress(canonicalizedNumber
);
2619 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2620 SignalServiceAddress address
= Util
.getSignalServiceAddressFromIdentifier(identifier
);
2622 return resolveSignalServiceAddress(address
);
2625 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2626 if (address
.matches(account
.getSelfAddress())) {
2627 return account
.getSelfAddress();
2630 return account
.getRecipientStore().resolveServiceAddress(address
);
2634 public void close() throws IOException
{
2635 if (messagePipe
!= null) {
2636 messagePipe
.shutdown();
2640 if (unidentifiedMessagePipe
!= null) {
2641 unidentifiedMessagePipe
.shutdown();
2642 unidentifiedMessagePipe
= null;
2648 public interface ReceiveMessageHandler
{
2650 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);