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 groupInfoV2
.setGroup(group
);
1759 account
.getGroupStore().updateGroup(groupInfoV2
);
1765 private void storeProfileKeysFromMembers(final DecryptedGroup group
) {
1766 for (DecryptedMember member
: group
.getMembersList()) {
1767 final SignalServiceAddress address
= resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil
.parseOrThrow(
1768 member
.getUuid().toByteArray()), null));
1770 account
.getProfileStore()
1771 .storeProfileKey(address
, new ProfileKey(member
.getProfileKey().toByteArray()));
1772 } catch (InvalidInputException ignored
) {
1777 private void retryFailedReceivedMessages(
1778 ReceiveMessageHandler handler
, boolean ignoreAttachments
1780 final File cachePath
= new File(getMessageCachePath());
1781 if (!cachePath
.exists()) {
1784 for (final File dir
: Objects
.requireNonNull(cachePath
.listFiles())) {
1785 if (!dir
.isDirectory()) {
1786 retryFailedReceivedMessage(handler
, ignoreAttachments
, dir
);
1790 for (final File fileEntry
: Objects
.requireNonNull(dir
.listFiles())) {
1791 if (!fileEntry
.isFile()) {
1794 retryFailedReceivedMessage(handler
, ignoreAttachments
, fileEntry
);
1796 // Try to delete directory if empty
1801 private void retryFailedReceivedMessage(
1802 final ReceiveMessageHandler handler
, final boolean ignoreAttachments
, final File fileEntry
1804 SignalServiceEnvelope envelope
;
1806 envelope
= Utils
.loadEnvelope(fileEntry
);
1807 if (envelope
== null) {
1810 } catch (IOException e
) {
1811 e
.printStackTrace();
1814 SignalServiceContent content
= null;
1815 if (!envelope
.isReceipt()) {
1817 content
= decryptMessage(envelope
);
1818 } catch (org
.whispersystems
.libsignal
.UntrustedIdentityException e
) {
1820 } catch (Exception er
) {
1821 // All other errors are not recoverable, so delete the cached message
1823 Files
.delete(fileEntry
.toPath());
1824 } catch (IOException e
) {
1825 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1829 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1830 for (HandleAction action
: actions
) {
1832 action
.execute(this);
1833 } catch (Throwable e
) {
1834 e
.printStackTrace();
1839 handler
.handleMessage(envelope
, content
, null);
1841 Files
.delete(fileEntry
.toPath());
1842 } catch (IOException e
) {
1843 System
.err
.println("Failed to delete cached message file “" + fileEntry
+ "”: " + e
.getMessage());
1847 public void receiveMessages(
1850 boolean returnOnTimeout
,
1851 boolean ignoreAttachments
,
1852 ReceiveMessageHandler handler
1853 ) throws IOException
{
1854 retryFailedReceivedMessages(handler
, ignoreAttachments
);
1856 Set
<HandleAction
> queuedActions
= null;
1858 getOrCreateMessagePipe();
1860 boolean hasCaughtUpWithOldMessages
= false;
1863 SignalServiceEnvelope envelope
;
1864 SignalServiceContent content
= null;
1865 Exception exception
= null;
1866 final long now
= new Date().getTime();
1868 Optional
<SignalServiceEnvelope
> result
= messagePipe
.readOrEmpty(timeout
, unit
, envelope1
-> {
1869 // store message on disk, before acknowledging receipt to the server
1871 String source
= envelope1
.getSourceE164().isPresent() ? envelope1
.getSourceE164().get() : "";
1872 File cacheFile
= getMessageCacheFile(source
, now
, envelope1
.getTimestamp());
1873 Utils
.storeEnvelope(envelope1
, cacheFile
);
1874 } catch (IOException e
) {
1875 System
.err
.println("Failed to store encrypted message in disk cache, ignoring: "
1879 if (result
.isPresent()) {
1880 envelope
= result
.get();
1882 // Received indicator that server queue is empty
1883 hasCaughtUpWithOldMessages
= true;
1885 if (queuedActions
!= null) {
1886 for (HandleAction action
: queuedActions
) {
1888 action
.execute(this);
1889 } catch (Throwable e
) {
1890 e
.printStackTrace();
1894 queuedActions
.clear();
1895 queuedActions
= null;
1898 // Continue to wait another timeout for new messages
1901 } catch (TimeoutException e
) {
1902 if (returnOnTimeout
) return;
1904 } catch (InvalidVersionException e
) {
1905 System
.err
.println("Ignoring error: " + e
.getMessage());
1909 if (envelope
.hasSource()) {
1910 // Store uuid if we don't have it already
1911 SignalServiceAddress source
= envelope
.getSourceAddress();
1912 resolveSignalServiceAddress(source
);
1914 if (!envelope
.isReceipt()) {
1916 content
= decryptMessage(envelope
);
1917 } catch (Exception e
) {
1920 List
<HandleAction
> actions
= handleMessage(envelope
, content
, ignoreAttachments
);
1921 if (hasCaughtUpWithOldMessages
) {
1922 for (HandleAction action
: actions
) {
1924 action
.execute(this);
1925 } catch (Throwable e
) {
1926 e
.printStackTrace();
1930 if (queuedActions
== null) {
1931 queuedActions
= new HashSet
<>();
1933 queuedActions
.addAll(actions
);
1937 if (!isMessageBlocked(envelope
, content
)) {
1938 handler
.handleMessage(envelope
, content
, exception
);
1940 if (!(exception
instanceof org
.whispersystems
.libsignal
.UntrustedIdentityException
)) {
1941 File cacheFile
= null;
1943 String source
= envelope
.getSourceE164().isPresent() ? envelope
.getSourceE164().get() : "";
1944 cacheFile
= getMessageCacheFile(source
, now
, envelope
.getTimestamp());
1945 Files
.delete(cacheFile
.toPath());
1946 // Try to delete directory if empty
1947 new File(getMessageCachePath()).delete();
1948 } catch (IOException e
) {
1949 System
.err
.println("Failed to delete cached message file “" + cacheFile
+ "”: " + e
.getMessage());
1955 private boolean isMessageBlocked(
1956 SignalServiceEnvelope envelope
, SignalServiceContent content
1958 SignalServiceAddress source
;
1959 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1960 source
= envelope
.getSourceAddress();
1961 } else if (content
!= null) {
1962 source
= content
.getSender();
1966 ContactInfo sourceContact
= account
.getContactStore().getContact(source
);
1967 if (sourceContact
!= null && sourceContact
.blocked
) {
1971 if (content
!= null && content
.getDataMessage().isPresent()) {
1972 SignalServiceDataMessage message
= content
.getDataMessage().get();
1973 if (message
.getGroupContext().isPresent()) {
1974 if (message
.getGroupContext().get().getGroupV1().isPresent()) {
1975 SignalServiceGroup groupInfo
= message
.getGroupContext().get().getGroupV1().get();
1976 if (groupInfo
.getType() != SignalServiceGroup
.Type
.DELIVER
) {
1980 GroupId groupId
= GroupUtils
.getGroupId(message
.getGroupContext().get());
1981 GroupInfo group
= account
.getGroupStore().getGroup(groupId
);
1982 if (group
!= null && group
.isBlocked()) {
1990 private List
<HandleAction
> handleMessage(
1991 SignalServiceEnvelope envelope
, SignalServiceContent content
, boolean ignoreAttachments
1993 List
<HandleAction
> actions
= new ArrayList
<>();
1994 if (content
!= null) {
1995 final SignalServiceAddress sender
;
1996 if (!envelope
.isUnidentifiedSender() && envelope
.hasSource()) {
1997 sender
= envelope
.getSourceAddress();
1999 sender
= content
.getSender();
2001 // Store uuid if we don't have it already
2002 resolveSignalServiceAddress(sender
);
2004 if (content
.getDataMessage().isPresent()) {
2005 SignalServiceDataMessage message
= content
.getDataMessage().get();
2007 if (content
.isNeedsReceipt()) {
2008 actions
.add(new SendReceiptAction(sender
, message
.getTimestamp()));
2011 actions
.addAll(handleSignalServiceDataMessage(message
,
2014 account
.getSelfAddress(),
2015 ignoreAttachments
));
2017 if (content
.getSyncMessage().isPresent()) {
2018 account
.setMultiDevice(true);
2019 SignalServiceSyncMessage syncMessage
= content
.getSyncMessage().get();
2020 if (syncMessage
.getSent().isPresent()) {
2021 SentTranscriptMessage message
= syncMessage
.getSent().get();
2022 final SignalServiceAddress destination
= message
.getDestination().orNull();
2023 if (destination
!= null) {
2024 actions
.addAll(handleSignalServiceDataMessage(message
.getMessage(),
2028 ignoreAttachments
));
2031 if (syncMessage
.getRequest().isPresent()) {
2032 RequestMessage rm
= syncMessage
.getRequest().get();
2033 if (rm
.isContactsRequest()) {
2034 actions
.add(SendSyncContactsAction
.create());
2036 if (rm
.isGroupsRequest()) {
2037 actions
.add(SendSyncGroupsAction
.create());
2039 if (rm
.isBlockedListRequest()) {
2040 actions
.add(SendSyncBlockedListAction
.create());
2042 // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest();
2044 if (syncMessage
.getGroups().isPresent()) {
2045 File tmpFile
= null;
2047 tmpFile
= IOUtils
.createTempFile();
2048 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(syncMessage
.getGroups()
2050 .asPointer(), tmpFile
)) {
2051 DeviceGroupsInputStream s
= new DeviceGroupsInputStream(attachmentAsStream
);
2053 while ((g
= s
.read()) != null) {
2054 GroupInfoV1 syncGroup
= account
.getGroupStore()
2055 .getOrCreateGroupV1(GroupId
.v1(g
.getId()));
2056 if (syncGroup
!= null) {
2057 if (g
.getName().isPresent()) {
2058 syncGroup
.name
= g
.getName().get();
2060 syncGroup
.addMembers(g
.getMembers()
2062 .map(this::resolveSignalServiceAddress
)
2063 .collect(Collectors
.toSet()));
2064 if (!g
.isActive()) {
2065 syncGroup
.removeMember(account
.getSelfAddress());
2067 // Add ourself to the member set as it's marked as active
2068 syncGroup
.addMembers(Collections
.singleton(account
.getSelfAddress()));
2070 syncGroup
.blocked
= g
.isBlocked();
2071 if (g
.getColor().isPresent()) {
2072 syncGroup
.color
= g
.getColor().get();
2075 if (g
.getAvatar().isPresent()) {
2076 retrieveGroupAvatarAttachment(g
.getAvatar().get(), syncGroup
.getGroupId());
2078 syncGroup
.inboxPosition
= g
.getInboxPosition().orNull();
2079 syncGroup
.archived
= g
.isArchived();
2080 account
.getGroupStore().updateGroup(syncGroup
);
2084 } catch (Exception e
) {
2085 e
.printStackTrace();
2087 if (tmpFile
!= null) {
2089 Files
.delete(tmpFile
.toPath());
2090 } catch (IOException e
) {
2091 System
.err
.println("Failed to delete received groups temp file “"
2099 if (syncMessage
.getBlockedList().isPresent()) {
2100 final BlockedListMessage blockedListMessage
= syncMessage
.getBlockedList().get();
2101 for (SignalServiceAddress address
: blockedListMessage
.getAddresses()) {
2102 setContactBlocked(resolveSignalServiceAddress(address
), true);
2104 for (GroupId groupId
: blockedListMessage
.getGroupIds()
2106 .map(GroupId
::unknownVersion
)
2107 .collect(Collectors
.toSet())) {
2109 setGroupBlocked(groupId
, true);
2110 } catch (GroupNotFoundException e
) {
2111 System
.err
.println("BlockedListMessage contained groupID that was not found in GroupStore: "
2112 + groupId
.toBase64());
2116 if (syncMessage
.getContacts().isPresent()) {
2117 File tmpFile
= null;
2119 tmpFile
= IOUtils
.createTempFile();
2120 final ContactsMessage contactsMessage
= syncMessage
.getContacts().get();
2121 try (InputStream attachmentAsStream
= retrieveAttachmentAsStream(contactsMessage
.getContactsStream()
2122 .asPointer(), tmpFile
)) {
2123 DeviceContactsInputStream s
= new DeviceContactsInputStream(attachmentAsStream
);
2124 if (contactsMessage
.isComplete()) {
2125 account
.getContactStore().clear();
2128 while ((c
= s
.read()) != null) {
2129 if (c
.getAddress().matches(account
.getSelfAddress()) && c
.getProfileKey().isPresent()) {
2130 account
.setProfileKey(c
.getProfileKey().get());
2132 final SignalServiceAddress address
= resolveSignalServiceAddress(c
.getAddress());
2133 ContactInfo contact
= account
.getContactStore().getContact(address
);
2134 if (contact
== null) {
2135 contact
= new ContactInfo(address
);
2137 if (c
.getName().isPresent()) {
2138 contact
.name
= c
.getName().get();
2140 if (c
.getColor().isPresent()) {
2141 contact
.color
= c
.getColor().get();
2143 if (c
.getProfileKey().isPresent()) {
2144 account
.getProfileStore().storeProfileKey(address
, c
.getProfileKey().get());
2146 if (c
.getVerified().isPresent()) {
2147 final VerifiedMessage verifiedMessage
= c
.getVerified().get();
2148 account
.getSignalProtocolStore()
2149 .setIdentityTrustLevel(verifiedMessage
.getDestination(),
2150 verifiedMessage
.getIdentityKey(),
2151 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
2153 if (c
.getExpirationTimer().isPresent()) {
2154 contact
.messageExpirationTime
= c
.getExpirationTimer().get();
2156 contact
.blocked
= c
.isBlocked();
2157 contact
.inboxPosition
= c
.getInboxPosition().orNull();
2158 contact
.archived
= c
.isArchived();
2159 account
.getContactStore().updateContact(contact
);
2161 if (c
.getAvatar().isPresent()) {
2162 retrieveContactAvatarAttachment(c
.getAvatar().get(), contact
.number
);
2166 } catch (Exception e
) {
2167 e
.printStackTrace();
2169 if (tmpFile
!= null) {
2171 Files
.delete(tmpFile
.toPath());
2172 } catch (IOException e
) {
2173 System
.err
.println("Failed to delete received contacts temp file “"
2181 if (syncMessage
.getVerified().isPresent()) {
2182 final VerifiedMessage verifiedMessage
= syncMessage
.getVerified().get();
2183 account
.getSignalProtocolStore()
2184 .setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage
.getDestination()),
2185 verifiedMessage
.getIdentityKey(),
2186 TrustLevel
.fromVerifiedState(verifiedMessage
.getVerified()));
2188 if (syncMessage
.getStickerPackOperations().isPresent()) {
2189 final List
<StickerPackOperationMessage
> stickerPackOperationMessages
= syncMessage
.getStickerPackOperations()
2191 for (StickerPackOperationMessage m
: stickerPackOperationMessages
) {
2192 if (!m
.getPackId().isPresent()) {
2195 Sticker sticker
= account
.getStickerStore().getSticker(m
.getPackId().get());
2196 if (sticker
== null) {
2197 if (!m
.getPackKey().isPresent()) {
2200 sticker
= new Sticker(m
.getPackId().get(), m
.getPackKey().get());
2202 sticker
.setInstalled(!m
.getType().isPresent()
2203 || m
.getType().get() == StickerPackOperationMessage
.Type
.INSTALL
);
2204 account
.getStickerStore().updateSticker(sticker
);
2207 if (syncMessage
.getConfiguration().isPresent()) {
2215 private File
getContactAvatarFile(String number
) {
2216 return new File(pathConfig
.getAvatarsPath(), "contact-" + number
);
2219 private File
retrieveContactAvatarAttachment(
2220 SignalServiceAttachment attachment
, String number
2221 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2222 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2223 if (attachment
.isPointer()) {
2224 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
2225 return retrieveAttachment(pointer
, getContactAvatarFile(number
), false);
2227 SignalServiceAttachmentStream stream
= attachment
.asStream();
2228 return Utils
.retrieveAttachment(stream
, getContactAvatarFile(number
));
2232 private File
getGroupAvatarFile(GroupId groupId
) {
2233 return new File(pathConfig
.getAvatarsPath(), "group-" + groupId
.toBase64().replace("/", "_"));
2236 private File
retrieveGroupAvatarAttachment(
2237 SignalServiceAttachment attachment
, GroupId groupId
2238 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2239 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2240 if (attachment
.isPointer()) {
2241 SignalServiceAttachmentPointer pointer
= attachment
.asPointer();
2242 return retrieveAttachment(pointer
, getGroupAvatarFile(groupId
), false);
2244 SignalServiceAttachmentStream stream
= attachment
.asStream();
2245 return Utils
.retrieveAttachment(stream
, getGroupAvatarFile(groupId
));
2249 private File
getProfileAvatarFile(SignalServiceAddress address
) {
2250 return new File(pathConfig
.getAvatarsPath(), "profile-" + address
.getLegacyIdentifier());
2253 private File
retrieveProfileAvatar(
2254 SignalServiceAddress address
, String avatarPath
, ProfileKey profileKey
2255 ) throws IOException
{
2256 IOUtils
.createPrivateDirectories(pathConfig
.getAvatarsPath());
2257 SignalServiceMessageReceiver receiver
= getOrCreateMessageReceiver();
2258 File outputFile
= getProfileAvatarFile(address
);
2260 File tmpFile
= IOUtils
.createTempFile();
2261 try (InputStream input
= receiver
.retrieveProfileAvatar(avatarPath
,
2264 ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
2265 // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
2266 IOUtils
.copyStreamToFile(input
, outputFile
, (int) ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
);
2269 Files
.delete(tmpFile
.toPath());
2270 } catch (IOException e
) {
2271 System
.err
.println("Failed to delete received avatar temp file “" + tmpFile
+ "”: " + e
.getMessage());
2277 public File
getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId
) {
2278 return new File(pathConfig
.getAttachmentsPath(), attachmentId
.toString());
2281 private File
retrieveAttachment(SignalServiceAttachmentPointer pointer
) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2282 IOUtils
.createPrivateDirectories(pathConfig
.getAttachmentsPath());
2283 return retrieveAttachment(pointer
, getAttachmentFile(pointer
.getRemoteId()), true);
2286 private File
retrieveAttachment(
2287 SignalServiceAttachmentPointer pointer
, File outputFile
, boolean storePreview
2288 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2289 if (storePreview
&& pointer
.getPreview().isPresent()) {
2290 File previewFile
= new File(outputFile
+ ".preview");
2291 try (OutputStream output
= new FileOutputStream(previewFile
)) {
2292 byte[] preview
= pointer
.getPreview().get();
2293 output
.write(preview
, 0, preview
.length
);
2294 } catch (FileNotFoundException e
) {
2295 e
.printStackTrace();
2300 final SignalServiceMessageReceiver messageReceiver
= getOrCreateMessageReceiver();
2302 File tmpFile
= IOUtils
.createTempFile();
2303 try (InputStream input
= messageReceiver
.retrieveAttachment(pointer
,
2305 ServiceConfig
.MAX_ATTACHMENT_SIZE
)) {
2306 IOUtils
.copyStreamToFile(input
, outputFile
);
2309 Files
.delete(tmpFile
.toPath());
2310 } catch (IOException e
) {
2311 System
.err
.println("Failed to delete received attachment temp file “"
2320 private InputStream
retrieveAttachmentAsStream(
2321 SignalServiceAttachmentPointer pointer
, File tmpFile
2322 ) throws IOException
, InvalidMessageException
, MissingConfigurationException
{
2323 final SignalServiceMessageReceiver messageReceiver
= getOrCreateMessageReceiver();
2324 return messageReceiver
.retrieveAttachment(pointer
, tmpFile
, ServiceConfig
.MAX_ATTACHMENT_SIZE
);
2327 void sendGroups() throws IOException
, UntrustedIdentityException
{
2328 File groupsFile
= IOUtils
.createTempFile();
2331 try (OutputStream fos
= new FileOutputStream(groupsFile
)) {
2332 DeviceGroupsOutputStream out
= new DeviceGroupsOutputStream(fos
);
2333 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
2334 if (record instanceof GroupInfoV1
) {
2335 GroupInfoV1 groupInfo
= (GroupInfoV1
) record;
2336 out
.write(new DeviceGroup(groupInfo
.getGroupId().serialize(),
2337 Optional
.fromNullable(groupInfo
.name
),
2338 new ArrayList
<>(groupInfo
.getMembers()),
2339 createGroupAvatarAttachment(groupInfo
.getGroupId()),
2340 groupInfo
.isMember(account
.getSelfAddress()),
2341 Optional
.of(groupInfo
.messageExpirationTime
),
2342 Optional
.fromNullable(groupInfo
.color
),
2344 Optional
.fromNullable(groupInfo
.inboxPosition
),
2345 groupInfo
.archived
));
2350 if (groupsFile
.exists() && groupsFile
.length() > 0) {
2351 try (FileInputStream groupsFileStream
= new FileInputStream(groupsFile
)) {
2352 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
2353 .withStream(groupsFileStream
)
2354 .withContentType("application/octet-stream")
2355 .withLength(groupsFile
.length())
2358 sendSyncMessage(SignalServiceSyncMessage
.forGroups(attachmentStream
));
2363 Files
.delete(groupsFile
.toPath());
2364 } catch (IOException e
) {
2365 System
.err
.println("Failed to delete groups temp file “" + groupsFile
+ "”: " + e
.getMessage());
2370 public void sendContacts() throws IOException
, UntrustedIdentityException
{
2371 File contactsFile
= IOUtils
.createTempFile();
2374 try (OutputStream fos
= new FileOutputStream(contactsFile
)) {
2375 DeviceContactsOutputStream out
= new DeviceContactsOutputStream(fos
);
2376 for (ContactInfo
record : account
.getContactStore().getContacts()) {
2377 VerifiedMessage verifiedMessage
= null;
2378 JsonIdentityKeyStore
.Identity currentIdentity
= account
.getSignalProtocolStore()
2379 .getIdentity(record.getAddress());
2380 if (currentIdentity
!= null) {
2381 verifiedMessage
= new VerifiedMessage(record.getAddress(),
2382 currentIdentity
.getIdentityKey(),
2383 currentIdentity
.getTrustLevel().toVerifiedState(),
2384 currentIdentity
.getDateAdded().getTime());
2387 ProfileKey profileKey
= account
.getProfileStore().getProfileKey(record.getAddress());
2388 out
.write(new DeviceContact(record.getAddress(),
2389 Optional
.fromNullable(record.name
),
2390 createContactAvatarAttachment(record.number
),
2391 Optional
.fromNullable(record.color
),
2392 Optional
.fromNullable(verifiedMessage
),
2393 Optional
.fromNullable(profileKey
),
2395 Optional
.of(record.messageExpirationTime
),
2396 Optional
.fromNullable(record.inboxPosition
),
2400 if (account
.getProfileKey() != null) {
2401 // Send our own profile key as well
2402 out
.write(new DeviceContact(account
.getSelfAddress(),
2407 Optional
.of(account
.getProfileKey()),
2415 if (contactsFile
.exists() && contactsFile
.length() > 0) {
2416 try (FileInputStream contactsFileStream
= new FileInputStream(contactsFile
)) {
2417 SignalServiceAttachmentStream attachmentStream
= SignalServiceAttachment
.newStreamBuilder()
2418 .withStream(contactsFileStream
)
2419 .withContentType("application/octet-stream")
2420 .withLength(contactsFile
.length())
2423 sendSyncMessage(SignalServiceSyncMessage
.forContacts(new ContactsMessage(attachmentStream
, true)));
2428 Files
.delete(contactsFile
.toPath());
2429 } catch (IOException e
) {
2430 System
.err
.println("Failed to delete contacts temp file “" + contactsFile
+ "”: " + e
.getMessage());
2435 void sendBlockedList() throws IOException
, UntrustedIdentityException
{
2436 List
<SignalServiceAddress
> addresses
= new ArrayList
<>();
2437 for (ContactInfo
record : account
.getContactStore().getContacts()) {
2438 if (record.blocked
) {
2439 addresses
.add(record.getAddress());
2442 List
<byte[]> groupIds
= new ArrayList
<>();
2443 for (GroupInfo
record : account
.getGroupStore().getGroups()) {
2444 if (record.isBlocked()) {
2445 groupIds
.add(record.getGroupId().serialize());
2448 sendSyncMessage(SignalServiceSyncMessage
.forBlocked(new BlockedListMessage(addresses
, groupIds
)));
2451 private void sendVerifiedMessage(
2452 SignalServiceAddress destination
, IdentityKey identityKey
, TrustLevel trustLevel
2453 ) throws IOException
, UntrustedIdentityException
{
2454 VerifiedMessage verifiedMessage
= new VerifiedMessage(destination
,
2456 trustLevel
.toVerifiedState(),
2457 System
.currentTimeMillis());
2458 sendSyncMessage(SignalServiceSyncMessage
.forVerified(verifiedMessage
));
2461 public List
<ContactInfo
> getContacts() {
2462 return account
.getContactStore().getContacts();
2465 public ContactInfo
getContact(String number
) {
2466 return account
.getContactStore().getContact(Util
.getSignalServiceAddressFromIdentifier(number
));
2469 public GroupInfo
getGroup(GroupId groupId
) {
2470 return account
.getGroupStore().getGroup(groupId
);
2473 public List
<JsonIdentityKeyStore
.Identity
> getIdentities() {
2474 return account
.getSignalProtocolStore().getIdentities();
2477 public List
<JsonIdentityKeyStore
.Identity
> getIdentities(String number
) throws InvalidNumberException
{
2478 return account
.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number
));
2482 * Trust this the identity with this fingerprint
2484 * @param name username of the identity
2485 * @param fingerprint Fingerprint
2487 public boolean trustIdentityVerified(String name
, byte[] fingerprint
) throws InvalidNumberException
{
2488 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2489 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2493 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2494 if (!Arrays
.equals(id
.getIdentityKey().serialize(), fingerprint
)) {
2498 account
.getSignalProtocolStore()
2499 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2501 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2502 } catch (IOException
| UntrustedIdentityException e
) {
2503 e
.printStackTrace();
2512 * Trust this the identity with this safety number
2514 * @param name username of the identity
2515 * @param safetyNumber Safety number
2517 public boolean trustIdentityVerifiedSafetyNumber(String name
, String safetyNumber
) throws InvalidNumberException
{
2518 SignalServiceAddress address
= canonicalizeAndResolveSignalServiceAddress(name
);
2519 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2523 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2524 if (!safetyNumber
.equals(computeSafetyNumber(address
, id
.getIdentityKey()))) {
2528 account
.getSignalProtocolStore()
2529 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2531 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_VERIFIED
);
2532 } catch (IOException
| UntrustedIdentityException e
) {
2533 e
.printStackTrace();
2542 * Trust all keys of this identity without verification
2544 * @param name username of the identity
2546 public boolean trustIdentityAllKeys(String name
) {
2547 SignalServiceAddress address
= resolveSignalServiceAddress(name
);
2548 List
<JsonIdentityKeyStore
.Identity
> ids
= account
.getSignalProtocolStore().getIdentities(address
);
2552 for (JsonIdentityKeyStore
.Identity id
: ids
) {
2553 if (id
.getTrustLevel() == TrustLevel
.UNTRUSTED
) {
2554 account
.getSignalProtocolStore()
2555 .setIdentityTrustLevel(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2557 sendVerifiedMessage(address
, id
.getIdentityKey(), TrustLevel
.TRUSTED_UNVERIFIED
);
2558 } catch (IOException
| UntrustedIdentityException e
) {
2559 e
.printStackTrace();
2567 public String
computeSafetyNumber(
2568 SignalServiceAddress theirAddress
, IdentityKey theirIdentityKey
2570 return Utils
.computeSafetyNumber(account
.getSelfAddress(),
2571 getIdentityKeyPair().getPublicKey(),
2576 void saveAccount() {
2580 public SignalServiceAddress
canonicalizeAndResolveSignalServiceAddress(String identifier
) throws InvalidNumberException
{
2581 String canonicalizedNumber
= UuidUtil
.isUuid(identifier
)
2583 : Util
.canonicalizeNumber(identifier
, account
.getUsername());
2584 return resolveSignalServiceAddress(canonicalizedNumber
);
2587 public SignalServiceAddress
resolveSignalServiceAddress(String identifier
) {
2588 SignalServiceAddress address
= Util
.getSignalServiceAddressFromIdentifier(identifier
);
2590 return resolveSignalServiceAddress(address
);
2593 public SignalServiceAddress
resolveSignalServiceAddress(SignalServiceAddress address
) {
2594 if (address
.matches(account
.getSelfAddress())) {
2595 return account
.getSelfAddress();
2598 return account
.getRecipientStore().resolveServiceAddress(address
);
2602 public void close() throws IOException
{
2603 if (messagePipe
!= null) {
2604 messagePipe
.shutdown();
2608 if (unidentifiedMessagePipe
!= null) {
2609 unidentifiedMessagePipe
.shutdown();
2610 unidentifiedMessagePipe
= null;
2616 public interface ReceiveMessageHandler
{
2618 void handleMessage(SignalServiceEnvelope envelope
, SignalServiceContent decryptedContent
, Throwable e
);