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