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()
821 .map(this::resolveSignalServiceAddress
)
822 .collect(Collectors
.toSet()));
823 if (newMembers
.size() > 0) {
824 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.updateGroupV2(groupInfoV2
,
826 result
= sendUpdateGroupMessage(groupInfoV2
,
827 groupGroupChangePair
.first(),
828 groupGroupChangePair
.second());
831 if (result
== null || name
!= null || avatarFile
!= null) {
832 Pair
<DecryptedGroup
, GroupChange
> groupGroupChangePair
= groupHelper
.updateGroupV2(groupInfoV2
,
835 result
= sendUpdateGroupMessage(groupInfoV2
,
836 groupGroupChangePair
.first(),
837 groupGroupChangePair
.second());
840 return new Pair
<>(group
.getGroupId(), result
.second());
842 GroupInfoV1 gv1
= (GroupInfoV1
) group
;
843 updateGroupV1(gv1
, name
, members
, avatarFile
);
844 messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
849 account
.getGroupStore().updateGroup(g
);
851 final Pair
<Long
, List
<SendMessageResult
>> result
= sendMessage(messageBuilder
,
852 g
.getMembersIncludingPendingWithout(account
.getSelfAddress()));
853 return new Pair
<>(g
.getGroupId(), result
.second());
856 public Pair
<GroupId
, List
<SendMessageResult
>> joinGroup(
857 GroupInviteLinkUrl inviteLinkUrl
858 ) throws IOException
, GroupLinkNotActiveException
{
859 return sendJoinGroupMessage(inviteLinkUrl
);
862 private Pair
<GroupId
, List
<SendMessageResult
>> sendJoinGroupMessage(
863 GroupInviteLinkUrl inviteLinkUrl
864 ) throws IOException
, GroupLinkNotActiveException
{
865 final DecryptedGroupJoinInfo groupJoinInfo
= groupHelper
.getDecryptedGroupJoinInfo(inviteLinkUrl
.getGroupMasterKey(),
866 inviteLinkUrl
.getPassword());
867 final GroupChange groupChange
= groupHelper
.joinGroup(inviteLinkUrl
.getGroupMasterKey(),
868 inviteLinkUrl
.getPassword(),
870 final GroupInfoV2 group
= getOrMigrateGroup(inviteLinkUrl
.getGroupMasterKey(),
871 groupJoinInfo
.getRevision() + 1,
872 groupChange
.toByteArray());
874 if (group
.getGroup() == null) {
875 // Only requested member, can't send update to group members
876 return new Pair
<>(group
.getGroupId(), List
.of());
879 final Pair
<Long
, List
<SendMessageResult
>> result
= sendUpdateGroupMessage(group
, group
.getGroup(), groupChange
);
881 return new Pair
<>(group
.getGroupId(), result
.second());
884 private Pair
<Long
, List
<SendMessageResult
>> sendUpdateGroupMessage(
885 GroupInfoV2 group
, DecryptedGroup newDecryptedGroup
, GroupChange groupChange
886 ) throws IOException
{
887 group
.setGroup(newDecryptedGroup
);
888 final SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(group
,
889 groupChange
.toByteArray());
890 account
.getGroupStore().updateGroup(group
);
891 return sendMessage(messageBuilder
, group
.getMembersIncludingPendingWithout(account
.getSelfAddress()));
894 private void updateGroupV1(
897 final Collection
<SignalServiceAddress
> members
,
898 final String avatarFile
899 ) throws IOException
{
904 if (members
!= null) {
905 final Set
<String
> newE164Members
= new HashSet
<>();
906 for (SignalServiceAddress member
: members
) {
907 if (g
.isMember(member
) || !member
.getNumber().isPresent()) {
910 newE164Members
.add(member
.getNumber().get());
913 final List
<ContactTokenDetails
> contacts
= accountManager
.getContacts(newE164Members
);
914 if (contacts
.size() != newE164Members
.size()) {
915 // Some of the new members are not registered on Signal
916 for (ContactTokenDetails contact
: contacts
) {
917 newE164Members
.remove(contact
.getNumber());
919 throw new IOException("Failed to add members "
920 + Util
.join(", ", newE164Members
)
921 + " to group: Not registered on Signal");
924 g
.addMembers(members
);
927 if (avatarFile
!= null) {
928 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
929 File aFile
= getGroupAvatarFile(g
.getGroupId());
930 Files
.copy(Paths
.get(avatarFile
), aFile
.toPath(), StandardCopyOption
.REPLACE_EXISTING
);
934 Pair
<Long
, List
<SendMessageResult
>> sendUpdateGroupMessage(
935 GroupIdV1 groupId
, SignalServiceAddress recipient
936 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
938 GroupInfo group
= getGroupForSending(groupId
);
939 if (!(group
instanceof GroupInfoV1
)) {
940 throw new RuntimeException("Received an invalid group request for a v2 group!");
942 g
= (GroupInfoV1
) group
;
944 if (!g
.isMember(recipient
)) {
945 throw new NotAGroupMemberException(groupId
, g
.name
);
948 SignalServiceDataMessage
.Builder messageBuilder
= getGroupUpdateMessageBuilder(g
);
950 // Send group message only to the recipient who requested it
951 return sendMessage(messageBuilder
, Collections
.singleton(recipient
));
954 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV1 g
) throws AttachmentInvalidException
{
955 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
956 .withId(g
.getGroupId().serialize())
958 .withMembers(new ArrayList
<>(g
.getMembers()));
960 File aFile
= getGroupAvatarFile(g
.getGroupId());
961 if (aFile
.exists()) {
963 group
.withAvatar(Utils
.createAttachment(aFile
));
964 } catch (IOException e
) {
965 throw new AttachmentInvalidException(aFile
.toString(), e
);
969 return SignalServiceDataMessage
.newBuilder()
970 .asGroupMessage(group
.build())
971 .withExpiration(g
.getMessageExpirationTime());
974 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV2 g
, byte[] signedGroupChange
) {
975 SignalServiceGroupV2
.Builder group
= SignalServiceGroupV2
.newBuilder(g
.getMasterKey())
976 .withRevision(g
.getGroup().getRevision())
977 .withSignedGroupChange(signedGroupChange
);
978 return SignalServiceDataMessage
.newBuilder()
979 .asGroupMessage(group
.build())
980 .withExpiration(g
.getMessageExpirationTime());
983 Pair
<Long
, List
<SendMessageResult
>> sendGroupInfoRequest(
984 GroupIdV1 groupId
, SignalServiceAddress recipient
985 ) throws IOException
{
986 SignalServiceGroup
.Builder group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
)
987 .withId(groupId
.serialize());
989 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
990 .asGroupMessage(group
.build());
992 // Send group info request message to the recipient who sent us a message with this groupId
993 return sendMessage(messageBuilder
, Collections
.singleton(recipient
));
997 SignalServiceAddress remoteAddress
, long messageId
998 ) throws IOException
, UntrustedIdentityException
{
999 SignalServiceReceiptMessage receiptMessage
= new SignalServiceReceiptMessage(SignalServiceReceiptMessage
.Type
.DELIVERY
,
1000 Collections
.singletonList(messageId
),
1001 System
.currentTimeMillis());
1003 createMessageSender().sendReceipt(remoteAddress
,
1004 unidentifiedAccessHelper
.getAccessFor(remoteAddress
),
1008 public Pair
<Long
, List
<SendMessageResult
>> sendMessage(
1009 String messageText
, List
<String
> attachments
, List
<String
> recipients
1010 ) throws IOException
, AttachmentInvalidException
, InvalidNumberException
{
1011 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1012 .withBody(messageText
);
1013 if (attachments
!= null) {
1014 List
<SignalServiceAttachment
> attachmentStreams
= Utils
.getSignalServiceAttachments(attachments
);
1016 // Upload attachments here, so we only upload once even for multiple recipients
1017 SignalServiceMessageSender messageSender
= createMessageSender();
1018 List
<SignalServiceAttachment
> attachmentPointers
= new ArrayList
<>(attachmentStreams
.size());
1019 for (SignalServiceAttachment attachment
: attachmentStreams
) {
1020 if (attachment
.isStream()) {
1021 attachmentPointers
.add(messageSender
.uploadAttachment(attachment
.asStream()));
1022 } else if (attachment
.isPointer()) {
1023 attachmentPointers
.add(attachment
.asPointer());
1027 messageBuilder
.withAttachments(attachmentPointers
);
1029 return sendMessage(messageBuilder
, getSignalServiceAddresses(recipients
));
1032 public Pair
<Long
, List
<SendMessageResult
>> sendMessageReaction(
1033 String emoji
, boolean remove
, String targetAuthor
, long targetSentTimestamp
, List
<String
> recipients
1034 ) throws IOException
, InvalidNumberException
{
1035 SignalServiceDataMessage
.Reaction reaction
= new SignalServiceDataMessage
.Reaction(emoji
,
1037 canonicalizeAndResolveSignalServiceAddress(targetAuthor
),
1038 targetSentTimestamp
);
1039 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1040 .withReaction(reaction
);
1041 return sendMessage(messageBuilder
, getSignalServiceAddresses(recipients
));
1044 public Pair
<Long
, List
<SendMessageResult
>> sendEndSessionMessage(List
<String
> recipients
) throws IOException
, InvalidNumberException
{
1045 SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder().asEndSessionMessage();
1047 final Collection
<SignalServiceAddress
> signalServiceAddresses
= getSignalServiceAddresses(recipients
);
1049 return sendMessage(messageBuilder
, signalServiceAddresses
);
1050 } catch (Exception e
) {
1051 for (SignalServiceAddress address
: signalServiceAddresses
) {
1052 handleEndSession(address
);
1059 public String
getContactName(String number
) throws InvalidNumberException
{
1060 ContactInfo contact
= account
.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number
));
1061 if (contact
== null) {
1064 return contact
.name
;
1068 public void setContactName(String number
, String name
) throws InvalidNumberException
{
1069 final SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
1070 ContactInfo contact
= account
.getContactStore().getContact(address
);
1071 if (contact
== null) {
1072 contact
= new ContactInfo(address
);
1074 contact
.name
= name
;
1075 account
.getContactStore().updateContact(contact
);
1079 public void setContactBlocked(String number
, boolean blocked
) throws InvalidNumberException
{
1080 setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number
), blocked
);
1083 private void setContactBlocked(SignalServiceAddress address
, boolean blocked
) {
1084 ContactInfo contact
= account
.getContactStore().getContact(address
);
1085 if (contact
== null) {
1086 contact
= new ContactInfo(address
);
1088 contact
.blocked
= blocked
;
1089 account
.getContactStore().updateContact(contact
);
1093 public void setGroupBlocked(final GroupId groupId
, final boolean blocked
) throws GroupNotFoundException
{
1094 GroupInfo group
= getGroup(groupId
);
1095 if (group
== null) {
1096 throw new GroupNotFoundException(groupId
);
1099 group
.setBlocked(blocked
);
1100 account
.getGroupStore().updateGroup(group
);
1104 public Pair
<GroupId
, List
<SendMessageResult
>> updateGroup(
1105 GroupId groupId
, String name
, List
<String
> members
, String avatar
1106 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, InvalidNumberException
, NotAGroupMemberException
{
1107 return sendUpdateGroupMessage(groupId
,
1109 members
== null ?
null : getSignalServiceAddresses(members
),
1114 * Change the expiration timer for a contact
1116 public void setExpirationTimer(SignalServiceAddress address
, int messageExpirationTimer
) throws IOException
{
1117 ContactInfo contact
= account
.getContactStore().getContact(address
);
1118 contact
.messageExpirationTime
= messageExpirationTimer
;
1119 account
.getContactStore().updateContact(contact
);
1120 sendExpirationTimerUpdate(address
);
1124 private void sendExpirationTimerUpdate(SignalServiceAddress address
) throws IOException
{
1125 final SignalServiceDataMessage
.Builder messageBuilder
= SignalServiceDataMessage
.newBuilder()
1126 .asExpirationUpdate();
1127 sendMessage(messageBuilder
, Collections
.singleton(address
));
1131 * Change the expiration timer for a contact
1133 public void setExpirationTimer(
1134 String number
, int messageExpirationTimer
1135 ) throws IOException
, InvalidNumberException
{
1136 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(number
);
1137 setExpirationTimer(address
, messageExpirationTimer
);
1141 * Change the expiration timer for a group
1143 public void setExpirationTimer(GroupId groupId
, int messageExpirationTimer
) {
1144 GroupInfo g
= account
.getGroupStore().getGroup(groupId
);
1145 if (g
instanceof GroupInfoV1
) {
1146 GroupInfoV1 groupInfoV1
= (GroupInfoV1
) g
;
1147 groupInfoV1
.messageExpirationTime
= messageExpirationTimer
;
1148 account
.getGroupStore().updateGroup(groupInfoV1
);
1150 throw new RuntimeException("TODO Not implemented!");
1155 * Upload the sticker pack from path.
1157 * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
1158 * @return if successful, returns the URL to install the sticker pack in the signal app
1160 public String
uploadStickerPack(String path
) throws IOException
, StickerPackInvalidException
{
1161 SignalServiceStickerManifestUpload manifest
= getSignalServiceStickerManifestUpload(path
);
1163 SignalServiceMessageSender messageSender
= createMessageSender();
1165 byte[] packKey
= KeyUtils
.createStickerUploadKey();
1166 String packId
= messageSender
.uploadStickerManifest(manifest
, packKey
);
1168 Sticker sticker
= new Sticker(Hex
.fromStringCondensed(packId
), packKey
);
1169 account
.getStickerStore().updateSticker(sticker
);
1173 return new URI("https",
1176 "pack_id=" + URLEncoder
.encode(packId
, StandardCharsets
.UTF_8
) + "&pack_key=" + URLEncoder
.encode(
1177 Hex
.toStringCondensed(packKey
),
1178 StandardCharsets
.UTF_8
)).toString();
1179 } catch (URISyntaxException e
) {
1180 throw new AssertionError(e
);
1184 private SignalServiceStickerManifestUpload
getSignalServiceStickerManifestUpload(
1186 ) throws IOException
, StickerPackInvalidException
{
1188 String rootPath
= null;
1190 final File file
= new File(path
);
1191 if (file
.getName().endsWith(".zip")) {
1192 zip
= new ZipFile(file
);
1193 } else if (file
.getName().equals("manifest.json")) {
1194 rootPath
= file
.getParent();
1196 throw new StickerPackInvalidException("Could not find manifest.json");
1199 JsonStickerPack pack
= parseStickerPack(rootPath
, zip
);
1201 if (pack
.stickers
== null) {
1202 throw new StickerPackInvalidException("Must set a 'stickers' field.");
1205 if (pack
.stickers
.isEmpty()) {
1206 throw new StickerPackInvalidException("Must include stickers.");
1209 List
<StickerInfo
> stickers
= new ArrayList
<>(pack
.stickers
.size());
1210 for (JsonStickerPack
.JsonSticker sticker
: pack
.stickers
) {
1211 if (sticker
.file
== null) {
1212 throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
1215 Pair
<InputStream
, Long
> data
;
1217 data
= getInputStreamAndLength(rootPath
, zip
, sticker
.file
);
1218 } catch (IOException ignored
) {
1219 throw new StickerPackInvalidException("Could not find find " + sticker
.file
);
1222 String contentType
= Utils
.getFileMimeType(new File(sticker
.file
), null);
1223 StickerInfo stickerInfo
= new StickerInfo(data
.first(),
1225 Optional
.fromNullable(sticker
.emoji
).or(""),
1227 stickers
.add(stickerInfo
);
1230 StickerInfo cover
= null;
1231 if (pack
.cover
!= null) {
1232 if (pack
.cover
.file
== null) {
1233 throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
1236 Pair
<InputStream
, Long
> data
;
1238 data
= getInputStreamAndLength(rootPath
, zip
, pack
.cover
.file
);
1239 } catch (IOException ignored
) {
1240 throw new StickerPackInvalidException("Could not find find " + pack
.cover
.file
);
1243 String contentType
= Utils
.getFileMimeType(new File(pack
.cover
.file
), null);
1244 cover
= new StickerInfo(data
.first(),
1246 Optional
.fromNullable(pack
.cover
.emoji
).or(""),
1250 return new SignalServiceStickerManifestUpload(pack
.title
, pack
.author
, cover
, stickers
);
1253 private static JsonStickerPack
parseStickerPack(String rootPath
, ZipFile zip
) throws IOException
{
1254 InputStream inputStream
;
1256 inputStream
= zip
.getInputStream(zip
.getEntry("manifest.json"));
1258 inputStream
= new FileInputStream((new File(rootPath
, "manifest.json")));
1260 return new ObjectMapper().readValue(inputStream
, JsonStickerPack
.class);
1263 private static Pair
<InputStream
, Long
> getInputStreamAndLength(
1264 final String rootPath
, final ZipFile zip
, final String subfile
1265 ) throws IOException
{
1267 final ZipEntry entry
= zip
.getEntry(subfile
);
1268 return new Pair
<>(zip
.getInputStream(entry
), entry
.getSize());
1270 final File file
= new File(rootPath
, subfile
);
1271 return new Pair
<>(new FileInputStream(file
), file
.length());
1275 void requestSyncGroups() throws IOException
{
1276 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1277 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.GROUPS
)
1279 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1281 sendSyncMessage(message
);
1282 } catch (UntrustedIdentityException e
) {
1283 e
.printStackTrace();
1287 void requestSyncContacts() throws IOException
{
1288 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1289 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONTACTS
)
1291 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1293 sendSyncMessage(message
);
1294 } catch (UntrustedIdentityException e
) {
1295 e
.printStackTrace();
1299 void requestSyncBlocked() throws IOException
{
1300 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1301 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.BLOCKED
)
1303 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1305 sendSyncMessage(message
);
1306 } catch (UntrustedIdentityException e
) {
1307 e
.printStackTrace();
1311 void requestSyncConfiguration() throws IOException
{
1312 SignalServiceProtos
.SyncMessage
.Request r
= SignalServiceProtos
.SyncMessage
.Request
.newBuilder()
1313 .setType(SignalServiceProtos
.SyncMessage
.Request
.Type
.CONFIGURATION
)
1315 SignalServiceSyncMessage message
= SignalServiceSyncMessage
.forRequest(new RequestMessage(r
));
1317 sendSyncMessage(message
);
1318 } catch (UntrustedIdentityException e
) {
1319 e
.printStackTrace();
1323 private byte[] getSenderCertificate() {
1324 // TODO support UUID capable sender certificates
1325 // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
1328 certificate
= accountManager
.getSenderCertificate();
1329 } catch (IOException e
) {
1330 System
.err
.println("Failed to get sender certificate: " + e
);
1333 // TODO cache for a day
1337 private void sendSyncMessage(SignalServiceSyncMessage message
) throws IOException
, UntrustedIdentityException
{
1338 SignalServiceMessageSender messageSender
= createMessageSender();
1340 messageSender
.sendMessage(message
, unidentifiedAccessHelper
.getAccessForSync());
1341 } catch (UntrustedIdentityException e
) {
1342 account
.getSignalProtocolStore()
1343 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1345 TrustLevel
.UNTRUSTED
);
1350 private Collection
<SignalServiceAddress
> getSignalServiceAddresses(Collection
<String
> numbers
) throws InvalidNumberException
{
1351 final Set
<SignalServiceAddress
> signalServiceAddresses
= new HashSet
<>(numbers
.size());
1352 final Set
<SignalServiceAddress
> missingUuids
= new HashSet
<>();
1354 for (String number
: numbers
) {
1355 final SignalServiceAddress resolvedAddress
= canonicalizeAndResolveSignalServiceAddress(number
);
1356 if (resolvedAddress
.getUuid().isPresent()) {
1357 signalServiceAddresses
.add(resolvedAddress
);
1359 missingUuids
.add(resolvedAddress
);
1363 Map
<String
, UUID
> registeredUsers
;
1365 registeredUsers
= accountManager
.getRegisteredUsers(getIasKeyStore(),
1366 missingUuids
.stream().map(a
-> a
.getNumber().get()).collect(Collectors
.toSet()),
1368 } catch (IOException
| Quote
.InvalidQuoteFormatException
| UnauthenticatedQuoteException
| SignatureException
| UnauthenticatedResponseException e
) {
1369 System
.err
.println("Failed to resolve uuids from server: " + e
.getMessage());
1370 registeredUsers
= new HashMap
<>();
1373 for (SignalServiceAddress address
: missingUuids
) {
1374 final String number
= address
.getNumber().get();
1375 if (registeredUsers
.containsKey(number
)) {
1376 final SignalServiceAddress newAddress
= resolveSignalServiceAddress(new SignalServiceAddress(
1377 registeredUsers
.get(number
),
1379 signalServiceAddresses
.add(newAddress
);
1381 signalServiceAddresses
.add(address
);
1385 return signalServiceAddresses
;
1388 private Pair
<Long
, List
<SendMessageResult
>> sendMessage(
1389 SignalServiceDataMessage
.Builder messageBuilder
, Collection
<SignalServiceAddress
> recipients
1390 ) throws IOException
{
1391 recipients
= recipients
.stream().map(this::resolveSignalServiceAddress
).collect(Collectors
.toSet());
1392 final long timestamp
= System
.currentTimeMillis();
1393 messageBuilder
.withTimestamp(timestamp
);
1394 getOrCreateMessagePipe();
1395 getOrCreateUnidentifiedMessagePipe();
1396 SignalServiceDataMessage message
= null;
1398 message
= messageBuilder
.build();
1399 if (message
.getGroupContext().isPresent()) {
1401 SignalServiceMessageSender messageSender
= createMessageSender();
1402 final boolean isRecipientUpdate
= false;
1403 List
<SendMessageResult
> result
= messageSender
.sendMessage(new ArrayList
<>(recipients
),
1404 unidentifiedAccessHelper
.getAccessFor(recipients
),
1407 for (SendMessageResult r
: result
) {
1408 if (r
.getIdentityFailure() != null) {
1409 account
.getSignalProtocolStore()
1410 .saveIdentity(r
.getAddress(),
1411 r
.getIdentityFailure().getIdentityKey(),
1412 TrustLevel
.UNTRUSTED
);
1415 return new Pair
<>(timestamp
, result
);
1416 } catch (UntrustedIdentityException e
) {
1417 account
.getSignalProtocolStore()
1418 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1420 TrustLevel
.UNTRUSTED
);
1421 return new Pair
<>(timestamp
, Collections
.emptyList());
1424 // Send to all individually, so sync messages are sent correctly
1425 List
<SendMessageResult
> results
= new ArrayList
<>(recipients
.size());
1426 for (SignalServiceAddress address
: recipients
) {
1427 ContactInfo contact
= account
.getContactStore().getContact(address
);
1428 if (contact
!= null) {
1429 messageBuilder
.withExpiration(contact
.messageExpirationTime
);
1430 messageBuilder
.withProfileKey(account
.getProfileKey().serialize());
1432 messageBuilder
.withExpiration(0);
1433 messageBuilder
.withProfileKey(null);
1435 message
= messageBuilder
.build();
1436 if (address
.matches(account
.getSelfAddress())) {
1437 results
.add(sendSelfMessage(message
));
1439 results
.add(sendMessage(address
, message
));
1442 return new Pair
<>(timestamp
, results
);
1445 if (message
!= null && message
.isEndSession()) {
1446 for (SignalServiceAddress recipient
: recipients
) {
1447 handleEndSession(recipient
);
1454 private SendMessageResult
sendSelfMessage(SignalServiceDataMessage message
) throws IOException
{
1455 SignalServiceMessageSender messageSender
= createMessageSender();
1457 SignalServiceAddress recipient
= account
.getSelfAddress();
1459 final Optional
<UnidentifiedAccessPair
> unidentifiedAccess
= unidentifiedAccessHelper
.getAccessFor(recipient
);
1460 SentTranscriptMessage transcript
= new SentTranscriptMessage(Optional
.of(recipient
),
1461 message
.getTimestamp(),
1463 message
.getExpiresInSeconds(),
1464 Collections
.singletonMap(recipient
, unidentifiedAccess
.isPresent()),
1466 SignalServiceSyncMessage syncMessage
= SignalServiceSyncMessage
.forSentTranscript(transcript
);
1469 long startTime
= System
.currentTimeMillis();
1470 messageSender
.sendMessage(syncMessage
, unidentifiedAccess
);
1471 return SendMessageResult
.success(recipient
,
1472 unidentifiedAccess
.isPresent(),
1474 System
.currentTimeMillis() - startTime
);
1475 } catch (UntrustedIdentityException e
) {
1476 account
.getSignalProtocolStore()
1477 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1479 TrustLevel
.UNTRUSTED
);
1480 return SendMessageResult
.identityFailure(recipient
, e
.getIdentityKey());
1484 private SendMessageResult
sendMessage(
1485 SignalServiceAddress address
, SignalServiceDataMessage message
1486 ) throws IOException
{
1487 SignalServiceMessageSender messageSender
= createMessageSender();
1490 return messageSender
.sendMessage(address
, unidentifiedAccessHelper
.getAccessFor(address
), message
);
1491 } catch (UntrustedIdentityException e
) {
1492 account
.getSignalProtocolStore()
1493 .saveIdentity(resolveSignalServiceAddress(e
.getIdentifier()),
1495 TrustLevel
.UNTRUSTED
);
1496 return SendMessageResult
.identityFailure(address
, e
.getIdentityKey());
1500 private SignalServiceContent
decryptMessage(SignalServiceEnvelope envelope
) throws InvalidMetadataMessageException
, ProtocolInvalidMessageException
, ProtocolDuplicateMessageException
, ProtocolLegacyMessageException
, ProtocolInvalidKeyIdException
, InvalidMetadataVersionException
, ProtocolInvalidVersionException
, ProtocolNoSessionException
, ProtocolInvalidKeyException
, SelfSendException
, UnsupportedDataMessageException
, org
.whispersystems
.libsignal
.UntrustedIdentityException
{
1501 SignalServiceCipher cipher
= new SignalServiceCipher(account
.getSelfAddress(),
1502 account
.getSignalProtocolStore(),
1503 Utils
.getCertificateValidator());
1505 return cipher
.decrypt(envelope
);
1506 } catch (ProtocolUntrustedIdentityException e
) {
1507 if (e
.getCause() instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
) {
1508 org
.whispersystems
.libsignal
.UntrustedIdentityException identityException
= (org
.whispersystems
.libsignal
.UntrustedIdentityException
) e
1510 account
.getSignalProtocolStore()
1511 .saveIdentity(resolveSignalServiceAddress(identityException
.getName()),
1512 identityException
.getUntrustedIdentity(),
1513 TrustLevel
.UNTRUSTED
);
1514 throw identityException
;
1516 throw new AssertionError(e
);
1520 private void handleEndSession(SignalServiceAddress source
) {
1521 account
.getSignalProtocolStore().deleteAllSessions(source
);
1524 private static int currentTimeDays() {
1525 return (int) TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis());
1528 private GroupsV2AuthorizationString
getGroupAuthForToday(
1529 final GroupSecretParams groupSecretParams
1530 ) throws IOException
{
1531 final int today
= currentTimeDays();
1532 // Returns credentials for the next 7 days
1533 final HashMap
<Integer
, AuthCredentialResponse
> credentials
= groupsV2Api
.getCredentials(today
);
1534 // TODO cache credentials until they expire
1535 AuthCredentialResponse authCredentialResponse
= credentials
.get(today
);
1537 return groupsV2Api
.getGroupsV2AuthorizationString(account
.getUuid(),
1540 authCredentialResponse
);
1541 } catch (VerificationFailedException e
) {
1542 throw new IOException(e
);
1546 private List
<HandleAction
> handleSignalServiceDataMessage(
1547 SignalServiceDataMessage message
,
1549 SignalServiceAddress source
,
1550 SignalServiceAddress destination
,
1551 boolean ignoreAttachments
1553 List
<HandleAction
> actions
= new ArrayList
<>();
1554 if (message
.getGroupContext().isPresent()) {
1555 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1556 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1557 GroupIdV1 groupId
= GroupId
.v1(groupInfo
.getGroupId());
1558 GroupInfo group
= account
.getGroupStore().getGroup(groupId
);
1559 if (group
== null || group
instanceof GroupInfoV1
) {
1560 GroupInfoV1 groupV1
= (GroupInfoV1
) group
;
1561 switch (groupInfo
.getType()) {
1563 if (groupV1
== null) {
1564 groupV1
= new GroupInfoV1(groupId
);
1567 if (groupInfo
.getAvatar().isPresent()) {
1568 SignalServiceAttachment avatar
= groupInfo
.getAvatar().get();
1569 if (avatar
.isPointer()) {
1571 retrieveGroupAvatarAttachment(avatar
.asPointer(), groupV1
.getGroupId());
1572 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1573 System
.err
.println("Failed to retrieve group avatar (" + avatar
.asPointer()
1574 .getRemoteId() + "): " + e
.getMessage());
1579 if (groupInfo
.getName().isPresent()) {
1580 groupV1
.name
= groupInfo
.getName().get();
1583 if (groupInfo
.getMembers().isPresent()) {
1584 groupV1
.addMembers(groupInfo
.getMembers()
1587 .map(this::resolveSignalServiceAddress
)
1588 .collect(Collectors
.toSet()));
1591 account
.getGroupStore().updateGroup(groupV1
);
1595 if (groupV1
== null && !isSync
) {
1596 actions
.add(new SendGroupInfoRequestAction(source
, groupV1
.getGroupId()));
1600 if (groupV1
!= null) {
1601 groupV1
.removeMember(source
);
1602 account
.getGroupStore().updateGroup(groupV1
);
1607 if (groupV1
!= null && !isSync
) {
1608 actions
.add(new SendGroupUpdateAction(source
, groupV1
.getGroupId()));
1613 // Received a group v1 message for a v2 group
1616 if (message
.getGroupContext().get().getGroupV2().isPresent()) {
1617 final SignalServiceGroupV2 groupContext
= message
.getGroupContext().get().getGroupV2().get();
1618 final GroupMasterKey groupMasterKey
= groupContext
.getMasterKey();
1620 getOrMigrateGroup(groupMasterKey
,
1621 groupContext
.getRevision(),
1622 groupContext
.hasSignedGroupChange() ? groupContext
.getSignedGroupChange() : null);
1626 final SignalServiceAddress conversationPartnerAddress
= isSync ? destination
: source
;
1627 if (conversationPartnerAddress
!= null && message
.isEndSession()) {
1628 handleEndSession(conversationPartnerAddress
);
1630 if (message
.isExpirationUpdate() || message
.getBody().isPresent()) {
1631 if (message
.getGroupContext().isPresent()) {
1632 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1633 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1634 GroupInfoV1 group
= account
.getGroupStore().getOrCreateGroupV1(GroupId
.v1(groupInfo
.getGroupId()));
1635 if (group
!= null) {
1636 if (group
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1637 group
.messageExpirationTime
= message
.getExpiresInSeconds();
1638 account
.getGroupStore().updateGroup(group
);
1641 } else if (message
.getGroupContext().get().getGroupV2().isPresent()) {
1642 // disappearing message timer already stored in the DecryptedGroup
1644 } else if (conversationPartnerAddress
!= null) {
1645 ContactInfo contact
= account
.getContactStore().getContact(conversationPartnerAddress
);
1646 if (contact
== null) {
1647 contact
= new ContactInfo(conversationPartnerAddress
);
1649 if (contact
.messageExpirationTime
!= message
.getExpiresInSeconds()) {
1650 contact
.messageExpirationTime
= message
.getExpiresInSeconds();
1651 account
.getContactStore().updateContact(contact
);
1655 if (message
.getAttachments().isPresent() && !ignoreAttachments
) {
1656 for (SignalServiceAttachment attachment
: message
.getAttachments().get()) {
1657 if (attachment
.isPointer()) {
1659 retrieveAttachment(attachment
.asPointer());
1660 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1661 System
.err
.println("Failed to retrieve attachment ("
1662 + attachment
.asPointer().getRemoteId()
1669 if (message
.getProfileKey().isPresent() && message
.getProfileKey().get().length
== 32) {
1670 final ProfileKey profileKey
;
1672 profileKey
= new ProfileKey(message
.getProfileKey().get());
1673 } catch (InvalidInputException e
) {
1674 throw new AssertionError(e
);
1676 if (source
.matches(account
.getSelfAddress())) {
1677 this.account
.setProfileKey(profileKey
);
1679 this.account
.getProfileStore().storeProfileKey(source
, profileKey
);
1681 if (message
.getPreviews().isPresent()) {
1682 final List
<SignalServiceDataMessage
.Preview
> previews
= message
.getPreviews().get();
1683 for (SignalServiceDataMessage
.Preview preview
: previews
) {
1684 if (preview
.getImage().isPresent() && preview
.getImage().get().isPointer()) {
1685 SignalServiceAttachmentPointer attachment
= preview
.getImage().get().asPointer();
1687 retrieveAttachment(attachment
);
1688 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1689 System
.err
.println("Failed to retrieve attachment ("
1690 + attachment
.getRemoteId()
1697 if (message
.getQuote().isPresent()) {
1698 final SignalServiceDataMessage
.Quote quote
= message
.getQuote().get();
1700 for (SignalServiceDataMessage
.Quote
.QuotedAttachment quotedAttachment
: quote
.getAttachments()) {
1701 final SignalServiceAttachment attachment
= quotedAttachment
.getThumbnail();
1702 if (attachment
!= null && attachment
.isPointer()) {
1704 retrieveAttachment(attachment
.asPointer());
1705 } catch (IOException
| InvalidMessageException
| MissingConfigurationException e
) {
1706 System
.err
.println("Failed to retrieve attachment ("
1707 + attachment
.asPointer().getRemoteId()
1714 if (message
.getSticker().isPresent()) {
1715 final SignalServiceDataMessage
.Sticker messageSticker
= message
.getSticker().get();
1716 Sticker sticker
= account
.getStickerStore().getSticker(messageSticker
.getPackId());
1717 if (sticker
== null) {
1718 sticker
= new Sticker(messageSticker
.getPackId(), messageSticker
.getPackKey());
1719 account
.getStickerStore().updateSticker(sticker
);
1725 private GroupInfoV2
getOrMigrateGroup(
1726 final GroupMasterKey groupMasterKey
, final int revision
, final byte[] signedGroupChange
1728 final GroupSecretParams groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
1730 GroupIdV2 groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
1731 GroupInfo groupInfo
= account
.getGroupStore().getGroup(groupId
);
1732 final GroupInfoV2 groupInfoV2
;
1733 if (groupInfo
instanceof GroupInfoV1
) {
1734 // Received a v2 group message for a v1 group, we need to locally migrate the group
1735 account
.getGroupStore().deleteGroup(groupInfo
.getGroupId());
1736 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
1737 System
.err
.println("Locally migrated group "
1738 + groupInfo
.getGroupId().toBase64()
1739 + " to group v2, id: "
1740 + groupInfoV2
.getGroupId().toBase64()
1742 } else if (groupInfo
instanceof GroupInfoV2
) {
1743 groupInfoV2
= (GroupInfoV2
) groupInfo
;
1745 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
1748 if (groupInfoV2
.getGroup() == null || groupInfoV2
.getGroup().getRevision() < revision
) {
1749 DecryptedGroup group
= null;
1750 if (signedGroupChange
!= null
1751 && groupInfoV2
.getGroup() != null
1752 && groupInfoV2
.getGroup().getRevision() + 1 == revision
) {
1753 group
= groupHelper
.getUpdatedDecryptedGroup(groupInfoV2
.getGroup(), signedGroupChange
, groupMasterKey
);
1755 if (group
== null) {
1756 group
= groupHelper
.getDecryptedGroup(groupSecretParams
);
1758 if (group
!= null) {
1759 storeProfileKeysFromMembers(group
);
1761 retrieveGroupAvatar(groupId
, groupSecretParams
, group
.getAvatar());
1762 } catch (IOException e
) {
1763 System
.err
.println("Failed to download group avatar, ignoring ...");
1766 groupInfoV2
.setGroup(group
);
1767 account
.getGroupStore().updateGroup(groupInfoV2
);
1773 private void storeProfileKeysFromMembers(final DecryptedGroup group
) {
1774 for (DecryptedMember member
: group
.getMembersList()) {
1775 final SignalServiceAddress address
= resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil
.parseOrThrow(
1776 member
.getUuid().toByteArray()), null));
1778 account
.getProfileStore()
1779 .storeProfileKey(address
, new ProfileKey(member
.getProfileKey().toByteArray()));
1780 } catch (InvalidInputException ignored
) {
1785 private void retryFailedReceivedMessages(
1786 ReceiveMessageHandler handler
, boolean ignoreAttachments
1788 final File cachePath
= new File(getMessageCachePath());
1789 if (!cachePath
.exists()) {
1792 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1793 if (!dir
.isDirectory()) {
1794 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1798 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1799 if (!fileEntry
.isFile()) {
1802 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1804 // Try to delete directory if empty
1809 private void retryFailedReceivedMessage(
1810 final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
1812 SignalServiceEnvelope envelope
;
1814 envelope
= Utils
.loadEnvelope(fileEntry
);
1815 if (envelope
== null) {
1818 } catch (IOException e
) {
1819 e
.printStackTrace();
1822 SignalServiceContent content
= null;
1823 if (!envelope
.isReceipt()) {
1825 content
= decryptMessage(envelope
);
1826 } catch (org
.whispersystems
.libsignal
.UntrustedIdentityException e
) {
1828 } catch (Exception er
) {
1829 // All other errors are not recoverable, so delete the cached message
1831 Files
.delete(fileEntry
.toPath());
1832 } catch (IOException e
) {
1833 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1837 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1838 for (HandleAction action
: actions
) {
1840 action
.execute(this);
1841 } catch (Throwable e
) {
1842 e
.printStackTrace();
1847 handler
.handleMessage(envelope
, content
, null);
1849 Files
.delete(fileEntry
.toPath());
1850 } catch (IOException e
) {
1851 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1855 public void receiveMessages(
1858 boolean returnOnTimeout
,
1859 boolean ignoreAttachments
,
1860 ReceiveMessageHandler handler
1861 ) throws IOException
{
1862 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1864 Set
<HandleAction
> queuedActions
= null;
1866 getOrCreateMessagePipe();
1868 boolean hasCaughtUpWithOldMessages
= false;
1871 SignalServiceEnvelope envelope
;
1872 SignalServiceContent content
= null;
1873 Exception exception
= null;
1874 final long now
= new Date().getTime();
1876 Optional
<SignalServiceEnvelope
> result
= messagePipe
.readOrEmpty(timeout
, unit
, envelope1
-> {
1877 // store message on disk, before acknowledging receipt to the server
1879 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1880 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1881 Utils
.storeEnvelope(envelope1
, cacheFile
);
1882 } catch (IOException e
) {
1883 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: "
1887 if (result
.isPresent()) {
1888 envelope
= result
.get();
1890 // Received indicator that server queue is empty
1891 hasCaughtUpWithOldMessages
= true;
1893 if (queuedActions
!= null) {
1894 for (HandleAction action
: queuedActions
) {
1896 action
.execute(this);
1897 } catch (Throwable e
) {
1898 e
.printStackTrace();
1902 queuedActions
.clear();
1903 queuedActions
= null;
1906 // Continue to wait another timeout for new messages
1909 } catch (TimeoutException e
) {
1910 if (returnOnTimeout
) return;
1912 } catch (InvalidVersionException e
) {
1913 System
.err
.println("Ignoring error: " + e
.getMessage());
1917 if (envelope
.hasSource()) {
1918 // Store uuid if we don't have it already
1919 SignalServiceAddress source
= envelope
.getSourceAddress();
1920 resolveSignalServiceAddress(source
);
1922 if (!envelope
.isReceipt()) {
1924 content
= decryptMessage(envelope
);
1925 } catch (Exception e
) {
1928 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1929 if (hasCaughtUpWithOldMessages
) {
1930 for (HandleAction action
: actions
) {
1932 action
.execute(this);
1933 } catch (Throwable e
) {
1934 e
.printStackTrace();
1938 if (queuedActions
== null) {
1939 queuedActions
= new HashSet
<>();
1941 queuedActions
.addAll(actions
);
1945 if (!isMessageBlocked(envelope
, content
)) {
1946 handler
.handleMessage(envelope
, content
, exception
);
1948 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
1949 File cacheFile
= null;
1951 String source
= envelope
.getSourceE164().isPresent() ? envelope
.getSourceE164().get() : "";
1952 cacheFile
= getMessageCacheFile(source
, now
, envelope
.getTimestamp());
1953 Files
.delete(cacheFile
.toPath());
1954 // Try to delete directory if empty
1955 new File(getMessageCachePath()).delete();
1956 } catch (IOException e
) {
1957 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1963 private boolean isMessageBlocked(
1964 SignalServiceEnvelope envelope
, SignalServiceContent content
1966 SignalServiceAddress source
;
1967 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1968 source
= envelope
.getSourceAddress();
1969 } else if (content
!= null) {
1970 source
= content
.getSender();
1974 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
1975 if (sourceContact
!= null && sourceContact
.blocked
) {
1979 if (content
!= null && content
.getDataMessage().isPresent()) {
1980 SignalServiceDataMessage message
= content
.getDataMessage().get();
1981 if (message
.getGroupContext().isPresent()) {
1982 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1983 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1984 if (groupInfo
.getType() != SignalServiceGroup
.Type
.DELIVER
) {
1988 GroupId groupId
= GroupUtils
.getGroupId(message
.getGroupContext().get());
1989 GroupInfo group
= account
.getGroupStore().getGroup(groupId
);
1990 if (group
!= null && group
.isBlocked()) {
1998 private List
<HandleAction
> handleMessage(
1999 SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
2001 List
<HandleAction
> actions
= new ArrayList
<>();
2002 if (content
!= null) {
2003 final SignalServiceAddress sender
;
2004 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
2005 sender
= envelope
.getSourceAddress();
2007 sender
= content
.getSender();
2009 // Store uuid if we don't have it already
2010 resolveSignalServiceAddress(sender
);
2012 if (content
.getDataMessage().isPresent()) {
2013 SignalServiceDataMessage message
= content
.getDataMessage().get();
2015 if (content
.isNeedsReceipt()) {
2016 actions
.add(new SendReceiptAction(sender
, message
.getTimestamp()));
2019 actions
.addAll(handleSignalServiceDataMessage(message
,
2022 account
.getSelfAddress(),
2023 ignoreAttachments
));
2025 if (content
.getSyncMessage().isPresent()) {
2026 account
.setMultiDevice(true);
2027 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
2028 if (syncMessage
.getSent().isPresent()) {
2029 SentTranscriptMessage message
= syncMessage
.getSent().get();
2030 final SignalServiceAddress destination
= message
.getDestination().orNull();
2031 actions
.addAll(handleSignalServiceDataMessage(message
.getMessage(),
2035 ignoreAttachments
));
2037 if (syncMessage
.getRequest().isPresent()) {
2038 RequestMessage rm
= syncMessage
.getRequest().get();
2039 if (rm
.isContactsRequest()) {
2040 actions
.add(SendSyncContactsAction
.create());
2042 if (rm
.isGroupsRequest()) {
2043 actions
.add(SendSyncGroupsAction
.create());
2045 if (rm
.isBlockedListRequest()) {
2046 actions
.add(SendSyncBlockedListAction
.create());
2048 // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest();
2050 if (syncMessage
.getGroups().isPresent()) {
2051 File tmpFile
= null;
2053 tmpFile
= IOUtils
.createTempFile();
2054 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups()
2056 .asPointer(), tmpFile
)) {
2057 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
2059 while ((g
= s
.read()) != null) {
2060 GroupInfoV1 syncGroup
= account
.getGroupStore()
2061 .getOrCreateGroupV1(GroupId
.v1(g
.getId()));
2062 if (syncGroup
!= null) {
2063 if (g
.getName().isPresent()) {
2064 syncGroup
.name
= g
.getName().get();
2066 syncGroup
.addMembers(g
.getMembers()
2068 .map(this::resolveSignalServiceAddress
)
2069 .collect(Collectors
.toSet()));
2070 if (!g
.isActive()) {
2071 syncGroup
.removeMember(account
.getSelfAddress());
2073 // Add ourself to the member set as it's marked as active
2074 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
2076 syncGroup
.blocked
= g
.isBlocked();
2077 if (g
.getColor().isPresent()) {
2078 syncGroup
.color
= g
.getColor().get();
2081 if (g
.getAvatar().isPresent()) {
2082 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.getGroupId());
2084 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
2085 syncGroup
.archived
= g
.isArchived();
2086 account
.getGroupStore().updateGroup(syncGroup
);
2090 } catch (Exception e
) {
2091 e
.printStackTrace();
2093 if (tmpFile
!= null) {
2095 Files
.delete(tmpFile
.toPath());
2096 } catch (IOException e
) {
2097 System
.err
.println("Failed to delete received groups temp file “"
2105 if (syncMessage
.getBlockedList().isPresent()) {
2106 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
2107 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
2108 setContactBlocked(resolveSignalServiceAddress(address
), true);
2110 for (GroupId groupId
: blockedListMessage
.getGroupIds()
2112 .map(GroupId
::unknownVersion
)
2113 .collect(Collectors
.toSet())) {
2115 setGroupBlocked(groupId
, true);
2116 } catch (GroupNotFoundException e
) {
2117 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: "
2118 + groupId
.toBase64());
2122 if (syncMessage
.getContacts().isPresent()) {
2123 File tmpFile
= null;
2125 tmpFile
= IOUtils
.createTempFile();
2126 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
2127 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream()
2128 .asPointer(), tmpFile
)) {
2129 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
2130 if (contactsMessage
.isComplete()) {
2131 account
.getContactStore().clear();
2134 while ((c
= s
.read()) != null) {
2135 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
2136 account
.setProfileKey(c
.getProfileKey().get());
2138 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
2139 ContactInfo contact
= account
.getContactStore().getContact(address
);
2140 if (contact
== null) {
2141 contact
= new ContactInfo(address
);
2143 if (c
.getName().isPresent()) {
2144 contact
.name
= c
.getName().get();
2146 if (c
.getColor().isPresent()) {
2147 contact
.color
= c
.getColor().get();
2149 if (c
.getProfileKey().isPresent()) {
2150 account
.getProfileStore().storeProfileKey(address
, c
.getProfileKey().get());
2152 if (c
.getVerified().isPresent()) {
2153 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
2154 account
.getSignalProtocolStore()
2155 .setIdentityTrustLevel(verifiedMessage
.getDestination(),
2156 verifiedMessage
.getIdentityKey(),
2157 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
2159 if (c
.getExpirationTimer().isPresent()) {
2160 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
2162 contact
.blocked
= c
.isBlocked();
2163 contact
.inboxPosition
= c
.getInboxPosition().orNull();
2164 contact
.archived
= c
.isArchived();
2165 account
.getContactStore().updateContact(contact
);
2167 if (c
.getAvatar().isPresent()) {
2168 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
2172 } catch (Exception e
) {
2173 e
.printStackTrace();
2175 if (tmpFile
!= null) {
2177 Files
.delete(tmpFile
.toPath());
2178 } catch (IOException e
) {
2179 System
.err
.println("Failed to delete received contacts temp file “"
2187 if (syncMessage
.getVerified().isPresent()) {
2188 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
2189 account
.getSignalProtocolStore()
2190 .setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()),
2191 verifiedMessage
.getIdentityKey(),
2192 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
2194 if (syncMessage
.getStickerPackOperations().isPresent()) {
2195 final List
<StickerPackOperationMessage
> stickerPackOperationMessages
= syncMessage
.getStickerPackOperations()
2197 for (StickerPackOperationMessage m
: stickerPackOperationMessages
) {
2198 if (!m
.getPackId().isPresent()) {
2201 Sticker sticker
= account
.getStickerStore().getSticker(m
.getPackId().get());
2202 if (sticker
== null) {
2203 if (!m
.getPackKey().isPresent()) {
2206 sticker
= new Sticker(m
.getPackId().get(), m
.getPackKey().get());
2208 sticker
.setInstalled(!m
.getType().isPresent()
2209 || m
.getType().get() == StickerPackOperationMessage
.Type
.INSTALL
);
2210 account
.getStickerStore().updateSticker(sticker
);
2213 if (syncMessage
.getConfiguration().isPresent()) {
2221 private File
getContactAvatarFile(String number
) {
2222 return new File(pathConfig
.getAvatarsPath(), "contact-" + number
);
2225 private File
retrieveContactAvatarAttachment(
2226 SignalServiceAttachment attachment
, String number
2227 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2228 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2229 if (attachment
.isPointer()) {
2230 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
2231 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
2233 SignalServiceAttachmentStream stream
= attachment
.asStream();
2234 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
2238 private File
getGroupAvatarFile(GroupId groupId
) {
2239 return new File(pathConfig
.getAvatarsPath(), "group-" + groupId
.toBase64().replace("/", "_"));
2242 private File
retrieveGroupAvatarAttachment(
2243 SignalServiceAttachment attachment
, GroupId groupId
2244 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2245 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2246 if (attachment
.isPointer()) {
2247 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
2248 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
2250 SignalServiceAttachmentStream stream
= attachment
.asStream();
2251 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
2255 private File
retrieveGroupAvatar(
2256 GroupId groupId
, GroupSecretParams groupSecretParams
, String cdnKey
2257 ) throws IOException
{
2258 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2259 SignalServiceMessageReceiver receiver
= getOrCreateMessageReceiver();
2260 File outputFile
= getGroupAvatarFile(groupId
);
2261 GroupsV2Operations
.GroupOperations groupOperations
= groupsV2Operations
.forGroup(groupSecretParams
);
2263 File tmpFile
= IOUtils
.createTempFile();
2264 tmpFile
.deleteOnExit();
2265 try (InputStream input
= receiver
.retrieveGroupsV2ProfileAvatar(cdnKey
,
2267 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
2268 byte[] encryptedData
= IOUtils
.readFully(input
);
2270 byte[] decryptedData
= groupOperations
.decryptAvatar(encryptedData
);
2271 try (OutputStream output
= new FileOutputStream(outputFile
)) {
2272 output
.write(decryptedData
);
2276 Files
.delete(tmpFile
.toPath());
2277 } catch (IOException e
) {
2278 System
.err
.println("Failed to delete received avatar temp file “" + tmpFile
+ "”: " + e
.getMessage());
2284 private File
getProfileAvatarFile(SignalServiceAddress address
) {
2285 return new File(pathConfig
.getAvatarsPath(), "profile-" + address
.getLegacyIdentifier());
2288 private File
retrieveProfileAvatar(
2289 SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
2290 ) throws IOException
{
2291 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2292 SignalServiceMessageReceiver receiver
= getOrCreateMessageReceiver();
2293 File outputFile
= getProfileAvatarFile(address
);
2295 File tmpFile
= IOUtils
.createTempFile();
2296 try (InputStream input
= receiver
.retrieveProfileAvatar(avatarPath
,
2299 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
2300 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
2301 IOUtils
.copyStreamToFile(input
, outputFile
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
2304 Files
.delete(tmpFile
.toPath());
2305 } catch (IOException e
) {
2306 System
.err
.println("Failed to delete received avatar temp file “" + tmpFile
+ "”: " + e
.getMessage());
2312 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
2313 return new File(pathConfig
.getAttachmentsPath(), attachmentId
.toString());
2316 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2317 IOUtils
.createPrivateDirectories(pathConfig
.getAttachmentsPath());
2318 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
2321 private File
retrieveAttachment(
2322 SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
2323 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2324 if (storePreview
&& pointer
.getPreview().isPresent()) {
2325 File previewFile
= new File(outputFile
+ ".preview");
2326 try (OutputStream output
= new FileOutputStream(previewFile
)) {
2327 byte[] preview
= pointer
.getPreview().get();
2328 output
.write(preview
, 0, preview
.length
);
2329 } catch (FileNotFoundException e
) {
2330 e
.printStackTrace();
2335 final SignalServiceMessageReceiver messageReceiver
= getOrCreateMessageReceiver();
2337 File tmpFile
= IOUtils
.createTempFile();
2338 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
,
2340 ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
2341 IOUtils
.copyStreamToFile(input
, outputFile
);
2344 Files
.delete(tmpFile
.toPath());
2345 } catch (IOException e
) {
2346 System
.err
.println("Failed to delete received attachment temp file “"
2355 private InputStream
retrieveAttachmentAsStream(
2356 SignalServiceAttachmentPointer pointer
, File tmpFile
2357 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2358 final SignalServiceMessageReceiver messageReceiver
= getOrCreateMessageReceiver();
2359 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
2362 void sendGroups() throws IOException
, UntrustedIdentityException
{
2363 File groupsFile
= IOUtils
.createTempFile();
2366 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
2367 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
2368 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
2369 if (record instanceof GroupInfoV1
) {
2370 GroupInfoV1 groupInfo
= (GroupInfoV1
) record;
2371 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
2372 Optional
.fromNullable(groupInfo
.name
),
2373 new ArrayList
<>(groupInfo
.getMembers()),
2374 createGroupAvatarAttachment(groupInfo
.getGroupId()),
2375 groupInfo
.isMember(account
.getSelfAddress()),
2376 Optional
.of(groupInfo
.messageExpirationTime
),
2377 Optional
.fromNullable(groupInfo
.color
),
2379 Optional
.fromNullable(groupInfo
.inboxPosition
),
2380 groupInfo
.archived
));
2385 if (groupsFile
.exists() && groupsFile
.length() > 0) {
2386 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
2387 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
2388 .withStream(groupsFileStream
)
2389 .withContentType("application/octet-stream")
2390 .withLength(groupsFile
.length())
2393 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
2398 Files
.delete(groupsFile
.toPath());
2399 } catch (IOException e
) {
2400 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
2405 public void sendContacts() throws IOException
, UntrustedIdentityException
{
2406 File contactsFile
= IOUtils
.createTempFile();
2409 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
2410 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
2411 for (ContactInfo
record : account
.getContactStore().getContacts()) {
2412 VerifiedMessage verifiedMessage
= null;
2413 JsonIdentityKeyStore
.Identity currentIdentity
= account
.getSignalProtocolStore()
2414 .getIdentity(record.getAddress());
2415 if (currentIdentity
!= null) {
2416 verifiedMessage
= new VerifiedMessage(record.getAddress(),
2417 currentIdentity
.getIdentityKey(),
2418 currentIdentity
.getTrustLevel().toVerifiedState(),
2419 currentIdentity
.getDateAdded().getTime());
2422 ProfileKey profileKey
= account
.getProfileStore().getProfileKey(record.getAddress());
2423 out
.write(new DeviceContact(record.getAddress(),
2424 Optional
.fromNullable(record.name
),
2425 createContactAvatarAttachment(record.number
),
2426 Optional
.fromNullable(record.color
),
2427 Optional
.fromNullable(verifiedMessage
),
2428 Optional
.fromNullable(profileKey
),
2430 Optional
.of(record.messageExpirationTime
),
2431 Optional
.fromNullable(record.inboxPosition
),
2435 if (account
.getProfileKey() != null) {
2436 // Send our own profile key as well
2437 out
.write(new DeviceContact(account
.getSelfAddress(),
2442 Optional
.of(account
.getProfileKey()),
2450 if (contactsFile
.exists() && contactsFile
.length() > 0) {
2451 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
2452 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
2453 .withStream(contactsFileStream
)
2454 .withContentType("application/octet-stream")
2455 .withLength(contactsFile
.length())
2458 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
2463 Files
.delete(contactsFile
.toPath());
2464 } catch (IOException e
) {
2465 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
2470 void sendBlockedList() throws IOException
, UntrustedIdentityException
{
2471 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
2472 for (ContactInfo
record : account
.getContactStore().getContacts()) {
2473 if (record.blocked
) {
2474 addresses
.add(record.getAddress());
2477 List
<byte[]> groupIds
= new ArrayList
<>();
2478 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
2479 if (record.isBlocked()) {
2480 groupIds
.add(record.getGroupId().serialize());
2483 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
2486 private void sendVerifiedMessage(
2487 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
2488 ) throws IOException
, UntrustedIdentityException
{
2489 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
,
2491 trustLevel
.toVerifiedState(),
2492 System
.currentTimeMillis());
2493 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
2496 public List
<ContactInfo
> getContacts() {
2497 return account
.getContactStore().getContacts();
2500 public ContactInfo
getContact(String number
) {
2501 return account
.getContactStore().getContact(Util
.getSignalServiceAddressFromIdentifier(number
));
2504 public GroupInfo
getGroup(GroupId groupId
) {
2505 return account
.getGroupStore().getGroup(groupId
);
2508 public List
<JsonIdentityKeyStore
.Identity
> getIdentities() {
2509 return account
.getSignalProtocolStore().getIdentities();
2512 public List
<JsonIdentityKeyStore
.Identity
> getIdentities(String number
) throws InvalidNumberException
{
2513 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
2517 * Trust this the identity with this fingerprint
2519 * @param name username of the identity
2520 * @param fingerprint Fingerprint
2522 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
2523 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2524 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2528 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2529 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
2533 account
.getSignalProtocolStore()
2534 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2536 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2537 } catch (IOException
| UntrustedIdentityException e
) {
2538 e
.printStackTrace();
2547 * Trust this the identity with this safety number
2549 * @param name username of the identity
2550 * @param safetyNumber Safety number
2552 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
2553 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2554 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2558 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2559 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
2563 account
.getSignalProtocolStore()
2564 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2566 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2567 } catch (IOException
| UntrustedIdentityException e
) {
2568 e
.printStackTrace();
2577 * Trust all keys of this identity without verification
2579 * @param name username of the identity
2581 public boolean trustIdentityAllKeys(String name
) {
2582 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
2583 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2587 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2588 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2589 account
.getSignalProtocolStore()
2590 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2592 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2593 } catch (IOException
| UntrustedIdentityException e
) {
2594 e
.printStackTrace();
2602 public String
computeSafetyNumber(
2603 SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
2605 return Utils
.computeSafetyNumber(account
.getSelfAddress(),
2606 getIdentityKeyPair().getPublicKey(),
2611 void saveAccount() {
2615 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2616 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
)
2618 : Util
.canonicalizeNumber(identifier
, account
.getUsername());
2619 return resolveSignalServiceAddress(canonicalizedNumber
);
2622 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2623 SignalServiceAddress address
= Util
.getSignalServiceAddressFromIdentifier(identifier
);
2625 return resolveSignalServiceAddress(address
);
2628 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2629 if (address
.matches(account
.getSelfAddress())) {
2630 return account
.getSelfAddress();
2633 return account
.getRecipientStore().resolveServiceAddress(address
);
2637 public void close() throws IOException
{
2638 if (messagePipe
!= null) {
2639 messagePipe
.shutdown();
2643 if (unidentifiedMessagePipe
!= null) {
2644 unidentifiedMessagePipe
.shutdown();
2645 unidentifiedMessagePipe
= null;
2651 public interface ReceiveMessageHandler
{
2653 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);